云
11 小时以前 9dc4057ee86abf7a69e7d7a8720a41efa8dabdcc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import sys,json,csv,collections
from decimal import Decimal
from pathlib import Path
sys.path.insert(0,str(Path('scripts/iqc-import-20261012/deps').resolve()))
import pymysql
apply='--apply' in sys.argv
conn=pymysql.connect(host='36.213.150.237',port=10000,user='root',password=sys.stdin.readline().strip(),database='mom-hm',charset='utf8mb4',cursorclass=pymysql.cursors.DictCursor,connect_timeout=15,read_timeout=60,autocommit=False)
out=Path('scripts/mps-link-data-20261014');out.mkdir(exist_ok=True)
def save(name,data): (out/name).write_text(json.dumps(data,ensure_ascii=False,indent=2,default=str),encoding='utf8')
with conn.cursor() as q:
    q.execute('SELECT p.*,u.name AS plan_unit_name FROM mes_pro_mps p LEFT JOIN mes_md_unit_measure u ON u.id=p.unit_measure_id AND u.deleted=0 WHERE p.deleted=0 ORDER BY p.id')
    plans=q.fetchall()
    q.execute('SELECT i.*,o.no,o.customer_id,o.order_time,u.name AS sale_unit_name FROM erp_sale_order_items i JOIN erp_sale_order o ON o.id=i.order_id AND o.deleted=0 LEFT JOIN mdm_unit_measure u ON u.id=i.product_unit_id AND u.deleted=0 WHERE i.deleted=0')
    items=q.fetchall()
    q.execute('SELECT id,code,mdm_item_id,name FROM mes_md_item WHERE deleted=0')
    products={x['id']:x for x in q.fetchall()}
    q.execute('SELECT id,code FROM mdm_item WHERE deleted=0')
    mdm_by_code=collections.defaultdict(list)
    for x in q.fetchall():mdm_by_code[x['code']].append(x['id'])
save('strict-audit-before.json',{'plans':plans,'items':items,'products':products})
rows=[];matches=[]
for plan in plans:
    product=products.get(plan['product_id'],{})
    mdm_id=product.get('mdm_item_id')
    if not mdm_id and len(mdm_by_code[product.get('code')])==1:mdm_id=mdm_by_code[product['code']][0]
    choices=[x for x in items if mdm_id and x['product_id']==mdm_id]
    qty=[x for x in choices if Decimal(str(x['count']))==Decimal(str(plan['quantity']))]
    unit=[x for x in qty if plan['plan_unit_name'] and x['sale_unit_name'] and plan['plan_unit_name'].strip()==x['sale_unit_name'].strip()]
    month=[x for x in unit if plan['request_date'] and x['order_time'] and plan['request_date'].strftime('%Y-%m')==x['order_time'].strftime('%Y-%m')]
    available=[x for x in month if not x['mps_id'] or x['mps_id']==plan['id']]
    reason=('产品无对应销售明细' if not choices else '数量不一致' if not qty else '单位不一致或缺失' if not unit else '月份不一致或缺失' if not month else '销售明细已关联其他主计划' if not available else '多条符合条件的销售明细' if len(available)>1 else '唯一候选')
    if plan['sale_order_id'] or plan['sale_order_no'] or plan['sale_order_item_id']:reason='已有来源关联,保持原样'
    row={'主计划ID':plan['id'],'主计划编码':plan['code'],'产品编码':product.get('code',''),'产品名称':product.get('name',''),'计划数量':plan['quantity'],'计划单位':plan['plan_unit_name'],'需求日期':plan['request_date'],'同产品候选数':len(choices),'同数量候选数':len(qty),'同单位候选数':len(unit),'同月份候选数':len(month),'候选销售单号':';'.join(sorted(set(x['no'] for x in available))),'结果':reason}
    rows.append(row)
    if reason=='唯一候选':matches.append((plan,available[0],row))
reverse=collections.Counter(x[1]['id'] for x in matches)
for plan,item,row in matches:
    if reverse[item['id']]>1:row['结果']='同一销售明细匹配多个主计划,暂不关联'
matches=[x for x in matches if reverse[x[1]['id']]==1]
save('confirmed-candidates.json',[{'mps_id':p['id'],'mps_code':p['code'],'order_id':i['order_id'],'order_no':i['no'],'item_id':i['id']} for p,i,r in matches])
if apply and matches:
    try:
        with conn.cursor() as q:
            backups=[]
            for plan,item,row in matches:
                q.execute('SELECT * FROM mes_pro_mps WHERE id=%s AND deleted=0 FOR UPDATE',(plan['id'],));old_plan=q.fetchone()
                q.execute('SELECT * FROM erp_sale_order_items WHERE id=%s AND deleted=0 FOR UPDATE',(item['id'],));old_item=q.fetchone()
                assert old_plan and old_item and not any(old_plan[x] for x in ['sale_order_id','sale_order_no','sale_order_item_id'])
                assert not old_item['mps_id'] or old_item['mps_id']==plan['id']
                backups.append({'plan':old_plan,'item':old_item})
            save('updated-records-backup.json',backups)
            for plan,item,row in matches:
                q.execute('UPDATE mes_pro_mps SET sale_order_id=%s,sale_order_no=%s,sale_order_item_id=%s,update_time=update_time WHERE id=%s AND deleted=0',(item['order_id'],item['no'],item['id'],plan['id']));assert q.rowcount==1
                if not item['mps_id']:
                    q.execute('UPDATE erp_sale_order_items SET mps_id=%s,update_time=update_time WHERE id=%s AND deleted=0 AND (mps_id IS NULL OR mps_id=0)',(plan['id'],item['id']));assert q.rowcount==1
                row['结果']='已关联'
            for backup in backups:
                q.execute('SELECT * FROM mes_pro_mps WHERE id=%s',(backup['plan']['id'],));now_plan=q.fetchone()
                q.execute('SELECT * FROM erp_sale_order_items WHERE id=%s',(backup['item']['id'],));now_item=q.fetchone()
                changed=[k for k,v in backup['plan'].items() if k not in ['sale_order_id','sale_order_no','sale_order_item_id'] and now_plan[k]!=v]
                assert not changed, 'Unexpected plan fields changed: '+','.join(changed)
                assert all(now_item[k]==v for k,v in backup['item'].items() if k!='mps_id')
                assert now_item['mps_id']==now_plan['id'] and now_plan['sale_order_item_id']==now_item['id'] and now_plan['sale_order_id']==now_item['order_id']
        conn.commit()
    except Exception:
        conn.rollback();raise
else:conn.rollback()
with (out/'生产主计划销售订单严格匹配结果.csv').open('w',encoding='utf-8-sig',newline='') as f:
    w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
summary={'total_plans':len(plans),'unique_strict_matches':len(matches),'applied':len(matches) if apply else 0,'results':dict(collections.Counter(x['结果'] for x in rows))}
save('strict-match-summary.json',summary)
print(json.dumps(summary,ensure_ascii=False))
conn.close()