Files
zkxw/backend/scripts/generate_production_materials_seed_sql.py
T
2026-06-18 14:57:00 +08:00

332 lines
12 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.
#!/usr/bin/env python3
"""从 生产物料.txt 生成期初导入 SQL(biz_production_material*)。"""
from __future__ import annotations
import re
from collections import OrderedDict
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
TXT_PATH = ROOT / "生产物料.txt"
OUT_PATH = ROOT / "docs/sql/seed_production_materials_init.sql"
TENANT = "zkxw"
USER_ID = "d3000000000000000000000000000001"
TS = "2026-06-18 10:00:00"
STOCK_IN_DATE = "2026-06-18"
EXPIRE_DATE = "2029-06-18"
REMARK = "期初库存导入(生产物料.txt)"
MONTHS = ["2", "3", "4", "5", "6", "7", "8", "9"]
REAGENT_KEYWORDS = (
"培养基",
"血清",
"胰酶",
"冻存",
"PBS",
"DPBS",
"DMSO",
"双抗",
"链霉素",
"试剂",
"添加剂",
"激活剂",
"活化剂",
"因子",
"消毒液",
"酒精",
"新洁尔灭",
"除孢子",
"鲎试剂",
"台盼蓝",
"Ficoll",
"DEPC",
"血替",
"白蛋白",
"HES",
"氯化钠",
"沉降液",
"MSC",
"NK",
"Cryosure",
"保存液",
"羟乙基",
"青霉素",
"青/链霉素",
"Rh血型",
"琼脂",
"血平皿",
)
def parse_num(raw: str | None) -> Decimal | None:
if raw is None:
return None
s = str(raw).strip()
if not s:
return None
try:
return Decimal(s)
except Exception:
return None
def q4(val: Decimal) -> str:
return format(val.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP), "f")
def sql_str(val: str | None) -> str:
if val is None:
return "NULL"
s = str(val).replace("\\", "\\\\").replace("'", "''")
return f"'{s}'"
def norm_spec(spec: str) -> str | None:
s = (spec or "").strip()
if not s or s == "0":
return None
return s
def norm_batch(batch: str, row_no: int) -> str:
s = (batch or "").strip()
if not s or s in {"0", "/"}:
return f"INIT-ROW{row_no:04d}"
return s
def classify_type(name: str) -> str:
for kw in REAGENT_KEYWORDS:
if kw in name:
return "reagent"
return "consumable"
def calc_balance_and_in_qty(cols: dict[str, str]) -> tuple[Decimal, Decimal]:
opening = parse_num(cols.get("期初")) or Decimal("0")
balance: Decimal | None = None
total_in = opening
for m in MONTHS:
v_in = parse_num(cols.get(f"{m}月入库")) or Decimal("0")
v_out = parse_num(cols.get(f"{m}月出库")) or Decimal("0")
v_bal = parse_num(cols.get(f"{m}月结存"))
if v_in == 0 and v_out == 0 and v_bal is None:
continue
if balance is None:
balance = opening
total_in += v_in
if v_bal is not None:
balance = v_bal
else:
balance = balance + v_in - v_out
if balance is None:
balance = opening
if total_in <= 0 and balance <= 0:
in_qty = Decimal("0")
else:
in_qty = max(total_in, balance)
return balance, in_qty
def parse_rows() -> list[dict]:
text = TXT_PATH.read_text(encoding="utf-8")
lines = [ln.rstrip("\n") for ln in text.splitlines() if ln.strip()]
headers = lines[0].split("\t")
rows: list[dict] = []
for idx, line in enumerate(lines[1:], start=2):
parts = line.split("\t")
if len(parts) < len(headers):
parts.extend([""] * (len(headers) - len(parts)))
cols = dict(zip(headers, parts))
name = (cols.get("名称") or "").strip()
if not name:
continue
remaining, in_qty = calc_balance_and_in_qty(cols)
rows.append(
{
"row_no": idx,
"name": name,
"spec": norm_spec(cols.get("规格型号", "")),
"unit": (cols.get("单位") or "").strip() or "个",
"batch_no": norm_batch(cols.get("批号", ""), idx),
"remaining": remaining,
"in_qty": in_qty,
"material_type": classify_type(name),
}
)
return rows
def build_materials(rows: list[dict]) -> OrderedDict[tuple, dict]:
materials: OrderedDict[tuple, dict] = OrderedDict()
for row in rows:
key = (row["name"], row["spec"], row["unit"])
if key not in materials:
seq = len(materials) + 1
code = f"ZKXW-PROD-{seq:05d}"
mat_id = f"pm{seq:032d}"
materials[key] = {
"key": key,
"seq": seq,
"code": code,
"mat_id": mat_id,
"name": row["name"],
"spec": row["spec"],
"unit": row["unit"],
"material_type": row["material_type"],
"stock_qty": Decimal("0"),
"batches": [],
}
mat = materials[key]
mat["stock_qty"] += row["remaining"]
if row["material_type"] == "reagent":
mat["material_type"] = "reagent"
batch_seq = len(mat["batches"]) + 1
batch_id = f"pb{mat['seq']:03d}{batch_seq:029d}"
stock_in_no = f"INIT-20260618-{mat['seq']:05d}-{batch_seq:02d}"
mat["batches"].append(
{
**row,
"batch_id": batch_id,
"stock_in_no": stock_in_no,
"usage_id": f"pu{mat['seq']:03d}{batch_seq:029d}",
"material_code": mat["code"],
"material_type": mat["material_type"],
"mat_id": mat["mat_id"],
}
)
return materials
def gen_unit_inserts(units: set[str]) -> list[str]:
# 生产物料单位直接存 unit_name;此处仅确保 biz_item_unit 存在常用单位
unit_code_map = {
"个": "ge",
"件": "jian",
"套": "tao",
"台": "tai",
"支": "zhi",
"张": "zhang",
"片": "pian",
"把": "ba",
"双": "shuang",
"盒": "he",
"包": "bao",
"袋": "dai",
"瓶": "ping",
"桶": "tong",
"卷": "juan",
"箱": "xiang",
"L": "l",
}
lines: list[str] = []
uid = 0x60
for unit in sorted(units):
code = unit_code_map.get(unit)
if not code:
continue
uid += 1
unit_id = f"e400000000000000000000000000{uid:04x}"
lines.append(
f"INSERT INTO `biz_item_unit` (`id`, `unit_code`, `unit_name`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)\n"
f"SELECT {sql_str(unit_id)}, {sql_str(code)}, {sql_str(unit)}, {sql_str(TENANT)}, 500, '生产物料导入', {sql_str(USER_ID)}, '', {sql_str(TS)}, {sql_str(USER_ID)}, {sql_str(TS)}, 0 FROM DUAL\n"
f"WHERE NOT EXISTS (SELECT 1 FROM `biz_item_unit` WHERE `unit_code`={sql_str(code)} AND `tenant_no`={sql_str(TENANT)} AND `del_flag`=0);"
)
return lines
def gen_material_insert(mat: dict) -> str:
spec_sql = sql_str(mat["spec"]) if mat["spec"] else "NULL"
return (
f"INSERT INTO `biz_production_material` (`id`, `material_type`, `material_code`, `material_name`, `spec_model`, `unit`, `stock_qty`, `warning_qty`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)\n"
f"SELECT {sql_str(mat['mat_id'])}, {sql_str(mat['material_type'])}, {sql_str(mat['code'])}, {sql_str(mat['name'])}, {spec_sql}, {sql_str(mat['unit'])}, {q4(mat['stock_qty'])}, 0.0000, '1', {sql_str(TENANT)}, {mat['seq']}, {sql_str(REMARK)}, {sql_str(USER_ID)}, '', {sql_str(TS)}, {sql_str(USER_ID)}, {sql_str(TS)}, 0 FROM DUAL\n"
f"WHERE NOT EXISTS (SELECT 1 FROM `biz_production_material` WHERE `material_code`={sql_str(mat['code'])} AND `tenant_no`={sql_str(TENANT)} AND `del_flag`=0);"
)
def gen_batch_insert(batch: dict) -> str:
spec_sql = sql_str(batch["spec"]) if batch["spec"] else "NULL"
return (
f"INSERT INTO `biz_production_material_stock_batch` (`id`, `material_id`, `material_type`, `material_code`, `material_name`, `spec_model`, `unit`, `stock_in_no`, `stock_in_date`, `expire_date`, `catalog_no`, `batch_no`, `manufacturer`, `unit_price`, `amount`, `in_qty`, `remaining_qty`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)\n"
f"SELECT {sql_str(batch['batch_id'])}, (SELECT `id` FROM `biz_production_material` WHERE `material_code`={sql_str(batch['material_code'])} AND `tenant_no`={sql_str(TENANT)} AND `del_flag`=0 LIMIT 1), {sql_str(batch['material_type'])}, {sql_str(batch['material_code'])}, {sql_str(batch['name'])}, {spec_sql}, {sql_str(batch['unit'])}, {sql_str(batch['stock_in_no'])}, {sql_str(STOCK_IN_DATE)}, {sql_str(EXPIRE_DATE)}, {sql_str(batch['material_code'])}, {sql_str(batch['batch_no'])}, '期初导入', 0.00, 0.00, {q4(batch['in_qty'])}, {q4(batch['remaining'])}, {sql_str(TENANT)}, {batch['row_no']}, {sql_str(REMARK)}, {sql_str(USER_ID)}, '', {sql_str(TS)}, {sql_str(USER_ID)}, {sql_str(TS)}, 0 FROM DUAL\n"
f"WHERE NOT EXISTS (SELECT 1 FROM `biz_production_material_stock_batch` WHERE `stock_in_no`={sql_str(batch['stock_in_no'])} AND `tenant_no`={sql_str(TENANT)} AND `del_flag`=0);"
)
def gen_usage_insert(batch: dict) -> str:
if batch["in_qty"] <= 0:
return ""
return (
f"INSERT INTO `biz_production_material_usage` (`id`, `material_id`, `material_type`, `material_code`, `material_name`, `batch_id`, `stock_in_no`, `catalog_no`, `batch_no`, `manufacturer`, `unit_price`, `amount`, `change_type`, `change_qty`, `usage_unit`, `stock_before`, `stock_after`, `source_type`, `tenant_no`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)\n"
f"SELECT {sql_str(batch['usage_id'])}, (SELECT `id` FROM `biz_production_material` WHERE `material_code`={sql_str(batch['material_code'])} AND `tenant_no`={sql_str(TENANT)} AND `del_flag`=0 LIMIT 1), {sql_str(batch['material_type'])}, {sql_str(batch['material_code'])}, {sql_str(batch['name'])}, {sql_str(batch['batch_id'])}, {sql_str(batch['stock_in_no'])}, {sql_str(batch['material_code'])}, {sql_str(batch['batch_no'])}, '期初导入', 0.00, 0.00, 'in', {q4(batch['in_qty'])}, {sql_str(batch['unit'])}, 0.0000, {q4(batch['in_qty'])}, 'manual_in', {sql_str(TENANT)}, {sql_str(REMARK)}, {sql_str(USER_ID)}, '', {sql_str(TS)}, {sql_str(USER_ID)}, {sql_str(TS)}, 0 FROM DUAL\n"
f"WHERE NOT EXISTS (SELECT 1 FROM `biz_production_material_usage` WHERE `id`={sql_str(batch['usage_id'])});"
)
def main() -> None:
rows = parse_rows()
materials = build_materials(rows)
units = {m["unit"] for m in materials.values()}
parts = [
"-- 生产物料期初数据导入(来源:生产物料.txt)",
"-- 剩余数量:按台账逐月推算(期初 + 各月入库 - 出库,优先采用各月结存列)",
"-- 幂等:按 material_code / stock_in_no 跳过已存在记录",
f"-- 执行:mysql -u root -p zkxw < docs/sql/seed_production_materials_init.sql",
"",
"SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;",
"",
"-- ── 物品单位 biz_item_unit(确保本批物料所需单位存在)──",
"",
*gen_unit_inserts(units),
"",
"-- ── 物料主表 biz_production_material ──────────────────────",
"",
]
for mat in materials.values():
parts.append(gen_material_insert(mat))
parts.append("")
parts.append("-- ── 入库批次 biz_production_material_stock_batch ─────────")
parts.append("")
all_batches: list[dict] = []
for mat in materials.values():
all_batches.extend(mat["batches"])
for batch in all_batches:
parts.append(gen_batch_insert(batch))
parts.append("")
parts.append("-- ── 变动记录 biz_production_material_usage(期初入库)──────")
parts.append("")
for batch in all_batches:
usage_sql = gen_usage_insert(batch)
if usage_sql:
parts.append(usage_sql)
parts.append("")
OUT_PATH.write_text("\n".join(parts), encoding="utf-8")
print(f"Generated {OUT_PATH}")
print(f" txt rows: {len(rows)}")
print(f" materials: {len(materials)}")
print(f" batches: {len(all_batches)}")
nonzero = sum(1 for r in rows if r["remaining"] > 0)
print(f" batches with remaining > 0: {nonzero}")
if __name__ == "__main__":
main()