# ============================================================ # 结构化数据导入(moduleId: domain-intake, 可重生 ✅) # Pi 负责把自然语言规范化为 kind/rows;本模块只做字段校验与确定性归一化。 # ============================================================ from __future__ import annotations import datetime as dt from typing import Any World = dict[str, Any] IMPORT_KINDS = frozenset({"orders", "materials"}) MATERIAL_TYPES = frozenset({"FINISHED_PRODUCT", "SEMI_FINISHED", "RAW_MATERIAL"}) CUSTOMER_LEVELS = frozenset({"VIP", "A", "B", "C"}) def _import_error(kind: str | None, message: str) -> dict[str, Any]: return {"kind": kind, "rows": [], "errors": [message]} def _as_positive_int(value: Any) -> int | None: if isinstance(value, bool): return None try: number = int(value) except (TypeError, ValueError): return None return number if number > 0 else None def _exact_product(world: World, product_code: str) -> dict[str, Any] | None: lookup = product_code.casefold() for material in world.get("materials") or []: if material.get("type") != "FINISHED_PRODUCT": continue if str(material.get("code") or "").casefold() == lookup: return material return None def _normalize_order_row(world: World, row: Any, index: int) -> tuple[dict[str, Any] | None, str | None]: if not isinstance(row, dict): return None, f"第{index}行必须是 JSON 对象" customer = str(row.get("customerName") or "").strip() product_code = str(row.get("productCode") or "").strip() quantity = _as_positive_int(row.get("quantity")) delivery_date = str(row.get("deliveryDate") or "").strip() if not customer: return None, f"第{index}行缺少 customerName" if not product_code: return None, f"第{index}行缺少 productCode" if quantity is None: return None, f"第{index}行 quantity 必须是正整数" try: delivery_date = dt.date.fromisoformat(delivery_date).isoformat() except ValueError: return None, f"第{index}行 deliveryDate 必须是 YYYY-MM-DD" product = _exact_product(world, product_code) if product is None: return None, f"第{index}行 productCode「{product_code}」未找到精确匹配的成品" level = str(row.get("customerLevel") or "A").strip().upper() if level not in CUSTOMER_LEVELS: return None, f"第{index}行 customerLevel 必须是 VIP/A/B/C" is_rush = bool(row.get("isRush", False)) priority = row.get("priority") if priority is None: priority = 1 if is_rush else 5 if isinstance(priority, bool) or not isinstance(priority, int): try: priority = int(priority) except (TypeError, ValueError): return None, f"第{index}行 priority 必须是整数" return { "customerName": customer, "customerLevel": level, "productId": int(product["id"]), "productCode": product["code"], "productName": product["name"], "quantity": quantity, "deliveryDate": delivery_date, "priority": max(1, min(9, priority)), "isRush": is_rush, "status": "CONFIRMED", }, None def _normalize_material_row(row: Any, index: int) -> tuple[dict[str, Any] | None, str | None]: if not isinstance(row, dict): return None, f"第{index}行必须是 JSON 对象" code = str(row.get("code") or "").strip() name = str(row.get("name") or "").strip() material_type = str(row.get("type") or "").strip().upper() if not code: return None, f"第{index}行缺少 code" if not name: return None, f"第{index}行缺少 name" if material_type not in MATERIAL_TYPES: return None, f"第{index}行 type 必须是 FINISHED_PRODUCT/SEMI_FINISHED/RAW_MATERIAL" numeric: dict[str, float] = {} for field in ("stock", "inTransit", "safetyStock", "procurementLeadTime"): value = row.get(field, 0) if isinstance(value, bool): return None, f"第{index}行 {field} 必须是数字" try: numeric[field] = float(value) except (TypeError, ValueError): return None, f"第{index}行 {field} 必须是数字" return { "code": code, "name": name, "type": material_type, "unit": str(row.get("unit") or "件").strip() or "件", "spec": str(row.get("spec") or "").strip(), **numeric, }, None def parse_import(kind: str | None, world: World | None = None, *, rows: list[Any] | None = None) -> dict[str, Any]: """校验 Pi 已规范化的导入参数,不解析任何原始自然语言文本。""" normalized_kind = str(kind or "").strip().lower() if normalized_kind not in IMPORT_KINDS: return _import_error(None, "请明确导入类型:kind 必须为 orders 或 materials") if not isinstance(rows, list) or not rows: return _import_error(normalized_kind, "请提供结构化导入行:rows 必须是非空数组") normalized: list[dict[str, Any]] = [] errors: list[str] = [] for index, row in enumerate(rows, 1): if normalized_kind == "orders": if world is None: return _import_error(normalized_kind, "订单导入需要 world 用于精确校验 productCode") item, error = _normalize_order_row(world, row, index) else: item, error = _normalize_material_row(row, index) if error: errors.append(error) elif item is not None: normalized.append(item) return {"kind": normalized_kind, "rows": normalized, "errors": errors} def confirmation_for_import(parsed: dict[str, Any]) -> tuple[str, list[str]]: """批量导入确认卡。""" kind = parsed["kind"] rows = parsed["rows"] label = "订单" if kind == "orders" else "物料" title = f"批量导入{label} {len(rows)} 条" lines = [f"将新建 {len(rows)} 条{label}写入主干(P2)"] for r in rows[:8]: if kind == "orders": lines.append( f"· {r['customerName']} · {r.get('productName', '')} × {r['quantity']} · 交期 {r['deliveryDate']}" ) else: lines.append(f"· {r['code']} {r['name']} · {r['type']} · 库存 {r.get('stock', 0)}") if len(rows) > 8: lines.append(f"…另有 {len(rows) - 8} 条") if parsed.get("errors"): lines.append(f"跳过无法解析 {len(parsed['errors'])} 行") lines.append("批准后执行前自动建档,可回滚") return title, lines def apply_import(world: World, next_id, parsed: dict[str, Any]) -> dict[str, Any]: """执行批量导入。""" from server.aps_domain.masterdata import apply_master_action from server.aps_domain.orders import apply_order_action created: list[str] = [] kind = parsed["kind"] for row in parsed["rows"]: if kind == "orders": applied = apply_order_action(world, next_id, "order.upsert", row) created.append(applied["order"]["orderNo"]) else: applied = apply_master_action(world, next_id, "master.material.upsert", row) created.append(row["code"]) return {"kind": kind, "count": len(created), "created": created}