"""Procurement-order ledger import and supplier cooperation aggregation.""" from __future__ import annotations import hashlib import re from datetime import date, datetime from pathlib import Path from typing import Any from openpyxl import load_workbook from sqlalchemy import func, select from sqlalchemy.dialects.postgresql import insert from sqlalchemy.ext.asyncio import AsyncSession from app.models.entities import PurchaseOrderLine def clean_text(value: Any) -> str | None: if value is None: return None text = str(value).replace("\u200b", "").strip() return text or None def parse_date(value: Any) -> date | None: if isinstance(value, datetime): return value.date() if isinstance(value, date): return value text = clean_text(value) if not text: return None text = re.sub(r"\s+.*$", "", text).replace("/", "-").replace("年", "-").replace("月", "-").replace("日", "") for fmt in ("%Y-%m-%d", "%Y.%m.%d"): try: return datetime.strptime(text, fmt).date() except ValueError: continue return None def parse_datetime(value: Any) -> datetime | None: if isinstance(value, datetime): return value text = clean_text(value) if not text: return None for fmt in ("%Y/%m/%d %H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y/%m/%d", "%Y-%m-%d"): try: return datetime.strptime(text, fmt) except ValueError: continue return None def fingerprint(file_name: str, row_number: int) -> str: return hashlib.sha256(f"{file_name}\x1fSheet1\x1f{row_number}".encode()).hexdigest() def read_purchase_order_rows(path: Path) -> list[dict[str, Any]]: workbook = load_workbook(path, read_only=True, data_only=True) sheet = workbook.active headers = [clean_text(value) for value in next(sheet.iter_rows(values_only=True))] required = ["单据编号", "采购日期", "供应商"] missing = [name for name in required if name not in headers] if missing: raise ValueError(f"采购订单缺少必要字段:{', '.join(missing)}") indexes = {name: headers.index(name) for name in headers if name} def value(row: tuple[Any, ...], name: str) -> Any: index = indexes.get(name) return row[index] if index is not None and index < len(row) else None rows: list[dict[str, Any]] = [] for row_number, raw in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): document = clean_text(value(raw, "单据编号")) supplier = clean_text(value(raw, "供应商")) if not document or not supplier: continue rows.append( { "document_number": document, "purchase_date": parse_date(value(raw, "采购日期")), "supplier": supplier, "document_status": clean_text(value(raw, "单据状态")), "close_status": clean_text(value(raw, "关闭状态")), "material_code": clean_text(value(raw, "物料编码")), "material_name": clean_text(value(raw, "物料名称")), "purchase_unit": clean_text(value(raw, "采购单位")), "purchase_quantity": value(raw, "采购数量"), "delivery_date": parse_datetime(value(raw, "交货日期")), "tax_inclusive_unit_price": value(raw, "含税单价"), "is_gift": clean_text(value(raw, "是否赠品")), "business_close": clean_text(value(raw, "业务关闭")), "remark": clean_text(value(raw, "备注")), "source_file": path.name, "import_fingerprint": fingerprint(path.name, row_number), } ) return rows async def import_purchase_orders(session: AsyncSession, path: Path) -> dict[str, int]: rows = read_purchase_order_rows(path) inserted = 0 for start in range(0, len(rows), 500): batch = rows[start : start + 500] result = await session.execute( insert(PurchaseOrderLine).values(batch).on_conflict_do_nothing(index_elements=["import_fingerprint"]) ) inserted += result.rowcount or 0 await session.flush() return {"source_rows": len(rows), "inserted_rows": inserted, "skipped_rows": len(rows) - inserted} async def supplier_order_counts( session: AsyncSession, supplier: str, start: date, end: date, ) -> int: value = await session.scalar( select(func.count(func.distinct(PurchaseOrderLine.document_number))).where( PurchaseOrderLine.supplier.ilike(f"%{supplier}%"), PurchaseOrderLine.purchase_date >= start, PurchaseOrderLine.purchase_date <= end, ) ) return int(value or 0)