aps-agent/server/aps_domain/importers.py

622 lines
29 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.

# ============================================================
# Excel/CSV 导入管线(moduleId: domain-importers, 可重生 ✅)
# 对齐 docs/product/demand-data-intake.md · features MD-04
# preview(P1 校验)→ commit(P2 确认卡落库)
# ============================================================
from __future__ import annotations
import csv
import io
import re
from typing import Any
World = dict[str, Any]
_HEADER_ALIASES: dict[str, str] = {
"编码": "code", "code": "code", "设备编码": "code", "模具编码": "code",
"设备编号": "code", "工装模具编号": "code", "模具编号": "code",
"物料编码": "code", "物料代码": "code", "物料编号": "code",
"产品编码": "productCode", "productcode": "productCode",
"成品编码": "productCode", "料号": "productCode", "产品料号": "productCode",
"名称": "name", "name": "name", "设备名称": "name",
"模具名称": "name", "工装模具名称": "name", "物料名称": "name",
"产品名称": "productName", "物料描述": "productName", "productname": "productName",
"类型": "type", "type": "type", "物料类型": "type",
"单位": "unit", "unit": "unit", "库存": "stock", "stock": "stock",
"在途": "inTransit", "intransit": "inTransit", "安全库存": "safetyStock",
"前置期": "procurementLeadTime", "采购前置期": "procurementLeadTime",
"客户": "customerName", "customername": "customerName", "客户名称": "customerName",
"项目名称": "customerName", "项目": "customerName",
"数量": "quantity", "quantity": "quantity",
"订单数量": "quantity", "计划订单数量": "quantity", "单位用量": "quantity", "用量": "quantity",
"交期": "deliveryDate", "duedate": "deliveryDate", "交货期": "deliveryDate",
"计划结束时间": "deliveryDate", "计划结束": "deliveryDate", "完成日期": "deliveryDate",
"订单号": "orderNo", "orderno": "orderNo", "订单代码": "orderNo",
"生产订单": "orderNo", "工单编号": "orderNo", "工单号": "orderNo",
"优先级": "priority", "priority": "priority",
"等级": "customerLevel", "客户等级": "customerLevel",
"工序编码": "operationCode", "operationcode": "operationCode",
"工序编号": "operationCode", "工序": "operationCode",
"工序名称": "name", "operationname": "name",
"序号": "seq", "seq": "seq", "顺序": "seq", "排序号": "seq",
"单件工时": "stdTimePerUnit", "stdtimeperunit": "stdTimePerUnit",
"工时": "stdTimePerUnit", "标准工时": "stdTimePerUnit", "标准工时/分钟": "stdTimePerUnit",
"需模具": "requireMold", "requiremold": "requireMold",
"是否瓶颈": "isBottleneck", "isbottleneck": "isBottleneck", "瓶颈": "isBottleneck",
"换型": "changeoverMin", "换型时间": "changeoverMin", "changeovermin": "changeoverMin",
"能力": "capabilities", "capabilities": "capabilities", "可执行工序": "capabilities",
"单件工时映射": "opStdTime", "opstdtime": "opStdTime",
"可移动": "movable", "movable": "movable", "移动耗时": "moveTimeMin",
"movetimemin": "moveTimeMin", "区域": "zone", "zone": "zone",
"适配模具": "adaptableMolds", "adaptablemolds": "adaptableMolds",
"适配设备": "adaptableEquipment", "adaptableequipment": "adaptableEquipment",
"可动率": "availabilityRate", "availabilityrate": "availabilityRate",
"状态": "status", "status": "status", "寿命上限": "lifeTotal", "lifetotal": "lifeTotal",
"已用寿命": "lifeUsed", "lifeused": "lifeUsed",
"子件编码": "materialCode", "materialcode": "materialCode",
"关键料": "isKey", "iskey": "isKey", "是否关键件": "isKey",
"消耗工序": "consumeOp", "consumeop": "consumeOp",
"mes工序编号": "consumeOp", "MES工序编号": "consumeOp",
}
# 各类型「排产关键字段」:源表头别名 → 标准字段(用于动态识别展示)
_KIND_KEY_FIELDS: dict[str, tuple[tuple[str, ...], ...]] = {
"orders": (
("productCode", "料号", "产品编码", "成品编码"),
("quantity", "订单数量", "计划订单数量", "数量"),
("deliveryDate", "计划结束时间", "完成日期", "交期"),
("customerName", "项目名称", "客户", "客户名称"),
("orderNo", "订单代码", "订单号", "生产订单"),
),
"materials": (
("code", "物料代码", "物料编码", "物料编号", "编码"),
("name", "物料名称", "名称"),
),
"routing": (
("operationCode", "工序编号", "工序编码", "工序"),
("seq", "排序号", "序号", "顺序"),
("stdTimePerUnit", "标准工时/分钟", "标准工时", "工时", "单件工时"),
("productCode", "产品编码", "料号", "成品编码"),
),
"equipment": (
("code", "设备编号", "设备编码", "编码"),
("name", "设备名称", "名称"),
("capabilities", "可执行工序", "能力"),
),
"molds": (
("code", "工装模具编号", "模具编号", "模具编码", "编码"),
("name", "工装模具名称", "模具名称", "名称"),
("operationCode", "工序编号", "适用工序", "工序"),
),
"bom": (
("materialCode", "物料编号", "物料编码", "子件编码"),
("quantity", "单位用量", "用量", "数量"),
("productCode", "成品编码", "产品编码", "料号"),
("orderNo", "工单编号", "订单代码"),
),
"operations": (
("code", "工序编号", "工序编码", "编码"),
("name", "工序名称", "名称"),
),
}
_KIND_HINTS: dict[str, tuple[str, ...]] = {
"orders": ("订单", "order", "销售订单"),
"materials": ("物料", "material", "库存"),
"bom": ("bom", "物料清单", "产品bom"),
"routing": ("工艺路线", "routing", "路线"),
"equipment": ("设备", "equipment", "机器"),
"molds": ("模具", "mold", "工装"),
"operations": ("工序", "operation", "工序库"),
"zones": ("区域", "zone", "布局"),
}
IMPORT_KINDS = tuple(_KIND_HINTS.keys())
def _norm_header(h: str) -> str:
raw = str(h or "").strip()
key = re.sub(r"\s+", "", raw.lower())
return _HEADER_ALIASES.get(raw) or _HEADER_ALIASES.get(key) or key
def _truthy(v: Any) -> bool:
if isinstance(v, bool):
return v
s = str(v or "").strip().lower()
return s in ("1", "true", "yes", "y", "是", "有")
def _split_multi(v: Any) -> list[str]:
if v is None or v == "":
return []
if isinstance(v, (list, tuple)):
return [str(x).strip() for x in v if str(x).strip()]
return [p.strip() for p in re.split(r"[,,;/|、]", str(v)) if p.strip()]
def _parse_op_std(v: Any) -> dict[str, float]:
if isinstance(v, dict):
return {str(k): float(val) for k, val in v.items()}
out: dict[str, float] = {}
for part in _split_multi(v):
if ":" in part:
k, t = part.split(":", 1)
try:
out[k.strip()] = float(t)
except ValueError:
continue
return out
def detect_kind(name: str, headers: list[str]) -> str | None:
blob = (name or "").lower()
for kind, hints in _KIND_HINTS.items():
if any(h.lower() in blob for h in hints):
return kind
hs = {h.lower() for h in headers}
hdrs = {str(h or "").strip() for h in headers}
mapped = {_norm_header(h) for h in headers if h}
if (
{"customername", "客户", "交期", "deliverydate", "订单代码", "料号", "订单数量"} & (hs | hdrs)
or ({"productcode", "quantity"} <= mapped and ("deliverydate" in mapped or "customername" in mapped))
or ("orderno" in mapped and "productcode" in mapped)
):
return "orders"
if "capabilities" in mapped or "可执行工序" in hdrs or "movable" in mapped or "设备编号" in hdrs:
return "equipment"
if "lifetotal" in mapped or "寿命上限" in hdrs or "工装模具编号" in hdrs:
return "molds"
if "isbottleneck" in mapped or "是否瓶颈" in hdrs:
return "operations"
if "operationcode" in mapped and ("seq" in mapped or "stdtimeperunit" in mapped):
return "routing"
if ("materialcode" in mapped or "物料编号" in hdrs) and ("quantity" in mapped or "单位用量" in hdrs):
return "bom"
if "stock" in mapped or "库存" in hdrs or "物料代码" in hdrs or "物料编码" in hdrs:
return "materials"
if "zone" in mapped and len(headers) <= 4:
return "zones"
return None
def detect_field_map(kind: str, headers_raw: list[str]) -> list[dict[str, str]]:
"""根据原始表头动态识别关键字段映射:源列 → 标准字段。"""
specs = _KIND_KEY_FIELDS.get(kind) or ()
found: list[dict[str, str]] = []
seen_targets: set[str] = set()
raw_list = [str(h or "").strip() for h in headers_raw if h]
raw_set = set(raw_list)
for group in specs:
target = group[0]
if target in seen_targets:
continue
hit = None
# 1) 优先精确命中中文/英文别名(跳过 target 自身,避免 序号 抢在 排序号 前)
for alias in group[1:]:
if alias in raw_set:
hit = alias
break
# 2) 再按规范化列名匹配
if not hit:
for h in raw_list:
if _norm_header(h) == target:
hit = h
break
if hit:
seen_targets.add(target)
found.append({"source": hit, "target": target})
return found
def _sheet_as_code(sheet: str | None) -> str:
s = str(sheet or "").strip()
if not s or s.lower().startswith("sheet") or s in ("订单导出", "物料", "Equipment"):
return ""
return s
def _rows_from_csv(raw: bytes) -> tuple[list[str], list[dict[str, Any]]]:
text = raw.decode("utf-8-sig", errors="replace")
reader = csv.DictReader(io.StringIO(text))
headers = [_norm_header(h) for h in (reader.fieldnames or [])]
rows = []
for r in reader:
item = {_norm_header(k): (v.strip() if isinstance(v, str) else v) for k, v in r.items() if k}
if any(str(v or "").strip() for v in item.values()):
rows.append(item)
return headers, rows
def _rows_from_xlsx(raw: bytes) -> list[dict[str, Any]]:
from openpyxl import load_workbook
wb = load_workbook(io.BytesIO(raw), read_only=True, data_only=True)
sheets = []
for ws in wb.worksheets:
data = list(ws.iter_rows(values_only=True))
if not data:
continue
headers_raw = [str(c).strip() if c is not None else "" for c in data[0]]
headers = [_norm_header(h) for h in headers_raw]
rows = []
for line in data[1:]:
if not line or all(c is None or str(c).strip() == "" for c in line):
continue
item = {}
for i, h in enumerate(headers):
if not h or i >= len(line):
continue
val = line[i]
if isinstance(val, str):
val = val.strip()
item[h] = val
if any(str(v or "").strip() for v in item.values()):
rows.append(item)
sheets.append({"sheet": ws.title, "headers": headers, "headersRaw": headers_raw, "rows": rows})
return sheets
def _due_str(v: Any) -> str:
if v is None:
return ""
if hasattr(v, "strftime"):
return v.strftime("%Y-%m-%d")
return str(v).strip()[:10]
def validate_batch(
kind: str,
rows: list[dict[str, Any]],
world: World,
*,
soft: bool = False,
sheet: str | None = None,
product_by_order: dict[str, str] | None = None,
) -> dict[str, Any]:
"""校验一批导入行。
soft=True:工程目录分析模式——按现场表头容错,不要求成品已入库;
缺可执行工序/适用工序时给默认值并记 warning,便于动态识别关键字段。
"""
ok: list[dict[str, Any]] = []
errors: list[str] = []
warnings: list[str] = []
sheet_code = _sheet_as_code(sheet)
pmap = product_by_order or {}
def _seq_val(v: Any) -> int:
if v is None or v == "":
return 0
try:
return int(float(v))
except (TypeError, ValueError):
s = str(v).strip()
digits = re.sub(r"\D", "", s)
return int(digits) if digits else 0
for i, raw in enumerate(rows, 1):
try:
if kind == "orders":
from server.aps_domain.orders import find_product_by_hint
cust = str(raw.get("customerName") or "").strip()
hint = str(raw.get("productCode") or raw.get("productName") or "").strip()
# 料号形如 200042588[282000009 乘致电机] → 取括号内编码优先,否则整段
m_bracket = re.search(r"\[([^\]]+)\]", hint)
if m_bracket:
inner = m_bracket.group(1).strip()
code_part = re.split(r"\s+", inner, maxsplit=1)[0]
if code_part:
hint = code_part
if not str(raw.get("productName") or "").strip() and " " in inner:
raw = {**raw, "productName": inner.split(" ", 1)[1]}
try:
qty = int(float(raw.get("quantity") or 0))
except (TypeError, ValueError):
qty = 0
due = _due_str(raw.get("deliveryDate"))
if soft:
if not hint or qty <= 0 or not due:
raise ValueError("需产品(料号)/数量/交期(计划结束)")
if not cust:
cust = "现场项目"
warnings.append(f"第{i}行:无客户列,暂用「现场项目」(可用项目名称)")
elif not cust or not hint or qty <= 0 or not due:
raise ValueError("需客户/产品/数量/交期")
prod = find_product_by_hint(world, hint)
if prod is None:
prod = next((m for m in world.get("flexMaterials", [])
if m.get("type") == "FINISHED_PRODUCT"
and (m["code"].lower() == hint.lower() or hint in m.get("name", ""))), None)
if prod is None and not soft:
raise ValueError(f"找不到成品「{hint}」")
row_out = {
"customerName": cust,
"customerLevel": str(raw.get("customerLevel") or "A").upper(),
"productId": (prod or {}).get("id"),
"productCode": (prod or {}).get("code") or hint,
"productName": (prod or {}).get("name") or str(raw.get("productName") or hint),
"quantity": qty, "deliveryDate": due,
"priority": int(float(raw.get("priority") or 5)),
"isRush": _truthy(raw.get("isRush")), "status": "CONFIRMED",
}
ono = str(raw.get("orderNo") or "").strip()
if ono:
row_out["orderNo"] = ono
ok.append(row_out)
elif kind == "materials":
code = str(raw.get("code") or raw.get("materialCode") or "").strip()
name = str(raw.get("name") or raw.get("productName") or "").strip()
if not code or not name:
raise ValueError("需编码与名称")
type_map = {"成品": "FINISHED_PRODUCT", "半成品": "SEMI_FINISHED",
"原料": "RAW_MATERIAL", "原材料": "RAW_MATERIAL"}
raw_type = str(raw.get("type") or "").strip()
mtype = type_map.get(raw_type, raw_type.upper() if raw_type else "RAW_MATERIAL")
if soft and mtype not in ("FINISHED_PRODUCT", "SEMI_FINISHED", "RAW_MATERIAL"):
mtype = "RAW_MATERIAL"
ok.append({
"code": code, "name": name, "type": mtype,
"unit": str(raw.get("unit") or "件").strip('"') or "件",
"stock": float(raw.get("stock") or 0),
"inTransit": float(raw.get("inTransit") or 0),
"safetyStock": float(raw.get("safetyStock") or 0),
"procurementLeadTime": int(float(raw.get("procurementLeadTime") or 0)),
"spec": str(raw.get("spec") or ""),
})
elif kind == "equipment":
code = str(raw.get("code") or "").strip()
name = str(raw.get("name") or code).strip()
caps = _split_multi(raw.get("capabilities"))
op_std = _parse_op_std(raw.get("opStdTime"))
if not op_std and caps:
op_std = {c: 1.0 for c in caps}
if not caps and op_std:
caps = list(op_std.keys())
if soft and code and not caps:
caps = ["GENERAL"]
op_std = {"GENERAL": 1.0}
warnings.append(f"第{i}行:设备无「可执行工序」,暂记 GENERAL(可按工艺推断)")
if not code or not caps:
raise ValueError("需设备编码与可执行工序")
ok.append({
"code": code, "name": name, "capabilities": caps, "opStdTime": op_std,
"movable": _truthy(raw.get("movable")),
"moveTimeMin": float(raw.get("moveTimeMin") or 0),
"zone": str(raw.get("zone") or "ZONE-A"),
"adaptableMolds": _split_multi(raw.get("adaptableMolds")),
"availabilityRate": float(raw.get("availabilityRate") or 0.95),
"status": str(raw.get("status") or "RUNNING").upper(),
})
elif kind == "molds":
code = str(raw.get("code") or "").strip()
op = str(raw.get("operationCode") or raw.get("type") or "").strip()
if soft and code and not op:
op = "GENERAL"
warnings.append(f"第{i}行:模具无适用工序,暂记 GENERAL")
if not code or not op:
raise ValueError("需模具编码与适用工序")
ok.append({
"code": code, "name": str(raw.get("name") or code),
"operationCode": op,
"adaptableEquipment": _split_multi(raw.get("adaptableEquipment")),
"lifeTotal": int(float(raw.get("lifeTotal") or 50000)),
"lifeUsed": int(float(raw.get("lifeUsed") or 0)),
"zone": str(raw.get("zone") or "ZONE-A"),
"changeoverMin": float(raw.get("changeoverMin") or 20),
"status": str(raw.get("status") or "AVAILABLE").upper(),
})
elif kind == "operations":
code = str(raw.get("code") or raw.get("operationCode") or "").strip()
if not code:
raise ValueError("需工序编码")
ok.append({
"code": code, "name": str(raw.get("name") or code),
"isBottleneck": _truthy(raw.get("isBottleneck")),
"changeoverMin": float(raw.get("changeoverMin") or 0),
})
elif kind == "zones":
code = str(raw.get("code") or "").strip()
if not code:
raise ValueError("需区域编码")
ok.append({"code": code, "name": str(raw.get("name") or code)})
elif kind == "routing":
pc = str(raw.get("productCode") or "").strip()
if not pc and soft:
# sheet 名常为订单号 → 映射到料号;否则直接用 sheet
pc = pmap.get(sheet_code) or sheet_code
seq = _seq_val(raw.get("seq"))
op = str(raw.get("operationCode") or "").strip()
if not op or seq <= 0:
raise ValueError("需产品编码/序号/工序")
if not pc:
raise ValueError("需产品编码/序号/工序")
std = raw.get("stdTimePerUnit")
try:
std_f = float(std) if std not in (None, "") else None
except (TypeError, ValueError):
std_f = None
ok.append({
"productCode": pc,
"productName": str(raw.get("productName") or raw.get("name") or pc),
"seq": seq, "operationCode": op,
"operationName": str(raw.get("name") or raw.get("operationName") or op),
"requireMold": _truthy(raw.get("requireMold")),
"stdTimePerUnit": std_f,
})
elif kind == "bom":
pc = str(raw.get("productCode") or "").strip()
if not pc and soft:
ono = str(raw.get("orderNo") or sheet_code).strip()
pc = pmap.get(ono) or ono
mc = str(raw.get("materialCode") or raw.get("code") or "").strip()
qty = float(raw.get("quantity") or 0)
if not pc or not mc or qty <= 0:
raise ValueError("需成品/子件/用量")
ok.append({
"productCode": pc, "materialCode": mc, "quantity": qty,
"consumeOp": str(raw.get("consumeOp") or ""),
"isKey": _truthy(raw.get("isKey")),
})
else:
raise ValueError(f"不支持的导入类型:{kind}")
except Exception as exc: # noqa: BLE001
errors.append(f"第{i}行:{exc}")
# soft 模式下同类 warning 去重截断
if warnings:
uniq: list[str] = []
seen_w: set[str] = set()
for w in warnings:
key = re.sub(r"第\d+行:", "第N行:", w)
if key in seen_w:
continue
seen_w.add(key)
uniq.append(w)
if len(uniq) >= 5:
break
warnings = uniq
return {"kind": kind, "okRows": ok, "errors": errors, "warnings": warnings,
"okCount": len(ok), "errorCount": len(errors)}
def preview_file(
filename: str,
raw: bytes,
world: World,
*,
soft: bool = False,
product_by_order: dict[str, str] | None = None,
kind_hint: str | None = None,
) -> dict[str, Any]:
name = filename or "upload"
lower = name.lower()
batches = []
if lower.endswith((".csv", ".txt")):
headers, rows = _rows_from_csv(raw)
kind = kind_hint or detect_kind(name, headers) or "materials"
fmap = detect_field_map(kind, headers)
batch = validate_batch(
kind, rows, world, soft=soft, sheet=name, product_by_order=product_by_order)
batches.append({"sheet": name, "kind": kind, "fieldMap": fmap,
"headersRaw": headers, **batch})
elif lower.endswith((".xlsx", ".xlsm")):
for sh in _rows_from_xlsx(raw):
kind = (kind_hint
or detect_kind(f"{name}:{sh['sheet']}", sh["headersRaw"])
or detect_kind(name, sh["headers"])
or detect_kind(name, sh["headersRaw"])
or "materials")
fmap = detect_field_map(kind, sh["headersRaw"])
batch = validate_batch(
kind, sh["rows"], world, soft=soft,
sheet=sh["sheet"], product_by_order=product_by_order)
batches.append({
"sheet": sh["sheet"], "kind": kind, "fieldMap": fmap,
"headersRaw": sh["headersRaw"], **batch,
})
else:
raise ValueError("仅支持 .xlsx / .csv")
total_ok = sum(b["okCount"] for b in batches)
total_err = sum(b["errorCount"] for b in batches)
return {
"filename": name, "batches": batches,
"totalOk": total_ok, "totalErrors": total_err, "canCommit": total_ok > 0,
}
def confirmation_for_import_commit(preview: dict[str, Any]) -> tuple[str, list[str]]:
title = f"导入入库 · {preview.get('filename', '')}"
lines = [f"有效 {preview.get('totalOk', 0)} 行 · 错误 {preview.get('totalErrors', 0)} 行(错误行不入库)"]
for b in preview.get("batches", []):
lines.append(f"· [{b.get('sheet')}] {b['kind']} 通过 {b['okCount']} / 失败 {b['errorCount']}")
lines.append("批准后写入主干(P2);执行前自动建档可回滚")
return title, lines
def apply_import_commit(world: World, next_id, batches: list[dict[str, Any]]) -> dict[str, Any]:
from server.aps_domain.masterdata import apply_master_action
from server.aps_domain.orders import apply_order_action
from server.state.seed import ensure_flex_seed
ensure_flex_seed(world)
summary: dict[str, int] = {}
def _next_table_id(table: str) -> int:
items = world.get(table, [])
return max((x.get("id", 0) for x in items if isinstance(x.get("id"), int)), default=0) + 1
for batch in batches:
kind = batch["kind"]
for row in batch.get("okRows") or []:
if kind == "orders":
if row.get("productId"):
apply_order_action(world, next_id, "order.upsert", row)
summary["salesOrders"] = summary.get("salesOrders", 0) + 1
else:
items = world.setdefault("flexOrders", [])
mid = _next_table_id("flexOrders")
items.append({
"id": mid, "orderNo": row.get("orderNo") or f"FO-{mid:04d}",
"productCode": row.get("productCode"), "quantity": row["quantity"],
"dueDate": row["deliveryDate"], "priority": row.get("priority", 5),
"status": "RELEASED",
})
summary["flexOrders"] = summary.get("flexOrders", 0) + 1
elif kind == "materials":
apply_master_action(world, next_id, "master.material.upsert", row)
fm = world.setdefault("flexMaterials", [])
ex = next((m for m in fm if m["code"] == row["code"]), None)
if ex:
ex.update({k: row[k] for k in ("name", "type", "unit", "stock") if k in row})
else:
fm.append({"id": _next_table_id("flexMaterials"),
**{k: row.get(k) for k in
("code", "name", "type", "unit", "stock", "inTransit",
"safetyStock", "procurementLeadTime")}})
summary["materials"] = summary.get("materials", 0) + 1
elif kind == "equipment":
items = world.setdefault("flexEquipment", [])
ex = next((e for e in items if e["code"] == row["code"]), None)
if ex:
ex.update(row)
else:
items.append({"id": _next_table_id("flexEquipment"), **row})
summary["flexEquipment"] = summary.get("flexEquipment", 0) + 1
elif kind == "molds":
items = world.setdefault("flexMolds", [])
ex = next((e for e in items if e["code"] == row["code"]), None)
if ex:
ex.update(row)
else:
items.append({"id": _next_table_id("flexMolds"), **row})
summary["flexMolds"] = summary.get("flexMolds", 0) + 1
elif kind == "operations":
items = world.setdefault("flexOperations", [])
ex = next((e for e in items if e["code"] == row["code"]), None)
if ex:
ex.update(row)
else:
items.append(row)
summary["flexOperations"] = summary.get("flexOperations", 0) + 1
elif kind == "zones":
items = world.setdefault("flexZones", [])
ex = next((e for e in items if e["code"] == row["code"]), None)
if ex:
ex.update(row)
else:
items.append(row)
summary["flexZones"] = summary.get("flexZones", 0) + 1
elif kind == "routing":
items = world.setdefault("flexRoutings", [])
items[:] = [s for s in items
if not (s.get("productCode") == row["productCode"] and s.get("seq") == row["seq"])]
items.append(dict(row))
summary["flexRoutings"] = summary.get("flexRoutings", 0) + 1
elif kind == "bom":
items = world.setdefault("flexBom", [])
items[:] = [s for s in items
if not (s.get("productCode") == row["productCode"]
and s.get("materialCode") == row["materialCode"])]
items.append(dict(row))
summary["flexBom"] = summary.get("flexBom", 0) + 1
return {"summary": summary, "total": sum(summary.values())}