Files
2026-05-31 19:19:54 +00:00

440 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
E.ON Stromrechnung Generator
策略pymupdf redact透明背景 + insert_text完全保留原PDF图形/背景
依赖pip install PyQt5 pymupdf
用法将原始PDF命名为 eon_original.pdf与此脚本同目录
作者Claude AI2026-06
免责声明仅供学习使用生成的PDF仅供测试和演示用途请勿用于任何非法用途。作者不对任何使用此脚本生成的PDF承担责任。
"""
import sys, os
from datetime import datetime, date
import fitz # pymupdf
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QGridLayout, QLabel, QLineEdit, QPushButton, QGroupBox,
QFileDialog, QMessageBox, QScrollArea, QComboBox, QDateEdit
)
from PyQt5.QtCore import Qt, QDate
def get_resource_path(relative_path):
""" 获取资源文件的绝对路径(兼容开发和打包环境) """
try:
# 如果是打包环境,资源会被解压到临时目录 _MEIPASS
base_path = sys._MEIPASS
except Exception:
# 如果是开发环境,使用当前脚本所在目录
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
DE_MONTHS = ["","Januar","Februar","März","April","Mai","Juni",
"Juli","August","September","Oktober","November","Dezember"]
def fmt_de_date(d: date) -> str:
return f"{d.day:02d}, {DE_MONTHS[d.month]} {d.year}"
def fmt_de_month_year(d: date) -> str:
return f"{DE_MONTHS[d.month]} {d.year}"
ORIG_PDF = os.path.join(os.path.dirname(os.path.abspath(__file__)), "eon_original.pdf")
def generate_eon_statement(cfg, output_path):
doc = fitz.open(ORIG_PDF)
page = doc[0]
# 使用系统字体,确保€等特殊字符正确渲染
FONT_R = os.path.join(os.path.dirname(os.path.abspath(__file__)), "liberation_sans/LiberationSans-Regular.ttf")
FONT_B = os.path.join(os.path.dirname(os.path.abspath(__file__)), "liberation_sans/LiberationSans-Bold.ttf")
font_reg = fitz.Font(fontfile=FONT_R)
font_bold = fitz.Font(fontfile=FONT_B)
def insert(page, x, y, text, bold=False, fontsize=7.9):
"""用 TextWriter 写入文字,支持€等特殊字符"""
font = font_bold if bold else font_reg
tw = fitz.TextWriter(page.rect)
tw.append((x, y), text, font=font, fontsize=fontsize)
tw.write_text(page)
def text_width(text, bold=False, fontsize=7.9):
font = font_bold if bold else font_reg
return font.text_length(text, fontsize)
def replace(old, new, bold=False, fontsize=7.9, right_align_x=None):
"""找到 old 文字,透明 redact写入 new"""
areas = page.search_for(old)
if not areas:
print(f" WARNING not found: {old!r}")
return
rect = areas[0]
page.add_redact_annot(rect, fill=None) # fill=None → 透明,保留背景
page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_NONE)
if right_align_x:
x = right_align_x - text_width(new, bold=bold, fontsize=fontsize)
else:
x = rect.x0
insert(page, x, rect.y1 - 1.5, new, bold=bold, fontsize=fontsize)
def replace_partial(old_full, old_part, new_part, bold=False, fontsize=7.9):
"""
替换一行中的部分内容(如只改年份)。
找到整行 old_full重写为把 old_part 替换成 new_part 的版本。
"""
new_full = old_full.replace(old_part, new_part, 1)
replace(old_full, new_full, bold=bold, fontsize=fontsize)
# ── 收件人地址 ────────────────────────────────
replace("DASDEN RX INC", cfg["firma_name"], bold=True)
replace("UNTER DEN LINDEN 67/24", cfg["firma_strasse"])
replace("10117 BERLIN, GERMANY", cfg["firma_stadt"])
# ── 用电客户 ──────────────────────────────────
replace("Franziska Rabe", cfg["kunde_name"])
replace("Wladislaw Str. 1, 15517 Fürstenwalde", cfg["kunde_adresse"])
# ── 称谓 ──────────────────────────────────────
replace("Sehr geehrter Herr Marx,",
f"Sehr geehrter {cfg['anrede_titel']} {cfg['anrede_name']},")
# ── 账单标题编号 ──────────────────────────────
# "Ihre Stromrechnung 0000" → 替换 "0000"
replace_partial("Ihre Stromrechnung 0000", "0000",
cfg["rechnungsnummer_kurz"], bold=False, fontsize=10.1)
# ── Zeitraum ──────────────────────────────────
replace("Für den Zeitraum vorn 1, Januar 2023 bis, Dezember 2023",
f"Für den Zeitraum vorn {cfg['zeitraum_von']} bis, {cfg['zeitraum_bis']}")
# ── Verbrauchsjahr 标题年份 ──────────────────
replace_partial("Ihr Verbrauchsjahr 2023", "2023",
cfg["verbrauchsjahr"], fontsize=10.1)
# ── 右侧元数据值 ──────────────────────────────
replace("236 859 567 47", cfg["vertragskonto"], bold=True)
replace("856 639 785 465", cfg["rechnungsnummer"])
replace("01, Januar 2023", cfg["rechnungsdatum"])
# ── 所有金额替换 ─────────────────────────────
# 统一用 replace_at 函数处理,右对齐到原文字右边
def replace_at(rect, new_text, bold=False, fontsize=7.9, right_align=True, extra_w=6):
r = fitz.Rect(rect.x0, rect.y0, rect.x1 + extra_w, rect.y1)
page.add_redact_annot(r, fill=None)
page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_NONE)
if right_align:
x = rect.x1 - text_width(new_text, bold=bold, fontsize=fontsize)
else:
x = rect.x0
insert(page, x, rect.y1 - 1.5, new_text, bold=bold, fontsize=fontsize)
# 表格Energiekosten右对齐到原 x1=154
areas = page.search_for("152.,25€")
if areas:
replace_at(areas[0], cfg["energiekosten"] + "", bold=True, fontsize=9.1)
# 表格Zahlungen原文字无€€是独立span在x=246~251右对齐到251
areas = page.search_for("227,00")
if areas:
r = areas[0]
# 扩展到包含€spanx1≈251
r2 = fitz.Rect(r.x0, r.y0, 253, r.y1)
page.add_redact_annot(r2, fill=None)
page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_NONE)
tw = fitz.get_text_length(cfg["zahlungen"] + "", fontname="hebo", fontsize=9.1)
x = 253 - text_width(cfg["zahlungen"] + "", bold=True, fontsize=9.1)
insert(page, x, r.y1 - 1.5, cfg["zahlungen"] + "", bold=True, fontsize=9.1)
# 表格 + 框内 Guthabensearch_for 返回多个)
guthaben_areas = page.search_for("74,75€")
abschlag_areas = page.search_for("16,00€")
gutschrift_areas = page.search_for("42,75€")
new_guthaben = cfg["guthaben"] + ""
new_abschlag = cfg["abschlag"] + ""
new_vorauszahl = cfg["vorauszahlung"] + ""
new_gutschrift = cfg["gutschrift"] + ""
# guthaben_areas: [0]=框内(top≈402), [1]=表格(top≈344) → 按y排序
guthaben_areas_sorted = sorted(guthaben_areas, key=lambda r: r.y0)
if len(guthaben_areas_sorted) >= 2:
replace_at(guthaben_areas_sorted[0], new_guthaben, bold=True, fontsize=9.1) # 表格
replace_at(guthaben_areas_sorted[1], new_guthaben) # 框内
elif len(guthaben_areas_sorted) == 1:
replace_at(guthaben_areas_sorted[0], new_guthaben)
if len(abschlag_areas) >= 1:
replace_at(abschlag_areas[0], new_abschlag)
if len(abschlag_areas) >= 2:
replace_at(abschlag_areas[1], new_vorauszahl)
if gutschrift_areas:
replace_at(gutschrift_areas[0], new_gutschrift, bold=True)
# ── 用电量数据 ────────────────────────────────
# "Ihr Energieverbrauch von 200 kWh war 43% höher als im"
kwh = cfg["verbrauch_kwh"]
proz = cfg["verbrauch_prozent"]
replace(
"Ihr Energieverbrauch von 200 kWh war 43% höher als im",
f"Ihr Energieverbrauch von {kwh} kWh war {proz}% höher als im"
)
# "2023 203 kWh"
# 这行有很多空格,直接重写
jahr_areas = page.search_for("2023")
# 找在 top≈596 附近的那个(服务框里的年份行)
for r in jahr_areas:
if 590 < r.y0 < 610:
replace_at(r, cfg["verbrauchsjahr"])
break
kwh_aktuell_areas = page.search_for("203 kWh")
if kwh_aktuell_areas:
replace_at(kwh_aktuell_areas[0], cfg["verbrauch_aktuell"] + " kWh")
# "2022/23 142kWh"
vorjahr_areas = page.search_for("2022/23")
if vorjahr_areas:
replace_at(vorjahr_areas[0], cfg["vorjahr_label"])
kwh_vorjahr_areas = page.search_for("142kWh")
if kwh_vorjahr_areas:
replace_at(kwh_vorjahr_areas[0], cfg["verbrauch_vorjahr"] + "kWh")
# ── 元数据(创建时间)────────────────────────
try:
created = datetime.strptime(cfg.get("created_at", ""), "%Y-%m-%d %H:%M:%S")
except ValueError:
created = datetime.now()
ts = created.strftime("D:%Y%m%d%H%M%S+00'00'")
doc.set_metadata({
"title": "E.ON Stromrechnung",
"author": "E.ON Energie Deutschland GmbH",
"creator": "E.ON Portal",
"producer": "E.ON Portal",
"creationDate": ts,
"modDate": ts,
})
doc.save(output_path, garbage=4, deflate=True)
doc.close()
# ══════════════════════════════════════════════
# PyQt5 UI
# ══════════════════════════════════════════════
def labeled(text, widget):
w = QWidget()
v = QVBoxLayout(w)
v.setContentsMargins(0, 0, 0, 0)
v.setSpacing(2)
lbl = QLabel(text)
lbl.setStyleSheet("color:#666;font-size:11px;")
v.addWidget(lbl)
v.addWidget(widget)
return w
def field(default=""): return QLineEdit(default)
def date_picker(y=2023, m=1, d=1):
w = QDateEdit(QDate(y, m, d))
w.setCalendarPopup(True)
w.setDisplayFormat("yyyy-MM-dd")
return w
def qdate_to_date(qd): return date(qd.year(), qd.month(), qd.day())
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("E.ON 账单生成器")
self.setMinimumWidth(820)
self.resize(900, 820)
title_bar = QLabel("仅供学习使用作者Claude AI")
title_bar.setAlignment(Qt.AlignCenter)
title_bar.setStyleSheet("background:#E3000F;color:white;font-size:13px;padding:8px 0;")
scroll = QScrollArea()
scroll.setWidgetResizable(True)
wrapper = QWidget()
wl = QVBoxLayout(wrapper)
wl.setContentsMargins(0, 0, 0, 0)
wl.setSpacing(0)
wl.addWidget(title_bar)
wl.addWidget(scroll)
self.setCentralWidget(wrapper)
container = QWidget()
scroll.setWidget(container)
root = QVBoxLayout(container)
root.setSpacing(14)
root.setContentsMargins(20, 20, 20, 20)
# ── 收件人 ─────────────────────────────
g1 = QGroupBox("收件人(信封地址)")
l1 = QGridLayout(g1)
self.f_firma_name = field("DASDEN RX INC")
self.f_firma_strasse = field("UNTER DEN LINDEN 67/24")
self.f_firma_stadt = field("10117 BERLIN, GERMANY")
l1.addWidget(labeled("公司/姓名", self.f_firma_name), 0, 0, 1, 2)
l1.addWidget(labeled("街道地址", self.f_firma_strasse), 1, 0)
l1.addWidget(labeled("城市/邮编", self.f_firma_stadt), 1, 1)
root.addWidget(g1)
# ── 用电客户 ───────────────────────────
g2 = QGroupBox("用电客户(可与收件人不同)")
l2 = QGridLayout(g2)
self.f_kunde_name = field("Franziska Rabe")
self.f_kunde_adresse = field("Wladislaw Str. 1, 15517 Fürstenwalde")
self.f_anrede_titel = QComboBox()
self.f_anrede_titel.addItems(["Herr", "Frau"])
self.f_anrede_name = field("Marx")
l2.addWidget(labeled("客户姓名", self.f_kunde_name), 0, 0)
l2.addWidget(labeled("用电地址", self.f_kunde_adresse), 0, 1)
l2.addWidget(labeled("称谓Herr/Frau", self.f_anrede_titel), 1, 0)
l2.addWidget(labeled("称谓中的姓", self.f_anrede_name), 1, 1)
root.addWidget(g2)
# ── 账单元数据 ─────────────────────────
g3 = QGroupBox("账单元数据")
l3 = QGridLayout(g3)
self.f_vertragskonto = field("236 859 567 47")
self.f_rechnungsnummer = field("856 639 785 465")
self.f_rechnungsnummer_kurz = field("0000")
self.f_rechnungsdatum = date_picker(2023, 1, 1)
self.f_zeitraum_von = date_picker(2023, 1, 1)
self.f_zeitraum_bis = date_picker(2023, 12, 31)
self.f_verbrauchsjahr = field("2023")
self.f_created = date_picker(2023, 1, 1)
l3.addWidget(labeled("合同账户号 Vertragskonto", self.f_vertragskonto), 0, 0)
l3.addWidget(labeled("账单号 Rechnungsnummer", self.f_rechnungsnummer), 0, 1)
l3.addWidget(labeled("账单标题编号(短)", self.f_rechnungsnummer_kurz), 1, 0)
l3.addWidget(labeled("账单日期 Rechnungsdatum", self.f_rechnungsdatum), 1, 1)
l3.addWidget(labeled("账期起始日期", self.f_zeitraum_von), 2, 0)
l3.addWidget(labeled("账期结束日期", self.f_zeitraum_bis), 2, 1)
l3.addWidget(labeled("用电年份", self.f_verbrauchsjahr), 3, 0)
l3.addWidget(labeled("PDF创建日期", self.f_created), 3, 1)
root.addWidget(g3)
# ── 账单金额 ───────────────────────────
g4 = QGroupBox("账单金额(填数字,不含€符号)")
l4 = QGridLayout(g4)
self.f_energiekosten = field("152.,25")
self.f_zahlungen = field("227,00")
self.f_guthaben = field("74,75")
self.f_abschlag = field("16,00")
self.f_vorauszahlung = field("16,00")
self.f_gutschrift = field("42,75")
l4.addWidget(labeled("能源费用 Energiekosten", self.f_energiekosten), 0, 0)
l4.addWidget(labeled("已付款 Zahlungen", self.f_zahlungen), 0, 1)
l4.addWidget(labeled("余额 Guthaben", self.f_guthaben), 0, 2)
l4.addWidget(labeled("首个新分期 Abschlag", self.f_abschlag), 1, 0)
l4.addWidget(labeled("预付款 Vorauszahlung", self.f_vorauszahlung), 1, 1)
l4.addWidget(labeled("退款总额 Gutschrift", self.f_gutschrift), 1, 2)
root.addWidget(g4)
# ── 用电量 ─────────────────────────────
g5 = QGroupBox("用电量信息")
l5 = QGridLayout(g5)
self.f_verbrauch_kwh = field("200")
self.f_verbrauch_prozent = field("43")
self.f_verbrauch_aktuell = field("203")
self.f_vorjahr_label = field("2022/23")
self.f_verbrauch_vorjahr = field("142")
l5.addWidget(labeled("本年用电量 (kWh)", self.f_verbrauch_kwh), 0, 0)
l5.addWidget(labeled("比上年增加 (%)", self.f_verbrauch_prozent), 0, 1)
l5.addWidget(labeled("本年折算日均 (kWh)", self.f_verbrauch_aktuell), 0, 2)
l5.addWidget(labeled("上年标签如2022/23", self.f_vorjahr_label), 1, 0)
l5.addWidget(labeled("上年折算日均 (kWh)", self.f_verbrauch_vorjahr), 1, 1)
root.addWidget(g5)
# ── 输出 ───────────────────────────────
bot = QHBoxLayout()
self.f_outpath = QLineEdit(os.path.expanduser("~/eon_rechnung.pdf"))
btn_browse = QPushButton("浏览…")
btn_browse.clicked.connect(self.browse)
btn_gen = QPushButton("生成 PDF")
btn_gen.setFixedHeight(36)
btn_gen.setStyleSheet(
"QPushButton{background:#E3000F;color:white;border-radius:6px;"
"font-size:14px;font-weight:bold;}"
"QPushButton:hover{background:#b50000;}")
btn_gen.clicked.connect(self.generate)
bot.addWidget(self.f_outpath, 1)
bot.addWidget(btn_browse)
bot.addWidget(btn_gen)
root.addLayout(bot)
hint = QLabel("⚠️ 请将原始PDF命名为 eon_original.pdf与此脚本放在同一目录")
hint.setStyleSheet("color:#888;font-size:11px;")
root.addWidget(hint)
root.addStretch()
def browse(self):
path, _ = QFileDialog.getSaveFileName(
self, "保存PDF", self.f_outpath.text(), "PDF Files (*.pdf)")
if path: self.f_outpath.setText(path)
def get_config(self):
rd = qdate_to_date(self.f_rechnungsdatum.date())
zvd = qdate_to_date(self.f_zeitraum_von.date())
zbd = qdate_to_date(self.f_zeitraum_bis.date())
cd = qdate_to_date(self.f_created.date())
return {
"firma_name": self.f_firma_name.text(),
"firma_strasse": self.f_firma_strasse.text(),
"firma_stadt": self.f_firma_stadt.text(),
"kunde_name": self.f_kunde_name.text(),
"kunde_adresse": self.f_kunde_adresse.text(),
"anrede_titel": self.f_anrede_titel.currentText(),
"anrede_name": self.f_anrede_name.text(),
"vertragskonto": self.f_vertragskonto.text(),
"rechnungsnummer": self.f_rechnungsnummer.text(),
"rechnungsnummer_kurz": self.f_rechnungsnummer_kurz.text(),
"rechnungsdatum": fmt_de_date(rd),
"zeitraum_von": fmt_de_date(zvd),
"zeitraum_bis": fmt_de_month_year(zbd),
"verbrauchsjahr": self.f_verbrauchsjahr.text(),
"created_at": f"{cd.year}-{cd.month:02d}-{cd.day:02d} 10:00:00",
"energiekosten": self.f_energiekosten.text(),
"zahlungen": self.f_zahlungen.text(),
"guthaben": self.f_guthaben.text(),
"abschlag": self.f_abschlag.text(),
"vorauszahlung": self.f_vorauszahlung.text(),
"gutschrift": self.f_gutschrift.text(),
"verbrauch_kwh": self.f_verbrauch_kwh.text(),
"verbrauch_prozent": self.f_verbrauch_prozent.text(),
"verbrauch_aktuell": self.f_verbrauch_aktuell.text(),
"vorjahr_label": self.f_vorjahr_label.text(),
"verbrauch_vorjahr": self.f_verbrauch_vorjahr.text(),
}
def generate(self):
if not os.path.exists(ORIG_PDF):
QMessageBox.critical(self, "错误",
f"找不到原始PDF\n{ORIG_PDF}\n\n请将原始PDF命名为 eon_original.pdf 放到脚本同目录")
return
cfg = self.get_config()
path = self.f_outpath.text().strip()
if not path:
QMessageBox.warning(self, "错误", "请指定输出路径")
return
try:
generate_eon_statement(cfg, path)
QMessageBox.information(self, "完成", f"PDF 已生成:\n{path}")
except Exception as e:
QMessageBox.critical(self, "生成失败", str(e))
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion")
win = MainWindow()
win.show()
sys.exit(app.exec_())