|
|
@@ -0,0 +1,165 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+"""可选:从产品名称括号段拆出品牌。
|
|
|
+
|
|
|
+示例:「片碱(君正)」→ product_name=片碱,crm_product_brand.brand_name=君正。
|
|
|
+
|
|
|
+默认 dry-run:导出匹配清单 CSV,不改库。
|
|
|
+业务确认后加 --apply 才执行更新。
|
|
|
+
|
|
|
+不修改历史报价/合同/计划行上的 product_name 快照。
|
|
|
+
|
|
|
+用法:
|
|
|
+ cd backend && ./venv/bin/python scripts/migrate_20260727_split_brand_from_product_name.py
|
|
|
+ cd backend && ./venv/bin/python scripts/migrate_20260727_split_brand_from_product_name.py --csv /tmp/brands.csv
|
|
|
+ cd backend && ./venv/bin/python scripts/migrate_20260727_split_brand_from_product_name.py --apply
|
|
|
+"""
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import argparse
|
|
|
+import csv
|
|
|
+import re
|
|
|
+import sys
|
|
|
+from pathlib import Path
|
|
|
+
|
|
|
+# 允许直接 python scripts/... 运行
|
|
|
+ROOT = Path(__file__).resolve().parents[1]
|
|
|
+if str(ROOT) not in sys.path:
|
|
|
+ sys.path.insert(0, str(ROOT))
|
|
|
+
|
|
|
+from app.core.database import SessionLocal
|
|
|
+from app.core.utils import generate_uuid
|
|
|
+from app.models.crm_models import CrmProduct, CrmProductBrand
|
|
|
+
|
|
|
+# 全角/半角括号均可:片碱(君正) / 片碱(君正)
|
|
|
+NAME_WITH_BRAND_RE = re.compile(
|
|
|
+ r"^(?P<base>.+?)[((](?P<brand>[^))]+)[))]\s*$"
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def parse_name(name: str):
|
|
|
+ text = (name or "").strip()
|
|
|
+ m = NAME_WITH_BRAND_RE.match(text)
|
|
|
+ if not m:
|
|
|
+ return None
|
|
|
+ base = m.group("base").strip()
|
|
|
+ brand = m.group("brand").strip()
|
|
|
+ if not base or not brand:
|
|
|
+ return None
|
|
|
+ return base, brand
|
|
|
+
|
|
|
+
|
|
|
+def collect_candidates(db):
|
|
|
+ rows = (
|
|
|
+ db.query(CrmProduct)
|
|
|
+ .filter(CrmProduct.del_flag == 0)
|
|
|
+ .order_by(CrmProduct.create_time.desc())
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ out = []
|
|
|
+ for row in rows:
|
|
|
+ parsed = parse_name(row.product_name or "")
|
|
|
+ if not parsed:
|
|
|
+ continue
|
|
|
+ base, brand = parsed
|
|
|
+ out.append(
|
|
|
+ {
|
|
|
+ "id": row.id,
|
|
|
+ "tenant_no": row.tenant_no,
|
|
|
+ "product_no": row.product_no or "",
|
|
|
+ "old_name": row.product_name,
|
|
|
+ "new_name": base,
|
|
|
+ "brand_name": brand,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return out
|
|
|
+
|
|
|
+
|
|
|
+def write_csv(path: Path, rows: list[dict]) -> None:
|
|
|
+ path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
+ with path.open("w", encoding="utf-8-sig", newline="") as f:
|
|
|
+ writer = csv.DictWriter(
|
|
|
+ f,
|
|
|
+ fieldnames=["id", "tenant_no", "product_no", "old_name", "new_name", "brand_name"],
|
|
|
+ )
|
|
|
+ writer.writeheader()
|
|
|
+ writer.writerows(rows)
|
|
|
+
|
|
|
+
|
|
|
+def apply_updates(db, rows: list[dict]) -> int:
|
|
|
+ changed = 0
|
|
|
+ for item in rows:
|
|
|
+ product = (
|
|
|
+ db.query(CrmProduct)
|
|
|
+ .filter(
|
|
|
+ CrmProduct.id == item["id"],
|
|
|
+ CrmProduct.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+ if not product:
|
|
|
+ continue
|
|
|
+ if (product.product_name or "").strip() != item["old_name"]:
|
|
|
+ continue
|
|
|
+ exists = (
|
|
|
+ db.query(CrmProductBrand)
|
|
|
+ .filter(
|
|
|
+ CrmProductBrand.product_id == product.id,
|
|
|
+ CrmProductBrand.tenant_no == product.tenant_no,
|
|
|
+ CrmProductBrand.brand_name == item["brand_name"],
|
|
|
+ CrmProductBrand.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+ if not exists:
|
|
|
+ db.add(
|
|
|
+ CrmProductBrand(
|
|
|
+ id=generate_uuid(),
|
|
|
+ product_id=product.id,
|
|
|
+ brand_name=item["brand_name"],
|
|
|
+ status="on",
|
|
|
+ tenant_no=product.tenant_no,
|
|
|
+ order_num=0,
|
|
|
+ create_by=product.update_by or product.create_by,
|
|
|
+ create_dept_id=product.create_dept_id or "",
|
|
|
+ update_by=product.update_by or product.create_by,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ product.product_name = item["new_name"]
|
|
|
+ changed += 1
|
|
|
+ db.commit()
|
|
|
+ return changed
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ parser = argparse.ArgumentParser(description="从产品名括号段拆品牌(需业务确认后 --apply)")
|
|
|
+ parser.add_argument(
|
|
|
+ "--csv",
|
|
|
+ default="scripts/output/product_brand_split_candidates.csv",
|
|
|
+ help="导出候选清单路径",
|
|
|
+ )
|
|
|
+ parser.add_argument(
|
|
|
+ "--apply",
|
|
|
+ action="store_true",
|
|
|
+ help="确认后执行:改 product_name 并插入 crm_product_brand",
|
|
|
+ )
|
|
|
+ args = parser.parse_args()
|
|
|
+
|
|
|
+ db = SessionLocal()
|
|
|
+ try:
|
|
|
+ rows = collect_candidates(db)
|
|
|
+ csv_path = Path(args.csv)
|
|
|
+ if not csv_path.is_absolute():
|
|
|
+ csv_path = ROOT / csv_path
|
|
|
+ write_csv(csv_path, rows)
|
|
|
+ print(f"匹配 {len(rows)} 条,已导出: {csv_path}")
|
|
|
+ if not args.apply:
|
|
|
+ print("dry-run 完成。业务确认后加 --apply 再执行。")
|
|
|
+ return
|
|
|
+ changed = apply_updates(db, rows)
|
|
|
+ print(f"已更新 {changed} 条产品名称,并补品牌子表(已存在同名品牌则跳过插入)。")
|
|
|
+ finally:
|
|
|
+ db.close()
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|