kx_guo 680b0c20cf 接入采购订单台账并统一PDF与Word报告导出
按供应商近三年去重采购单据统计真实合作频次,替换异常记录代理值;新增采购订单导入、合作频次计算、风险评分口径同步,以及PDF/Word报告生成接口。
2026-09-08 11:59:10 +08:00

131 lines
4.7 KiB
Python

"""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)