171 lines
5.9 KiB
Python
171 lines
5.9 KiB
Python
"""把「定制开发」从通用项目体系拆出,独立成定制交付项目。
|
||
|
||
数据来源:
|
||
1. 《项目明细表》台账(区域 / 金额 / 类别 / 状态说明)
|
||
2. 原「定制开发」项目下的子任务(子项目名,部分自带金额)与各周期执行记录
|
||
|
||
产出:custom_projects + custom_updates 两张独立表;
|
||
原 projects / tasks / updates 中的「定制开发」整体移除,常规看板只留平台产品类项目。
|
||
|
||
用法:python scripts/migrate_custom.py [--dry-run]
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||
|
||
import argparse # noqa: E402
|
||
|
||
from app.ai_parser import _guess_progress # noqa: E402
|
||
from app.constants import ( # noqa: E402
|
||
CUSTOM_STAGE_CODES,
|
||
guess_custom_stage,
|
||
infer_region,
|
||
normalize_custom_name,
|
||
)
|
||
from app.db import SessionLocal, engine # noqa: E402
|
||
from app.models import ( # noqa: E402
|
||
Base,
|
||
CustomProject,
|
||
CustomUpdate,
|
||
LedgerItem,
|
||
Period,
|
||
Project,
|
||
)
|
||
|
||
CONTAINER = "定制开发"
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--dry-run", action="store_true")
|
||
args = ap.parse_args()
|
||
|
||
Base.metadata.create_all(bind=engine)
|
||
db = SessionLocal()
|
||
|
||
# ---------------------------------------------------------------- 台账
|
||
by_name: dict[str, CustomProject] = {}
|
||
created_from_ledger = 0
|
||
for it in db.query(LedgerItem).order_by(LedgerItem.id).all():
|
||
clean, amt = normalize_custom_name(it.name)
|
||
if not clean:
|
||
continue
|
||
key = clean
|
||
cp = by_name.get(key)
|
||
if cp is None:
|
||
cp = CustomProject(
|
||
name=clean,
|
||
region=it.region,
|
||
amount=it.amount if it.amount else amt,
|
||
stage=guess_custom_stage(it.status_note, it.category),
|
||
progress=0,
|
||
remark=it.status_note,
|
||
period_id=it.period_id,
|
||
is_key=(it.category == "重点项目"),
|
||
source="import",
|
||
)
|
||
db.add(cp)
|
||
db.flush()
|
||
by_name[key] = cp
|
||
created_from_ledger += 1
|
||
else:
|
||
if not cp.amount and (it.amount or amt):
|
||
cp.amount = it.amount or amt
|
||
if not cp.region and it.region:
|
||
cp.region = it.region
|
||
|
||
# ---------------------------------------------------------------- 原「定制开发」子任务
|
||
container = db.query(Project).filter(Project.name == CONTAINER).one_or_none()
|
||
created_from_task = 0
|
||
moved_updates = 0
|
||
|
||
if container:
|
||
for t in sorted(container.tasks, key=lambda x: (x.sort_order, x.id)):
|
||
clean, amt = normalize_custom_name(t.name)
|
||
if not clean:
|
||
continue
|
||
cp = by_name.get(clean) or by_name.get(t.name)
|
||
if cp is None:
|
||
cp = CustomProject(
|
||
name=clean,
|
||
amount=amt,
|
||
stage="signed",
|
||
progress=0,
|
||
source="import",
|
||
)
|
||
db.add(cp)
|
||
db.flush()
|
||
by_name[clean] = cp
|
||
created_from_task += 1
|
||
elif not cp.amount and amt:
|
||
cp.amount = amt
|
||
|
||
# 该子项目的各周期执行记录 → custom_updates
|
||
for u in t.updates:
|
||
stage = guess_custom_stage(u.content)
|
||
cu = CustomUpdate(
|
||
custom_project_id=cp.id,
|
||
period_id=u.period_id,
|
||
stage=stage,
|
||
content=u.content,
|
||
summary=u.summary,
|
||
progress=u.progress,
|
||
risk=u.risk,
|
||
next_step=u.next_step,
|
||
owner=u.owner,
|
||
source="import",
|
||
)
|
||
db.add(cu)
|
||
moved_updates += 1
|
||
# 用最新记录回写项目状态
|
||
if cp.period_id is None or u.period_id >= cp.period_id:
|
||
cp.period_id = u.period_id
|
||
cp.stage = stage
|
||
cp.remark = (u.content or "")[:500]
|
||
if u.progress is not None:
|
||
cp.progress = u.progress
|
||
|
||
# ------------------------------------------------------------ 清理常规表
|
||
if not args.dry_run:
|
||
db.delete(container)
|
||
db.query(LedgerItem).delete()
|
||
|
||
# ---------------------------------------------------------------- 收尾
|
||
for cp in by_name.values():
|
||
if cp.stage not in CUSTOM_STAGE_CODES:
|
||
cp.stage = "signed"
|
||
if cp.stage in ("delivered", "accepted", "revenue", "closed") and not cp.progress:
|
||
cp.progress = 100
|
||
elif cp.stage == "developing" and not cp.progress:
|
||
cp.progress = 30
|
||
if not cp.region:
|
||
cp.region = infer_region(cp.name)
|
||
if cp.remark and not cp.progress:
|
||
p = _guess_progress(cp.remark)
|
||
if p is not None:
|
||
cp.progress = p
|
||
|
||
total = len(by_name)
|
||
if args.dry_run:
|
||
db.rollback()
|
||
print(f"[dry-run] 将生成定制项目 {total} 个(台账 {created_from_ledger} + 子任务 {created_from_task}),迁移执行记录 {moved_updates} 条")
|
||
return
|
||
|
||
db.commit()
|
||
|
||
amount_sum = sum(c.amount or 0 for c in by_name.values())
|
||
print(f"[ok] 定制项目 {total} 个(台账 {created_from_ledger} / 子任务 {created_from_task})")
|
||
print(f"[ok] 迁移执行记录 {moved_updates} 条 → custom_updates")
|
||
print(f"[ok] 合同金额合计 ¥{amount_sum:,.2f}")
|
||
from collections import Counter
|
||
print("[ok] 阶段分布:", dict(Counter(c.stage for c in by_name.values())))
|
||
print(f"[ok] 常规项目剩余 {db.query(Project).count()} 个(已移除「{CONTAINER}」)")
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|