"""User-authorized reference BOM configuration, using existing material IDs only.""" import sys,json,re,csv from pathlib import Path from collections import defaultdict from datetime import datetime from decimal import Decimal,ROUND_HALF_UP 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' D=lambda v:Decimal(str(v)) tables=['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','mes_pro_process'] 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,read_timeout=60,cursorclass=pymysql.cursors.DictCursor) as c: with c.cursor() as q: q.execute('SELECT DATABASE() db');assert q.fetchone()['db']=='mom-hm' data={} for t in tables: 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']};process_names={p['id']:p['name'] for p in data['mes_pro_process']} base=defaultdict(list);rp=defaultdict(list);products=defaultdict(set);allocated=defaultdict(list) for b in data['mes_md_product_bom']:base[b['item_id']].append(b) for p in data['mes_pro_route_process']:rp[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']:allocated[(b['route_id'],b['product_id'])].append(b) for ps in rp.values():ps.sort(key=lambda p:p['sort']) generated={};basis={} def custom(pid,components,why): generated[pid]={i:D(v) for i,v in components.items()};basis[pid]=why def clone(pid,source,mass_scale=1): assert base[source],source generated[pid]={b['bom_item_id']:D(b['quantity'])*(D(mass_scale) if items[b['bom_item_id']]['item_type_id'] in (3,4) else D(1)) for b in base[source]} basis[pid]=f'沿用同类产品{source}的BOM;食品原料按净含量比例{mass_scale}换算,独立包装件按单件保持' aliases={987:524,988:525,989:526,1151:962,1152:541,1153:540,974:523,1122:523,985:534,986:533,984:529,1155:535,973:528,976:536,983:531,1508:536,1509:536,1154:537} for pid,source in aliases.items():clone(pid,source) clone(979,484,D('0.9')) clone(975,484,D('0.6')) custom(1544,{575:'.1512',576:1,577:1,579:1,574:1},'150克黑果枸杞单瓶;沿用产品486原料比例0.252/0.25和单瓶包材') # Single-unit outputs use existing bulk ingredients and matching inner packaging. # Organic and ordinary fruit inputs are kept separate. for pid in (776,1013):custom(pid,{1064:'.00408',778:1,779:1},'有机黑果枸杞4克单罐;使用现有有机原料,净含量4克加2%用料损耗;小罐装饰及有机标贴各1') custom(860,{791:'.00408',778:1},'普通黑果枸杞4克单罐;沿用去柄半成品,净含量4克加2%用料损耗') custom(992,{773:'.0084',1199:'.00136'},'8克冻干枸杞;原料和复合膜沿用产品523的单盒BOM,按25袋均分') custom(2004,{575:'.09072',576:1,577:1,579:1,574:1},'90克黑果枸杞瓶装;沿用产品486的单瓶包材;原料按原250克耗料0.252千克等比换算') custom(2005,{604:'.050714',576:1,577:1,606:1,607:1},'50克雪菊瓶装;沿用产品492的原料比例0.142/0.14及单瓶内袋、瓶签、卡纸') # Liquid inputs are in kg. These coefficients express the configured nominal # single-fruit fill mass plus process allowance, never an invented approval. film_or_bottle={639:(650,'.051',),663:(650,'.051',),815:(650,'.0612',),643:(658,'.0306',),1157:(658,'.051',),807:(658,'.0612',),628:(1065,'.0306',),649:(1065,'.051',),811:(1065,'.0612',),835:(1065,'.0306',),641:(696,'.0306',)} for pid,(raw,mass) in film_or_bottle.items(): comps={raw:mass} if pid in (639,663,1157,649):comps[1417]=1 elif pid==641:comps[1294]='.0019' elif pid in (643,835,628):comps[656 if pid==643 else 1280]='.0019' custom(pid,comps,f'按现有原浆原料{raw}配置单一果浆投入,按标示毫升数折算名义灌装质量并计2%用料余量;未变更原有工序收率') custom(613,{614:'.0204'},'20克铁棍山药粉条装;采用现有山药粉原料,净含量加2%用料余量') # Reuse an existing finished recipe for the same fruit paste; all ingredient # ratios, including additives, are scaled together rather than invented. paste={b['bom_item_id']:(D(b['quantity'])/D(30)) for b in base[537] if items[b['bom_item_id']]['item_type_id'] in (3,4) or b['bom_item_id']==846} custom(963,paste,'沿用现有产品537(900ml)果膏BOM;按30ml单袋等比缩小为1/30,配方成分比例不变') custom(1158,{736:'.0331',737:'5.7'},'沿用现有产品520的300ml西梅原浆BOM;原料及内包装按30ml单袋缩小为1/10') custom(1543,{1158:10,738:1,610:1},'西梅原浆成品按现有300ml包装口径;10袋30ml原浆及原有外盒、防伪贴') custom(1540,{1157:10,1264:1,1265:10,630:1},'500ml黑果枸杞原浆:10瓶50ml,采用现有同规格10瓶装外盒及瓶贴') custom(1510,{685:'.0153',686:'.0153',687:'.00095',688:'.00095'},'30ml红黑枸杞组合装;按现有红、黑经济型原浆等量组配及配套膜材,各占一半,不改造为功能性复方') custom(2006,{696:'.51',1417:10,1293:10,626:'.028',627:'.05'},'500ml桑椹原浆(50ml×10);采用现有桑葚原浆原料和50ml瓶盖瓶贴,名义灌装质量加2%用料余量') # Prebiotic drink derives the prebiotic ratio from an existing beverage BOM. ratios=[] for parent,bs in base.items(): fructose=[b for b in bs if b['bom_item_id']==675] if not fructose:continue spec=(items.get(parent,{}).get('specification') or '') match=re.search(r'(\d+(?:\.\d+)?)\s*(?:ml|毫升)',spec,re.I) if match:ratios.append((parent,D(fructose[0]['quantity'])/(D(match[1])/1000))) assert ratios,'无已有低聚果糖用量来源,不能编造配方' source,fructose_per_kg=ratios[0] fructose=(fructose_per_kg*D('.035')).quantize(D('.000001'),rounding=ROUND_HALF_UP) assert D(0)0 b={'item_id':pid,'bom_item_id':item,'quantity':qty,'status':0,'remark':'本次配置:'+basis[pid]} newbase.append(b);base[pid].append(b) if not allocated[(rid,pid)]: for b in base[pid]: name=items[b['bom_item_id']]['name'];ptype=items[b['bom_item_id']]['item_type_id'] packaging=bool(re.search('盒|箱|垫板|标签|瓶贴|卡纸|封套|内托|保护|袋|瓶盖|膜|干燥剂|装饰|RFID|防伪|保障|瓶身|亚克力',name)) and ptype not in (3,4) if packaging:preferred=['外包装'] if re.search('盒|箱|垫板|标签|瓶贴|卡纸|封套|内托|保护|RFID|防伪|保障|装饰',name) else ['内包装','灌装','封口','外包装'] elif items[b['bom_item_id']]['item_type_id'] in (20,23) or b['bom_item_id'] in products[rid]:preferred=['内包装','外包装'] else:preferred=['调配','混合','投料','打浆','粉碎','人工选别','领料脱包','原料验收'] p=next((p for n in preferred for p in ps if n in process_names[p['process_id']]),ps[0]) newinputs.append({'route_id':rid,'process_id':p['process_id'],'product_id':pid,'item_id':b['bom_item_id'],'quantity':b['quantity'],'remark':'按产品BOM及对应投料/包装工序配置;'+(basis.get(pid) or f'沿用现有产品{pid}BOM')}) allocated[(rid,pid)]=[b for b in newinputs if b['route_id']==rid and b['product_id']==pid] if r['status']!=0:disabled.append(rid) # Empty is the native per-work-order output. Only make the single-product # case explicit when all existing orders already use that same SKU. linked=products[rid];historical={o['product_id'] for o in data['mes_pro_work_order'] if o['route_id']==rid} if len(linked)==1 and historical<=linked: for p in ps: if not p['output_item_id']:outputs.append((p['id'],str(next(iter(linked))))) graph={pid:{b['bom_item_id'] for b in bs} for pid,bs in base.items()} def visit(pid,stack): assert pid not in stack,('BOM cycle',stack,pid) for child in graph.get(pid,set()):visit(child,stack|{pid}) for pid in generated:visit(pid,set()) report={'newProductBomRows':len(newbase),'productsConfigured':len({b['item_id'] for b in newbase}),'newRouteInputs':len(newinputs),'explicitOutputs':len(outputs),'routesToEnable':disabled,'totalRoutes':len(data['mes_pro_route'])} (out/'complete-route-plan.json').write_text(json.dumps({'report':report,'productBom':newbase,'routeInputs':newinputs,'outputs':outputs},ensure_ascii=False,default=str,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'complete-route-before-{stamp}.json').write_text(json.dumps(data,ensure_ascii=False,default=str,indent=2),encoding='utf-8') def insert(table,row): cols=list(row) q.execute('INSERT INTO '+table+' ('+','.join(cols)+',create_time,update_time,deleted) VALUES ('+','.join(['%s']*len(cols))+',NOW(),NOW(),0)',list(row.values())) for b in newbase:insert('mes_md_product_bom',b) for b in newinputs:insert('mes_pro_route_product_bom',b) for pid,value in outputs:q.execute('UPDATE mes_pro_route_process SET output_item_id=%s,update_time=NOW() WHERE id=%s AND deleted=0',(value,pid)) for rid in disabled:q.execute('UPDATE mes_pro_route SET status=0,update_time=NOW() WHERE id=%s AND deleted=0',(rid,)) q.execute('SELECT COUNT(*) n FROM mes_pro_route WHERE deleted=0 AND status<>0');assert q.fetchone()['n']==0 q.execute('SELECT COUNT(*) n FROM mes_pro_route_product p WHERE p.deleted=0 AND NOT EXISTS (SELECT 1 FROM mes_pro_route_product_bom b WHERE b.deleted=0 AND b.route_id=p.route_id AND b.product_id=p.item_id)');assert q.fetchone()['n']==0 c.commit() (out/f'complete-route-result-{stamp}.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8') print(json.dumps({'committed':True,'allRoutesEnabled':True,'allBoundProductsHaveInputs':True}))