Compare commits
3 Commits
be21f0ab5a
...
04e1592fe3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04e1592fe3 | ||
|
|
240d8534fd | ||
|
|
327b745055 |
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,7 +3,6 @@
|
||||||
*.jpeg
|
*.jpeg
|
||||||
*.png
|
*.png
|
||||||
*.txt
|
*.txt
|
||||||
*.json
|
|
||||||
*.xlsx
|
*.xlsx
|
||||||
报销输出/
|
报销输出/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
|
||||||
177
差旅报销.py
177
差旅报销.py
|
|
@ -14,8 +14,11 @@ import argparse
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import zipfile
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
import pdfplumber
|
import pdfplumber
|
||||||
|
|
||||||
|
|
@ -218,10 +221,12 @@ def command_fill(args):
|
||||||
output_dir = json_path.parent
|
output_dir = json_path.parent
|
||||||
archive_dir = output_dir.parent if output_dir.name == OUTPUT_DIR_NAME else Path(".")
|
archive_dir = output_dir.parent if output_dir.name == OUTPUT_DIR_NAME else Path(".")
|
||||||
|
|
||||||
output_file = fill_template(result, output_dir)
|
output_file = fill_template(result, archive_dir)
|
||||||
verify_output(output_file, result)
|
verify_output(output_file, result)
|
||||||
final_archive_dir = finalize_archive_dir(archive_dir, result)
|
final_archive_dir = finalize_archive_dir(archive_dir, result)
|
||||||
final_output_file = final_archive_dir / OUTPUT_DIR_NAME / output_file.name
|
final_output_file = final_archive_dir / output_file.name
|
||||||
|
cleanup_output_dir(final_archive_dir / OUTPUT_DIR_NAME)
|
||||||
|
cleanup_pycache()
|
||||||
|
|
||||||
print(f"完成:{final_output_file}")
|
print(f"完成:{final_output_file}")
|
||||||
print(f"本次归档目录:{final_archive_dir}")
|
print(f"本次归档目录:{final_archive_dir}")
|
||||||
|
|
@ -305,35 +310,119 @@ def ensure_fill_dependencies():
|
||||||
|
|
||||||
def fill_template(result, output_dir):
|
def fill_template(result, output_dir):
|
||||||
output_dir.mkdir(exist_ok=True)
|
output_dir.mkdir(exist_ok=True)
|
||||||
import openpyxl
|
|
||||||
|
|
||||||
meta = result["meta"]
|
meta = result["meta"]
|
||||||
expenses = result["expenses"]
|
|
||||||
output_file = output_dir / build_output_filename(meta)
|
output_file = output_dir / build_output_filename(meta)
|
||||||
|
|
||||||
shutil.copy(TEMPLATE_FILE, output_file)
|
shutil.copy(TEMPLATE_FILE, output_file)
|
||||||
wb = openpyxl.load_workbook(output_file)
|
update_xlsx_cells(output_file, build_cell_values(result), build_formula_cached_values(result))
|
||||||
ws = wb.active
|
|
||||||
|
|
||||||
ws["E3"] = meta["出差事由"]
|
|
||||||
ws["A6"] = f'{meta["出发日期"]}-{meta["结束日期"]}'
|
|
||||||
ws["B6"] = f'{meta["出发地"]}-{meta["目的地"]}'
|
|
||||||
ws["C6"] = f'{meta["出差天数"]}天'
|
|
||||||
ws["D6"] = meta["出行方式"]
|
|
||||||
|
|
||||||
ws["H6"] = expenses["住宿"]["单据张数"]
|
|
||||||
ws["I6"] = expenses["住宿"]["金额"]
|
|
||||||
ws["H7"] = expenses["饮食"]["单据张数"]
|
|
||||||
ws["I7"] = expenses["饮食"]["金额"]
|
|
||||||
ws["H8"] = expenses["交通"]["单据张数"]
|
|
||||||
ws["I8"] = expenses["交通"]["金额"]
|
|
||||||
ws["H9"] = expenses["其他"]["单据张数"]
|
|
||||||
ws["I9"] = expenses["其他"]["金额"]
|
|
||||||
|
|
||||||
wb.save(output_file)
|
|
||||||
return output_file
|
return output_file
|
||||||
|
|
||||||
|
|
||||||
|
def build_cell_values(result):
|
||||||
|
meta = result["meta"]
|
||||||
|
expenses = result["expenses"]
|
||||||
|
return {
|
||||||
|
"E3": meta["出差事由"],
|
||||||
|
"A6": f'{meta["出发日期"]}-{meta["结束日期"]}',
|
||||||
|
"B6": f'{meta["出发地"]}-{meta["目的地"]}',
|
||||||
|
"C6": f'{meta["出差天数"]}天',
|
||||||
|
"D6": meta["出行方式"],
|
||||||
|
"F6": meta["出差天数"],
|
||||||
|
"H6": expenses["住宿"]["单据张数"],
|
||||||
|
"I6": expenses["住宿"]["金额"],
|
||||||
|
"H7": expenses["饮食"]["单据张数"],
|
||||||
|
"I7": expenses["饮食"]["金额"],
|
||||||
|
"H8": expenses["交通"]["单据张数"],
|
||||||
|
"I8": expenses["交通"]["金额"],
|
||||||
|
"H9": expenses["其他"]["单据张数"],
|
||||||
|
"I9": expenses["其他"]["金额"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_formula_cached_values(result):
|
||||||
|
summary = result["summary"]
|
||||||
|
advance = summary["预借差旅费"]
|
||||||
|
total = summary["报销总额"]
|
||||||
|
return {
|
||||||
|
"H2": summary["附件总张数"],
|
||||||
|
"F9": result["meta"]["出差天数"],
|
||||||
|
"C10": total,
|
||||||
|
"H10": max(total - advance, 0),
|
||||||
|
"H11": max(advance - total, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def update_xlsx_cells(xlsx_path, cell_values, formula_cached_values):
|
||||||
|
temp_file = NamedTemporaryFile(delete=False, suffix=".xlsx", dir=xlsx_path.parent)
|
||||||
|
temp_path = Path(temp_file.name)
|
||||||
|
temp_file.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(xlsx_path, "r") as source:
|
||||||
|
with zipfile.ZipFile(temp_path, "w", compression=zipfile.ZIP_DEFLATED) as target:
|
||||||
|
for item in source.infolist():
|
||||||
|
data = source.read(item.filename)
|
||||||
|
if item.filename == "xl/worksheets/sheet1.xml":
|
||||||
|
data = update_sheet_xml(data, cell_values, formula_cached_values)
|
||||||
|
target.writestr(item, data)
|
||||||
|
temp_path.replace(xlsx_path)
|
||||||
|
except Exception:
|
||||||
|
temp_path.unlink(missing_ok=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def update_sheet_xml(sheet_xml, cell_values, formula_cached_values):
|
||||||
|
namespace = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||||
|
ET.register_namespace("", namespace)
|
||||||
|
root = ET.fromstring(sheet_xml)
|
||||||
|
for cell_ref, value in cell_values.items():
|
||||||
|
cell = root.find(f".//{{{namespace}}}c[@r='{cell_ref}']")
|
||||||
|
if cell is None:
|
||||||
|
raise SystemExit(f"模板缺少单元格:{cell_ref}")
|
||||||
|
set_cell_xml_value(cell, value, namespace)
|
||||||
|
for cell_ref, value in formula_cached_values.items():
|
||||||
|
cell = root.find(f".//{{{namespace}}}c[@r='{cell_ref}']")
|
||||||
|
if cell is None:
|
||||||
|
raise SystemExit(f"模板缺少单元格:{cell_ref}")
|
||||||
|
set_formula_cached_value(cell, value, namespace)
|
||||||
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def set_cell_xml_value(cell, value, namespace):
|
||||||
|
for child in list(cell):
|
||||||
|
if child.tag in {f"{{{namespace}}}v", f"{{{namespace}}}f", f"{{{namespace}}}is"}:
|
||||||
|
cell.remove(child)
|
||||||
|
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
cell.attrib.pop("t", None)
|
||||||
|
value_node = ET.SubElement(cell, f"{{{namespace}}}v")
|
||||||
|
value_node.text = format_number_for_excel(value)
|
||||||
|
else:
|
||||||
|
cell.set("t", "inlineStr")
|
||||||
|
inline_string = ET.SubElement(cell, f"{{{namespace}}}is")
|
||||||
|
text_node = ET.SubElement(inline_string, f"{{{namespace}}}t")
|
||||||
|
text_node.text = str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_formula_cached_value(cell, value, namespace):
|
||||||
|
cell.attrib.pop("t", None)
|
||||||
|
value_node = None
|
||||||
|
for child in cell:
|
||||||
|
if child.tag == f"{{{namespace}}}v":
|
||||||
|
value_node = child
|
||||||
|
break
|
||||||
|
if value_node is None:
|
||||||
|
value_node = ET.SubElement(cell, f"{{{namespace}}}v")
|
||||||
|
value_node.text = format_number_for_excel(value)
|
||||||
|
|
||||||
|
|
||||||
|
def format_number_for_excel(value):
|
||||||
|
if isinstance(value, int):
|
||||||
|
return str(value)
|
||||||
|
return f"{float(value):.10f}".rstrip("0").rstrip(".")
|
||||||
|
|
||||||
|
|
||||||
def finalize_archive_dir(archive_dir, result):
|
def finalize_archive_dir(archive_dir, result):
|
||||||
if archive_dir == Path("."):
|
if archive_dir == Path("."):
|
||||||
return archive_dir
|
return archive_dir
|
||||||
|
|
@ -358,6 +447,17 @@ def unique_archive_dir(path):
|
||||||
index += 1
|
index += 1
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_output_dir(output_dir):
|
||||||
|
if output_dir.exists():
|
||||||
|
shutil.rmtree(output_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_pycache():
|
||||||
|
pycache_dir = Path("__pycache__")
|
||||||
|
if pycache_dir.exists():
|
||||||
|
shutil.rmtree(pycache_dir)
|
||||||
|
|
||||||
|
|
||||||
def build_archive_dir_name(meta):
|
def build_archive_dir_name(meta):
|
||||||
start_date = normalize_date_for_filename(meta["出发日期"])
|
start_date = normalize_date_for_filename(meta["出发日期"])
|
||||||
end_date = normalize_date_for_filename(meta["结束日期"])
|
end_date = normalize_date_for_filename(meta["结束日期"])
|
||||||
|
|
@ -421,6 +521,37 @@ def verify_output(output_file, result):
|
||||||
if not isinstance(value, str) or not value.startswith("="):
|
if not isinstance(value, str) or not value.startswith("="):
|
||||||
raise SystemExit(f"输出校验失败:{label} 单元格 {cell} 公式丢失,当前值={value!r}")
|
raise SystemExit(f"输出校验失败:{label} 单元格 {cell} 公式丢失,当前值={value!r}")
|
||||||
|
|
||||||
|
verify_xlsx_styles_preserved(output_file)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_xlsx_styles_preserved(output_file):
|
||||||
|
with zipfile.ZipFile(TEMPLATE_FILE, "r") as template, zipfile.ZipFile(output_file, "r") as output:
|
||||||
|
if template.read("xl/styles.xml") != output.read("xl/styles.xml"):
|
||||||
|
raise SystemExit("输出校验失败:样式表 xl/styles.xml 与模板不一致")
|
||||||
|
|
||||||
|
template_styles = read_sheet_cell_style_ids(template.read("xl/worksheets/sheet1.xml"))
|
||||||
|
output_styles = read_sheet_cell_style_ids(output.read("xl/worksheets/sheet1.xml"))
|
||||||
|
for cell_ref in filled_cell_refs():
|
||||||
|
if template_styles.get(cell_ref) != output_styles.get(cell_ref):
|
||||||
|
raise SystemExit(
|
||||||
|
f"输出校验失败:单元格 {cell_ref} 样式编号变化,"
|
||||||
|
f"模板={template_styles.get(cell_ref)!r} 输出={output_styles.get(cell_ref)!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_sheet_cell_style_ids(sheet_xml):
|
||||||
|
namespace = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||||
|
root = ET.fromstring(sheet_xml)
|
||||||
|
return {
|
||||||
|
cell.attrib["r"]: cell.attrib.get("s")
|
||||||
|
for cell in root.findall(f".//{{{namespace}}}c")
|
||||||
|
if "r" in cell.attrib
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def filled_cell_refs():
|
||||||
|
return ["E3", "A6", "B6", "C6", "D6", "H6", "I6", "H7", "I7", "H8", "I8", "H9", "I9"]
|
||||||
|
|
||||||
|
|
||||||
def build_parser():
|
def build_parser():
|
||||||
parser = argparse.ArgumentParser(description="差旅报销自动化脚本")
|
parser = argparse.ArgumentParser(description="差旅报销自动化脚本")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user