Files
Auto-Finance/src/doc/fill_consumable_doc.py
2026-06-11 19:22:34 +08:00

260 lines
7.9 KiB
Python
Raw 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.
"""
将 invoice_summary.csv 填入「易耗品、出库单.doc」表格。
仅写入表格数据单元格,保留原模板字体、边框与版式。
"""
from __future__ import annotations
import argparse
import re
import shutil
from datetime import date
from pathlib import Path
from typing import Any
from .. import get_logger
from ..config import load_config
from ..doc.invoice import load_invoice_csv
log = get_logger("fill_consumable_doc")
CONSUMABLE_DOC_FILENAME = "易耗品、出库单.doc"
# Word COM 常量
WD_CHARACTER = 1
# 表格统一字体宋体、五号10.5 磅)
TABLE_FONT_NAME = "宋体"
TABLE_FONT_SIZE = 10.5
def _split_name_spec(left: str) -> tuple[str, str]:
m = re.search(r"(\S+一批)\s*$", left)
if m:
return m.group(1), left[: m.start()].strip()
parts = left.split(" ", 1)
if len(parts) == 2:
return parts[0], parts[1]
return left, ""
def parse_spec_model(spec: str) -> dict[str, str]:
spec = (spec or "").strip()
if "" not in spec:
return {
"product_name": spec,
"spec": "",
"unit": "",
"qty": "",
"unit_price": "",
}
left, right = spec.split("", 1)
product_name, model_spec = _split_name_spec(left.strip())
tokens = right.split()
qty = ""
unit_price = ""
if len(tokens) >= 4 and re.fullmatch(r"\d+(?:\.\d+)?", tokens[0]):
qty, unit_price = tokens[0], tokens[1]
elif tokens and re.fullmatch(r"\d+(?:\.\d+)?", tokens[0]):
qty, unit_price = "1", tokens[0]
return {
"product_name": product_name,
"spec": model_spec,
"unit": "",
"qty": qty,
"unit_price": unit_price,
}
def _format_money(value: str | float) -> str:
"""单价、金额:固定保留两位小数。"""
if value is None or value == "":
return ""
try:
num = float(value)
except (TypeError, ValueError):
return str(value)
return f"{num:.2f}"
def _today_cn_date() -> str:
"""当前日期格式2026年5月26日"""
today = date.today()
return f"{today.year}{today.month}{today.day}"
def _apply_font(rng: Any) -> None:
"""将范围字体设为宋体五号(含数字与英文)。"""
font = rng.Font
font.Name = TABLE_FONT_NAME
font.NameFarEast = TABLE_FONT_NAME
font.NameAscii = TABLE_FONT_NAME
font.NameOther = TABLE_FONT_NAME
font.NameBi = TABLE_FONT_NAME
font.Size = TABLE_FONT_SIZE
def _set_cell_value(cell: Any, text: str) -> None:
"""写入单元格正文(不含末尾单元格标记)。"""
rng = cell.Range
rng.MoveEnd(WD_CHARACTER, -1)
rng.Text = "" if text is None else str(text)
_apply_font(rng)
def _normalize_table_font(tbl: Any) -> None:
"""填写完成后统一整张表的字体。"""
for row in tbl.Rows:
for cell in row.Cells:
rng = cell.Range
rng.MoveEnd(WD_CHARACTER, -1)
_apply_font(rng)
def _replace_date_in_doc(doc: Any, new_date: str) -> None:
"""仅替换表头段落中的日期文字,不改动段落其余部分。"""
if not new_date:
return
try:
para = doc.Paragraphs(3)
except Exception:
return
rng = para.Range
text = rng.Text.replace("\r", "").replace("\x07", "")
m = re.search(r"\d{4}\d{1,2}月\d{1,2}日", text)
if not m:
return
start = rng.Start + m.start()
end = rng.Start + m.end()
doc.Range(Start=start, End=end).Text = new_date
def fill_consumable_doc(
csv_path: str | Path,
doc_path: str | Path,
config: dict[str, Any] | None = None,
backup: bool = True,
) -> Path:
csv_path = Path(csv_path)
doc_path = Path(doc_path)
if config is None:
config = load_config()
invoices = load_invoice_csv(csv_path.parent / "invoice_summary.csv") or []
if backup:
bak = doc_path.with_suffix(doc_path.suffix + ".bak")
shutil.copy2(doc_path, bak)
import pythoncom
import win32com.client
pythoncom.CoInitialize()
try:
word = win32com.client.Dispatch("Word.Application")
word.Visible = False
word.DisplayAlerts = 0
doc = word.Documents.Open(str(doc_path.resolve()))
try:
_replace_date_in_doc(doc, _today_cn_date())
tbl = doc.Tables(1)
storage = config.get("consumable_storage", "躬行楼 C205")
for i, inv in enumerate(invoices):
row_idx = i + 2
if row_idx > tbl.Rows.Count:
break
parsed = parse_spec_model(str(inv.get("spec_model", "")))
# 当规格型号为空时,从项目名称提取产品信息
if not parsed["product_name"]:
item_name = str(inv.get("item_name", ""))
# 去除 "*分类*" 前缀(如 "*电子工业设备*元件盒" -> "元件盒"
if "*" in item_name:
item_name = item_name.split("*")[-1].strip()
parsed["product_name"] = item_name
card_amount_raw = inv.get("card_amount") or 0
card_amount: float = float(str(card_amount_raw).replace(",", ""))
qty_str = parsed["qty"]
qty_val = int(qty_str) if qty_str and qty_str.isdigit() else 0
# 金额填写刷卡金额,单价由刷卡金额反算
amount = _format_money(card_amount)
unit_price = _format_money(card_amount / qty_val) if qty_val > 0 else _format_money(card_amount)
# 数量:去掉前导零;若无数量则默认为 1
qty = str(qty_val) if qty_val > 0 else "1"
values = [
str(inv.get("index", i + 1)),
parsed["product_name"],
parsed["spec"],
parsed["unit"],
qty,
unit_price,
amount,
"", # 购货人签字 — 保持空白
storage,
"", # 领用人签字 — 保持空白
"", # 备注 — 保持空白,避免撑破版式
]
for col_idx, val in enumerate(values, start=1):
_set_cell_value(tbl.Cell(row_idx, col_idx), str(val))
_normalize_table_font(tbl)
doc.Save()
finally:
doc.Close()
word.Quit()
finally:
pythoncom.CoUninitialize()
return doc_path
def fill_consumable_from_template(
csv_path: str | Path,
template_path: str | Path,
output_path: str | Path,
config: dict[str, Any] | None = None,
) -> Path:
"""从模板复制并填写出库单Web 会话每次从模板重新生成)。"""
template_path = Path(template_path)
output_path = Path(output_path)
if not template_path.exists():
raise FileNotFoundError(f"出库单模板不存在: {template_path}")
shutil.copy2(template_path, output_path)
return fill_consumable_doc(csv_path, output_path, config=config, backup=False)
def main() -> None:
root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description="将发票 CSV 填入易耗品出库单")
parser.add_argument("--csv", default=str(root / "invoice_summary.csv"))
parser.add_argument("--doc", default=str(root / "易耗品、出库单.doc"))
parser.add_argument("--config", default=str(root / "scripts" / "data" / "config.json"))
parser.add_argument("--no-backup", action="store_true")
args = parser.parse_args()
cfg = None
if Path(args.config).exists():
import json
with open(args.config, encoding="utf-8") as f:
cfg = {**load_config(), **json.load(f)}
out = fill_consumable_doc(args.csv, args.doc, config=cfg, backup=not args.no_backup)
print(f"已填写并保存: {out}")
if __name__ == "__main__":
main()