云
昨天 74b0d9c5ab1dc316b29f8525dcffe45225d4c43c
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
74
75
76
import sys,json,csv,re
from pathlib import Path
from datetime import datetime
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
sys.path.insert(0,str(Path(__file__).parent/'iqc-import-20261012/deps'))
import pymysql
apply='--apply' in sys.argv
out=Path(__file__).parent/'schedule-diagnosis-20261015'
with pymysql.connect(host='36.213.150.237',port=10000,user='root',password=sys.stdin.readline().strip(),database='mom-hm',charset='utf8mb4',connect_timeout=15,cursorclass=pymysql.cursors.DictCursor) as c:
 with c.cursor() as q:
  data={}
  for t in ['mes_pro_route','mes_pro_route_process','mes_pro_route_product','mes_pro_route_product_bom','mes_md_product_bom','mes_md_item','mes_pro_work_order']:
   q.execute('SELECT * FROM '+t+' WHERE deleted=0 ORDER BY id'+(' FOR UPDATE' if apply else ''));data[t]=q.fetchall()
  items={i['id']:i for i in data['mes_md_item']}
  processes=defaultdict(list);products=defaultdict(set);bom=defaultdict(list);base=defaultdict(list)
  for p in data['mes_pro_route_process']:processes[p['route_id']].append(p)
  for p in data['mes_pro_route_product']:products[p['route_id']].add(p['item_id'])
  for b in data['mes_pro_route_product_bom']:bom[(b['route_id'],b['product_id'])].append(b)
  for b in data['mes_md_product_bom']:base[b['item_id']].append(b)
  # 502 is an existing gift-box assembly product: keep its real BOM quantities.
  additions=[]
  if 502 in products[57] and not bom[(57,502)]:
   for b in base[502]:
    process=10 if b['bom_item_id'] in (628,643) else 11
    assert process in {p['process_id'] for p in processes[57]}
    assert b['bom_item_id'] in items and b['quantity']>0
    additions.append({'route_id':57,'process_id':process,'product_id':502,'item_id':b['bom_item_id'],'quantity':b['quantity'],'remark':f"沿用产品BOM行{b['id']}原物料及用量;成品小袋用于内包装组装,盒箱及标签用于外包装"})
   bom[(57,502)].extend(additions)
  output_changes=[];enable=[];blocked=[];missing=[]
  for r in data['mes_pro_route']:
   ps=processes[r['id']]
   absent=[pid for pid in products[r['id']] if not bom[(r['id'],pid)]]
   for pid in absent:missing.append({'routeId':r['id'],'routeName':r['name'],'productId':pid,'productName':items[pid]['name'],'specification':items[pid]['specification']})
   valid=bool(ps) and any(p['key_flag']==b'\x01' for p in ps) and not absent
   if r['status']!=0:
    if valid:enable.append(r['id'])
    else:blocked.append({'routeId':r['id'],'routeName':r['name'],'missingProductIds':absent})
   # A single-product route can use an explicit product without changing behavior.
   # Shared routes must retain the native work-order-product fallback.
   linked=products[r['id']]
   historical={o['product_id'] for o in data['mes_pro_work_order'] if o['route_id']==r['id']}
   if len(linked)==1 and historical<=linked:
    pid=next(iter(linked))
    if items[pid]['status']==0:
     for p in ps:
      if not p['output_item_id']:output_changes.append({'id':p['id'],'output':str(pid)})
  # Concrete reference candidates, kept outside live production formulas.
  candidates={987:524,988:525,989:526,1013:524,776:524,979:484,1151:962,1152:541,1153:540,974:523,992:523,1122:523,985:534,986:533,984:529,1157:507,1540:529,1155:535,973:528,976:536,983:531,1508:536,1509:536,975:484,1544:484,1154:537,1158:520,1543:520,860:540,639:500,663:509,815:533,643:507,807:529,628:497,649:504,811:531,835:497,963:537,613:494,641:501,669:511,1510:514,2004:484,2005:492,2006:501}
  refs=[]
  for row in missing:
   source=candidates.get(row['productId'])
   if source in items:
    for b in base[source]:
     refs.append({**row,'sourceProductId':source,'sourceProductName':items[source]['name'],'sourceSpecification':items[source]['specification'],'materialId':b['bom_item_id'],'materialName':items[b['bom_item_id']]['name'],'sourceQuantity':str(b['quantity']),'review':'参考同类产品;原料与包材需按目标净含量、包装数量及单位换算,原浆浓度和配方需工艺复核;尚未写入正式BOM'})
  with (out/'投入配置参考清单.csv').open('w',encoding='utf-8-sig',newline='') as f:
   writer=csv.DictWriter(f,fieldnames=list(refs[0]) if refs else ['productId']);writer.writeheader();writer.writerows(refs)
  report={'inputAdded':len(additions),'explicitOutputsAdded':len(output_changes),'routesToEnable':enable,'blockedRoutes':blocked,'missingProductCount':len(missing),'referenceRows':len(refs),'nativeOutputFallback':'多产品共用路线按工单产品产出,避免固定SKU串料'}
  (out/'route-config-plan.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
  print(json.dumps(report,ensure_ascii=False))
  if apply:
   stamp=datetime.now().strftime('%Y%m%d%H%M%S')
   (out/f'route-config-before-{stamp}.json').write_text(json.dumps(data,ensure_ascii=False,default=str,indent=2),encoding='utf-8')
   for b in additions:
    cols=list(b)
    q.execute('INSERT INTO mes_pro_route_product_bom ('+','.join(cols)+',create_time,update_time,deleted) VALUES ('+','.join(['%s']*len(cols))+',NOW(),NOW(),0)',list(b.values()))
   for row in output_changes:q.execute('UPDATE mes_pro_route_process SET output_item_id=%s,update_time=NOW() WHERE id=%s AND deleted=0',(row['output'],row['id']))
   for rid in enable:q.execute('UPDATE mes_pro_route SET status=0,update_time=NOW() WHERE id=%s AND deleted=0',(rid,))
   for rid in enable:
    q.execute('SELECT status FROM mes_pro_route WHERE id=%s',(rid,));assert q.fetchone()['status']==0
   for row in output_changes:
    q.execute('SELECT output_item_id FROM mes_pro_route_process WHERE id=%s',(row['id'],));assert q.fetchone()['output_item_id']==row['output']
   q.execute('SELECT status,COUNT(*) n FROM mes_pro_route WHERE deleted=0 GROUP BY status');report['routeStatusesAfter']=q.fetchall()
   c.commit()
   (out/f'route-config-result-{stamp}.json').write_text(json.dumps(report,ensure_ascii=False,default=str,indent=2),encoding='utf-8')
   print(json.dumps({'committed':True,'routeStatusesAfter':report['routeStatusesAfter']},ensure_ascii=False))