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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
| """User-authorized, isolated demonstration data; default mode validates then rolls back."""
| import sys,json,csv,re,collections,hashlib
| from pathlib import Path
| from decimal import Decimal,ROUND_HALF_UP
| from datetime import datetime,timedelta,date
| sys.stdout.reconfigure(encoding='utf-8')
| sys.path.insert(0,str(Path('scripts/iqc-import-20261012/deps').resolve()))
| import pymysql
| P=Path(__file__).parent; D=Decimal
| TAG='OCT0814-DEMO'
| NOTE='10月8日至14日业务链演示补充;非真实交易、投料、检验或物流凭证;沿用现有示意BOM及工艺主数据。'
| apply='--apply' in sys.argv
| c=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=90,autocommit=False)
| schema=json.loads((P/'schema-snapshot.json').read_text(encoding='utf8'))
| a=json.loads((P/'assets.json').read_text(encoding='utf8'))
| fields={t:{x['Field']:x for x in v['columns']} for t,v in schema.items() if isinstance(v,dict) and 'columns' in v}
| inserted=collections.defaultdict(list); rows=collections.defaultdict(dict); sql_log=[]; links=[]; qc_links=[]; stock_keys={}; txids=[]
| def dump(name,v): (P/name).write_text(json.dumps(v,ensure_ascii=False,indent=2,default=str),encoding='utf8')
| def truth(v):return v in (1,True,b'\x01',"b'\\x01'")
| def money(v):return D(v).quantize(D('.01'),rounding=ROUND_HALF_UP)
| def qty(v):return D(v).quantize(D('.000001'),rounding=ROUND_HALF_UP)
| def weekday(dt):
| while dt.weekday()>4:dt+=timedelta(days=1)
| return dt
| def insert(t,v,user,at):
| v=dict(v);f=fields[t]
| for k,z in [('creator',str(user)),('updater',str(user)),('create_time',at),('update_time',at),('deleted',0),('tenant_id',0)]:
| if k in f:v.setdefault(k,z)
| if 'remark' in f:v.setdefault('remark',NOTE)
| assert set(v)<=set(f),(t,'unknown fields',set(v)-set(f))
| for k,z in list(v.items()):
| if f[k]['Type'].startswith('bit') and z is not None:v[k]=int(truth(z))
| if isinstance(z,str):
| m=re.match(r'(?:var)?char\((\d+)\)',f[k]['Type']);assert not m or len(z)<=int(m[1]),(t,k,'value too long')
| keys=list(v);sql='INSERT INTO `'+t+'` (`'+'`,`'.join(keys)+'`) VALUES ('+','.join(['%s']*len(keys))+')'
| q.execute(sql,tuple(v[k] for k in keys));rid=q.lastrowid
| assert rid and rid not in rows[t]
| inserted[t].append(rid);rows[t][rid]={'id':rid,**v};sql_log.append(q.mogrify(sql,tuple(v[k] for k in keys))+';')
| return rid
| def update(t,rid,v):
| assert rid in inserted[t],('Refuse existing record update',t,rid)
| sql='UPDATE `'+t+'` SET '+','.join('`'+k+'`=%s' for k in v)+',update_time=update_time WHERE id=%s'
| args=tuple(v.values())+(rid,);q.execute(sql,args);assert q.rowcount==1
| rows[t][rid].update(v);sql_log.append(q.mogrify(sql,args)+';')
| def stock(item,batch,wh,at):
| key=(item,batch,wh)
| if key in stock_keys:return stock_keys[key]
| loc=next(x for x in a['mes_wm_warehouse_location'] if x['warehouse_id']==wh and not truth(x['frozen']))
| area=next(x for x in a['mes_wm_warehouse_area'] if x['location_id']==loc['id'] and not truth(x['frozen']))
| b=rows['mes_wm_batch'][batch]
| sid=insert('mes_wm_material_stock',{'item_id':item,'batch_id':batch,'batch_code':b['code'],'warehouse_id':wh,'location_id':loc['id'],'area_id':area['id'],'quantity':D(0),'frozen_quantity':D(0),'reserved_quantity':D(0),'in_transit_quantity':D(0),'frozen':0,'receipt_time':at},244,at)
| stock_keys[key]=sid;return sid
| def dims(sid):return {k:rows['mes_wm_material_stock'][sid][k] for k in ['batch_id','batch_code','warehouse_id','location_id','area_id']}
| def tx(item,batch,wh,amount,biz,bid,code,line,at,user,related=None):
| sid=stock(item,batch,wh,at);s=rows['mes_wm_material_stock'][sid]
| new=s['quantity']+amount;assert new>=0,('Negative stock',sid,new)
| tid=insert('mes_wm_transaction',{'type':1 if amount>0 else 2,'biz_type':biz,'biz_id':bid,'biz_code':code,'biz_line_id':line,'material_stock_id':sid,'related_transaction_id':related,'item_id':item,'quantity':amount,**dims(sid),'transaction_time':at,'receipt_time':at},user,at)
| update('mes_wm_material_stock',sid,{'quantity':new});txids.append(tid);return tid,sid
| def master_template(base,code,name,types,items,at):
| if base:
| source=templates[base];inds=[x for x in a['mes_qc_template_indicator'] if x['template_id']==base]
| else:
| source={};inds=[{'indicator_id':8,'sort_order':10,'judge_flag':1,'check_method':'演示外观、数量核验,不作为真实检验标准'},{'indicator_id':9,'sort_order':20,'judge_flag':1,'check_method':'演示数量一致性核验,不作为真实检验标准'}]
| tid=insert('mes_qc_template',{'code':code,'name':name,'types':types,'status':0},289,at)
| templates[tid]=rows['mes_qc_template'][tid]
| for x in inds:
| v={k:x[k] for k in ['indicator_id','sort_order','judge_flag','check_method','standard_value','unit_measure_id','threshold_max','threshold_min','doc_url'] if k in x}
| v['template_id']=tid;rid=insert('mes_qc_template_indicator',v,289,at);a['mes_qc_template_indicator'].append(rows['mes_qc_template_indicator'][rid])
| for item in items:insert('mes_qc_template_item',{'template_id':tid,'item_id':item},289,at)
| return tid
| def qc(kind,code,item,amount,template,at,user,extra):
| t='mes_qc_'+kind;v={'code':code,'name':code+'-演示检验','template_id':template,'item_id':item,'check_quantity':amount,'qualified_quantity':amount,'unqualified_quantity':D(0),'critical_quantity':0,'major_quantity':0,'minor_quantity':0,'critical_rate':D(0),'major_rate':D(0),'minor_rate':D(0),'check_result':1,'inspect_date':at,'inspector_user_id':user,'status':4,'sampling_location':'演示样品,非真实质检记录',**extra}
| qid=insert(t,v,user,at);typ={'iqc':1,'ipqc':2,'oqc':3}[kind]
| result=insert('mes_qc_indicator_result',{'code':code+'-R','qc_id':qid,'qc_type':typ,'item_id':item},user,at)
| inds=[x for x in a['mes_qc_template_indicator'] if x['template_id']==template]
| assert inds,('Template has no indicators',template)
| for ti in inds:
| ind=indicators[ti['indicator_id']]
| insert(t+'_line',{kind+'_id':qid,'indicator_id':ind['id'],'tool':ind.get('tool'),'check_method':ti.get('check_method'),'standard_value':ti.get('standard_value'),'unit_measure_id':ti.get('unit_measure_id'),'max_threshold':ti.get('threshold_max'),'min_threshold':ti.get('threshold_min'),'critical_quantity':0,'major_quantity':0,'minor_quantity':0},user,at)
| if ind.get('result_type') in [1,2]:
| low=ti.get('threshold_min');high=ti.get('threshold_max');std=ti.get('standard_value')
| value=(D(str(low))+D(str(high)))/2 if low is not None and high is not None else D(str(std)) if std is not None else D(str(low)) if low is not None else D(str(high))/2 if high is not None else D(0)
| val=str(int(value)) if ind.get('result_type')==2 else str(value)
| else:val='演示:符合现有方案描述(非真实实测)'
| insert('mes_qc_indicator_result_detail',{'result_id':result,'indicator_id':ind['id'],'measure_no':1,'value':val,'remark':'演示样例结果;不作为真实检测或放行依据。'},user,at)
| qc_links.append({'table':t,'id':qid,'code':code,'result_id':result,'template_id':template});return qid
| def params(wo,task,rp,user,at,quantity):
| tid=rp.get('param_template_id')
| if not tid or tid not in pt:return
| template=pt[tid];details=[x for x in a['mes_pd_process_param_template_detail'] if x['template_id']==tid]
| if not details:return
| record=insert('mes_pro_work_order_param_record',{'work_order_id':wo,'task_id':task,'template_id':tid,'template_code':template['template_code'],'template_name':template['template_name'],'record_no':TAG+'-'+str(task)},user,at)
| for x in details:
| numbers=re.findall(r'\d+(?:\.\d+)?',x.get('remark') or '')
| value=numbers[0] if numbers else str(quantity) if '数量' in x['param_name'] else '1'
| if x['param_type'] not in [1,2]:value='演示:按现有模板记录'
| insert('mes_pro_work_order_param_record_detail',{'record_id':record,'template_detail_id':x['id'],'param_code':x['param_code'],'param_name':x['param_name'],'param_type':x['param_type'],'required':int(truth(x['required'])),'unit_measure_id':x.get('unit_measure_id'),'param_value':value,'remark':'演示参数值;不是现场实测参数,原工艺限值未改动。'},user,at)
|
| try:
| with c.cursor() as q:
| q.execute('SELECT DATABASE() db,NOW() now,GET_LOCK(%s,10) locked',(TAG,));state=q.fetchone();assert state['db']=='mom-hm' and state['locked']==1
| q.execute('SELECT COUNT(*) n FROM erp_sale_order WHERE no LIKE %s',(TAG+'%',));assert q.fetchone()['n']==0,'This dataset already exists; verify instead of repeating.'
| q.execute('SELECT COUNT(*) n FROM mes_energy_record WHERE remark LIKE %s',('%'+TAG+'%',));assert q.fetchone()['n']==0
| q.execute('SELECT * FROM mes_wm_material_stock WHERE deleted=0 ORDER BY id');old_stock=q.fetchall()
| q.execute('SELECT * FROM crm_receivable WHERE deleted=0 ORDER BY id');old_receivables=q.fetchall()
| protected={}
| for t in ['mdm_item','mes_md_item','mes_pro_route','mes_pro_route_product','mes_pro_route_process','mes_pro_route_product_bom','mes_qc_indicator','mes_wm_warehouse','mes_wm_warehouse_location','mes_wm_warehouse_area','crm_customer']:
| q.execute('SELECT * FROM `'+t+'` WHERE deleted=0 ORDER BY id');protected[t]=q.fetchall()
| dump('before-seed-backup.json',{'stock':old_stock,'receivables':old_receivables,'protected':protected,'at':state['now']})
| mdm={x['id']:x for x in a['mdm_item']};mes={x['id']:x for x in a['mes_md_item']};customers={x['id']:x for x in a['crm_customer']};templates={x['id']:x for x in a['mes_qc_template']};indicators={x['id']:x for x in a['mes_qc_indicator']};pt={x['id']:x for x in a['mes_pd_process_param_template']}
| active={x['id'] for x in a['users'] if x['status']==0};assert {291,244,273,289,290,243}<=active
| cfg=[('2026-10-08',977,1353,D(10),D('399.00'),12),('2026-10-09',999,1381,D(12),D('239.00'),12),('2026-10-12',497,1359,D(24),D('17.00'),14),('2026-10-14',507,1358,D(8),D('158.00'),35)]
| raw_items=sorted({b['item_id'] for day,pid,cid,n,price,qt in cfg for b in a['mes_pro_route_product_bom'] if b['product_id']==pid})
| assert all(x in mdm and x in mes for x in raw_items)
| base=datetime(2026,10,8,8)
| raw_template=master_template(None,TAG+'-IQC','演示用来料外观数量核验','1',raw_items,base)
| yan_template=master_template(12,TAG+'-IPQC','演示用燕窝过程检验(参考已有方案)','2',[977,999],base)
| yan_out_template=master_template(12,TAG+'-OQC','演示用燕窝出货检验(参考已有方案)','3',[977,999],base)
| for index,(day,pid,cid,count,price,quality_template) in enumerate(cfg,1):
| if pid in [977,999]:quality_template=yan_out_template
| day=datetime.fromisoformat(day);assert day.weekday()<5 and day.date()<=date(2026,10,14)
| at=lambda minutes:day+timedelta(hours=8,minutes=minutes)
| item=mdm[pid];mitem=mes[pid];assert mitem['mdm_item_id']==pid and mitem['unit_measure_id']==item['unit_measure_id']
| assert customers[cid]['owner_user_id']==291
| gross_unit=money(price*D('1.13'));net=money(price*count);tax=money(net*D('.13'));total=net+tax
| prefix=TAG+'-'+day.strftime('%m%d')+'-'+str(index)
| business=insert('crm_business',{'name':'演示-'+item['name']+'-'+day.strftime('%m%d'),'customer_id':cid,'owner_user_id':291,'follow_up_status':1,'status_type_id':1,'status_id':5,'end_status':1,'deal_time':at(25),'total_product_price':net,'discount_percent':D(0),'total_price':total},291,at(0))
| insert('crm_business_product',{'business_id':business,'item_id':pid,'item_unit_id':item['unit_measure_id'],'item_price':price,'business_price':price,'count':count,'total_price':net},291,at(0))
| quote_code=prefix+'-BJ';quote=insert('crm_sale_quotation',{'no':quote_code,'name':'演示报价-'+item['name'],'customer_id':cid,'business_id':business,'owner_user_id':291,'status':10,'quotation_time':at(10),'valid_until':weekday(at(10)+timedelta(days=30)),'total_product_price':net,'discount_percent':D(0),'discount_price':D(0),'total_price':total,'tax_rate':D(13),'tax_price':tax,'remark':NOTE+' 报价为演示价格,非现行报价政策;'+TAG},291,at(10))
| insert('crm_sale_quotation_product',{'quotation_id':quote,'item_id':pid,'item_unit_id':item['unit_measure_id'],'item_price':price,'quotation_price':price,'count':count,'total_price':net,'tax_percent':D(13),'tax_price':tax},291,at(10))
| contract_code=prefix+'-HT';contract=insert('crm_contract',{'no':contract_code,'name':'演示合同-'+item['name'],'customer_id':cid,'business_id':business,'owner_user_id':291,'audit_status':20,'order_date':at(20),'start_time':at(20),'end_time':weekday(at(20)+timedelta(days=30)),'total_product_price':total,'total_price':total,'discount_percent':D(0),'deposit_price':D(0),'sign_user_id':291},291,at(20))
| insert('crm_contract_product',{'contract_id':contract,'item_id':pid,'item_unit_id':item['unit_measure_id'],'item_price':gross_unit,'contract_price':gross_unit,'count':count,'total_price':total},291,at(20))
| sale_code=prefix+'-XS';order=insert('erp_sale_order',{'no':sale_code,'status':20,'customer_id':cid,'contract_id':contract,'contract_no':contract_code,'sale_user_id':291,'order_time':at(25),'total_count':count,'total_product_price':net,'total_tax_price':tax,'total_price':total,'discount_percent':D(0),'discount_price':D(0),'deposit_price':D(0),'out_count':count,'out_status':3,'production_status':1,'production_finish_time':at(345)},291,at(25))
| order_item=insert('erp_sale_order_items',{'order_id':order,'product_id':pid,'product_unit_id':item['unit_measure_id'],'product_price':price,'count':count,'total_price':net,'tax_percent':D(13),'tax_price':tax,'out_count':count,'need_production':1},291,at(25))
| update('crm_contract',contract,{'order_id':order,'order_no':sale_code});update('crm_sale_quotation',quote,{'contract_id':contract,'contract_no':contract_code});update('crm_business',business,{'order_id':order,'order_no':sale_code})
| plan_ids=[]
| for period,portion,days in [(1,money(total*D('.3')),7),(2,total-money(total*D('.3')),30)]:
| due=weekday(at(480)+timedelta(days=days));remind=weekday(due-timedelta(days=3))
| plan_ids.append(insert('crm_receivable_plan',{'period':period,'customer_id':cid,'contract_id':contract,'owner_user_id':291,'return_time':due,'return_type':8,'price':portion,'remind_days':3,'remind_time':remind,'receivable_id':None,'remark':NOTE+' 约定回款日期与比例为演示计划;未录入实际回款。'},291,at(22)))
| for biz,bid in [(4,business),(5,contract),(9,quote)]+[(8,p) for p in plan_ids]:insert('crm_permission',{'biz_type':biz,'biz_id':bid,'user_id':291,'level':1},291,at(25))
| insert('crm_follow_up_record',{'biz_type':4,'biz_id':business,'type':1,'content':'演示跟进:客户询价→报价确认→签约→转销售生产;'+TAG,'next_time':weekday(at(480)+timedelta(days=1))},291,at(5))
| route=next(x for x in a['mes_pro_route_product'] if x['item_id']==pid)['route_id'];processes=sorted([x for x in a['mes_pro_route_process'] if x['route_id']==route],key=lambda x:x['sort'])
| boms=[x for x in a['mes_pro_route_product_bom'] if x['route_id']==route and x['product_id']==pid];assert processes and boms
| wo_code=prefix+'-WO';wo=insert('mes_pro_work_order',{'code':wo_code,'name':'演示生产-'+item['name'],'type':1,'order_source_type':1,'order_source_code':sale_code,'product_id':pid,'quantity':count,'quantity_produced':count,'quantity_scheduled':count,'quantity_changed':D(0),'client_id':cid,'route_id':route,'auto_finish_flag':1,'request_date':at(330),'finish_date':at(345),'status':2,'is_feedback_approve':0},244,at(45))
| mps_code=prefix+'-MPS';mps=insert('mes_pro_mps',{'code':mps_code,'status':2,'sale_order_id':order,'sale_order_no':sale_code,'sale_order_item_id':order_item,'product_id':pid,'quantity':count,'quantity_produced':count,'unit_measure_id':item['unit_measure_id'],'client_id':cid,'request_date':at(330),'issue_date':at(45),'finish_date':at(345),'work_order_id':wo,'work_order_code':wo_code,'merge_flag':0},244,at(35));update('erp_sale_order_items',order_item,{'mps_id':mps})
| for b in boms:insert('mes_pro_work_order_bom',{'work_order_id':wo,'process_id':b['process_id'],'item_id':b['item_id'],'quantity':qty(D(b['quantity'])*count)},244,at(45))
| raw={}
| for n,b in enumerate(boms,1):
| raw_id=b['item_id'];rq=qty(D(b['quantity'])*count);assert rq>0
| code=prefix+'-RAW'+str(n)
| notice=insert('mes_wm_arrival_notice',{'code':code+'-DH','name':'演示来料-'+mdm[raw_id]['name'],'arrival_date':at(50),'status':4},244,at(50))
| nl=insert('mes_wm_arrival_notice_line',{'notice_id':notice,'item_id':raw_id,'arrival_quantity':rq,'qualified_quantity':rq,'iqc_check_flag':1,'iqc_check_result':1},244,at(50))
| iqc=qc('iqc',code+'-IQC',raw_id,rq,raw_template,at(60),290,{'source_doc_type':100,'source_doc_id':notice,'source_line_id':nl,'source_doc_code':code+'-DH','received_quantity':rq,'receive_date':at(50),'vendor_batch':code+'-PC','vendor_name':'演示批次来料(不代表真实供应商交付)'})
| update('mes_wm_arrival_notice_line',nl,{'iqc_id':iqc})
| batch=insert('mes_wm_batch',{'code':code+'-PC','item_id':raw_id,'produce_date':at(50).date(),'receipt_date':at(70).date(),'quality_status':1,'lot_number':TAG+'-RAW','purchase_order_code':None},244,at(65))
| wh=next((x['warehouse_id'] for x in a['mes_wm_material_stock'] if x['item_id']==raw_id and x['warehouse_id'] not in [20,21,23]),24)
| rec=insert('mes_wm_item_receipt',{'code':code+'-RK','name':'演示来料入库-'+mdm[raw_id]['name'],'iqc_id':iqc,'notice_id':notice,'receipt_date':at(70),'status':4},244,at(65))
| rl=insert('mes_wm_item_receipt_line',{'receipt_id':rec,'arrival_notice_line_id':nl,'item_id':raw_id,'enter_unit_measure_id':mdm[raw_id]['unit_measure_id'],'enter_quantity':rq,'received_quantity':rq,'batch_id':batch,'batch_code':code+'-PC','production_date':at(50).date(),'lot_number':TAG+'-RAW'},244,at(65))
| itx,sid=tx(raw_id,batch,wh,rq,110,rec,code+'-RK',rl,at(70),244)
| insert('mes_wm_item_receipt_detail',{'receipt_id':rec,'line_id':rl,'material_stock_id':sid,'item_id':raw_id,'quantity':rq,**{k:v for k,v in dims(sid).items() if k!='batch_code'}},244,at(70))
| raw.setdefault(b['process_id'],[]).append({'item':raw_id,'qty':rq,'batch':batch,'stock':sid,'bom':b})
| previous=None;task_ids=[];feedback_ids=[];all_batches=[]
| for j,rp in enumerate(processes):
| process=rp['process_id'];station=next(x['id'] for x in a['mes_md_workstation'] if x['process_id']==process);user=273 if j%2 else 244
| start=at(90+j*25);finish=start+timedelta(minutes=20);pcode=prefix+'-P'+str(j+1)
| snapshot={k:rp.get(k) for k in ['process_id','param_template_id','sort','next_process_id','link_type','prepare_time','wait_time','color_code','key_flag','check_flag','backflush_flag','output_item_id']};snapshot['work_order_id']=wo
| insert('mes_pro_work_order_process',snapshot,244,at(45))
| task=insert('mes_pro_task',{'code':pcode+'-TK','name':'演示工序-'+str(process),'work_order_id':wo,'workstation_id':station,'route_id':route,'process_id':process,'param_template_id':rp.get('param_template_id'),'item_id':pid,'quantity':count,'produced_quantity':count,'qualify_quantity':count,'unqualify_quantity':D(0),'client_id':cid,'start_time':start,'end_time':finish,'finish_date':finish+timedelta(minutes=3),'first_feedback_time':finish,'last_feedback_time':finish,'status':4},user,start);task_ids.append(task)
| params(wo,task,rp,user,finish,count)
| feedback=insert('mes_pro_feedback',{'code':pcode+'-FB','type':1,'feedback_time':finish,'workstation_id':station,'route_id':route,'process_id':process,'work_order_id':wo,'task_id':task,'item_id':pid,'scheduled_quantity':count,'feedback_quantity':count,'qualified_quantity':count,'unqualified_quantity':D(0),'uncheck_quantity':D(0),'labor_scrap_quantity':D(0),'material_scrap_quantity':D(0),'other_scrap_quantity':D(0),'feedback_user_id':user,'approve_user_id':244,'status':4},user,finish);feedback_ids.append(feedback)
| inputs=[]
| if process in raw:
| issue=insert('mes_wm_product_issue',{'code':pcode+'-LL','name':'演示生产领料','work_order_id':wo,'task_id':task,'workstation_id':station,'issue_date':start-timedelta(minutes=5),'required_time':start,'status':4},244,start-timedelta(minutes=10))
| for r in raw[process]:
| il=insert('mes_wm_product_issue_line',{'issue_id':issue,'task_id':task,'item_id':r['item'],'enter_unit_measure_id':mdm[r['item']]['unit_measure_id'],'enter_quantity':r['qty'],'quantity':r['qty'],'batch_id':r['batch']},244,start-timedelta(minutes=10))
| insert('mes_wm_product_issue_detail',{'issue_id':issue,'line_id':il,'material_stock_id':r['stock'],'item_id':r['item'],'quantity':r['qty'],**dims(r['stock'])},244,start-timedelta(minutes=10))
| otx,_=tx(r['item'],r['batch'],rows['mes_wm_material_stock'][r['stock']]['warehouse_id'],-r['qty'],116,issue,pcode+'-LL',il,start-timedelta(minutes=5),244)
| _,ws=tx(r['item'],r['batch'],23,r['qty'],116,issue,pcode+'-LL',il,start-timedelta(minutes=5),244,otx)
| insert('mes_pro_task_issue',{'task_id':task,'work_order_id':wo,'workstation_id':station,'source_doc_type':'PRODUCT_ISSUE','source_doc_id':issue,'source_line_id':il,'source_doc_code':pcode+'-LL','batch_code':dims(ws)['batch_code'],'item_id':r['item'],'unit_measure_id':mdm[r['item']]['unit_measure_id'],'issued_quantity':r['qty'],'available_quantity':D(0),'used_quantity':r['qty']},user,start)
| inputs.append({'item':r['item'],'qty':r['qty'],'batch':r['batch'],'stock':ws})
| if previous:inputs.append({'item':pid,'qty':count,'batch':previous['batch'],'stock':previous['stock']})
| consume=insert('mes_wm_item_consume',{'code':pcode+'-XH','name':'演示工序投入消耗','work_order_id':wo,'task_id':task,'workstation_id':station,'process_id':process,'feedback_id':feedback,'consume_date':start,'status':4},user,start)
| assert inputs,('Process missing material input',process)
| for inp in inputs:
| cl=insert('mes_wm_item_consume_line',{'consume_id':consume,'item_id':inp['item'],'quantity':inp['qty'],'batch_id':inp['batch'],'batch_code':dims(inp['stock'])['batch_code']},user,start)
| insert('mes_wm_item_consume_detail',{'consume_id':consume,'line_id':cl,'material_stock_id':inp['stock'],'item_id':inp['item'],'quantity':inp['qty'],**dims(inp['stock'])},user,start)
| tx(inp['item'],inp['batch'],23,-inp['qty'],123,consume,pcode+'-XH',cl,start,user)
| batch_code=pcode+'-PC';batch=insert('mes_wm_batch',{'code':batch_code,'item_id':pid,'produce_date':finish.date(),'expire_date':(finish+timedelta(days=180)).date(),'work_order_id':wo,'task_id':task,'workstation_id':station,'client_id':cid,'sales_order_code':sale_code,'lot_number':TAG+'-'+day.strftime('%m%d'),'quality_status':1},user,finish)
| produce=insert('mes_wm_product_produce',{'code':pcode+'-CC','name':'演示工序产出','work_order_id':wo,'feedback_id':feedback,'workstation_id':station,'process_id':process,'task_id':task,'produce_date':finish+timedelta(minutes=3),'status':4},user,finish)
| pl=insert('mes_wm_product_produce_line',{'produce_id':produce,'feedback_id':feedback,'item_id':pid,'quantity':count,'batch_id':batch,'batch_code':batch_code,'expire_date':(finish+timedelta(days=180)).date(),'lot_number':TAG+'-'+day.strftime('%m%d'),'quality_status':1},user,finish)
| if truth(rp['check_flag']):
| qc('ipqc',pcode+'-IPQC',pid,count,yan_template if pid in [977,999] else quality_template,finish+timedelta(minutes=2),289,{'type':5,'source_doc_type':124,'source_doc_id':produce,'source_line_id':pl,'source_doc_code':pcode+'-CC','work_order_id':wo,'task_id':task,'workstation_id':station,'process_id':process})
| _,sid=tx(pid,batch,23,count,124,produce,pcode+'-CC',pl,finish+timedelta(minutes=3),user)
| insert('mes_wm_product_produce_detail',{'produce_id':produce,'line_id':pl,'item_id':pid,'quantity':count,**dims(sid)},user,finish+timedelta(minutes=3))
| previous={'batch':batch,'stock':sid,'code':batch_code};all_batches.append(batch)
| assert raw.keys()<=set(x['process_id'] for x in processes)
| batch=previous['batch'];batch_code=previous['code']
| update('mes_pro_work_order',wo,{'batch_code':batch_code})
| rec=insert('mes_wm_product_receipt',{'code':prefix+'-RK','name':'演示成品入库-'+item['name'],'work_order_id':wo,'item_id':pid,'receipt_date':at(390),'status':4},244,at(375))
| rl=insert('mes_wm_product_receipt_line',{'receipt_id':rec,'item_id':pid,'quantity':count,'material_stock_id':previous['stock'],'batch_id':batch,'batch_code':batch_code},244,at(375))
| otx,_=tx(pid,batch,23,-count,118,rec,prefix+'-RK',rl,at(390),244)
| _,fg_stock=tx(pid,batch,18,count,118,rec,prefix+'-RK',rl,at(390),244,otx)
| insert('mes_wm_product_receipt_detail',{'receipt_id':rec,'line_id':rl,'material_stock_id':fg_stock,'item_id':pid,'quantity':count,**{k:v for k,v in dims(fg_stock).items() if k!='batch_code'}},244,at(390))
| notice=insert('mes_wm_sales_notice',{'code':prefix+'-FH','name':'演示发货通知-'+item['name'],'client_id':cid,'sale_order_id':order,'sale_order_code':sale_code,'sales_date':at(400),'delivery_date':at(450),'status':4,'out_status':2},291,at(400))
| nl=insert('mes_wm_sales_notice_line',{'notice_id':notice,'sale_order_item_id':order_item,'item_id':pid,'batch_id':batch,'batch_code':batch_code,'quantity':count,'out_count':count,'oqc_check_flag':1},291,at(400))
| sales=insert('mes_wm_product_sales',{'code':prefix+'-CK','name':'演示销售出库-'+item['name'],'client_id':cid,'sale_order_id':order,'notice_id':notice,'sales_order_code':sale_code,'sales_date':at(450),'carrier':'演示自配送','shipping_number':prefix+'-SHIP','status':4},244,at(410))
| sl=insert('mes_wm_product_sales_line',{'sales_id':sales,'sale_order_item_id':order_item,'notice_line_id':nl,'item_id':pid,'enter_unit_measure_id':item['unit_measure_id'],'enter_quantity':count,'batch_id':batch,'batch_code':batch_code,'material_stock_id':fg_stock,'quantity':count,'oqc_check_flag':1,'quality_status':1},244,at(410))
| oqc=qc('oqc',prefix+'-OQC',pid,count,quality_template,at(420),290,{'source_doc_type':119,'source_doc_id':sales,'source_line_id':sl,'source_doc_code':prefix+'-CK','client_id':cid,'batch_code':batch_code,'out_quantity':count,'out_date':at(450)})
| update('mes_wm_product_sales_line',sl,{'oqc_id':oqc})
| insert('mes_wm_product_sales_detail',{'sales_id':sales,'line_id':sl,'material_stock_id':fg_stock,'item_id':pid,'quantity':count,**dims(fg_stock)},244,at(450))
| insert('mes_wm_stock_reserve',{'material_stock_id':fg_stock,'biz_type':1,'biz_id':sales,'biz_code':prefix+'-CK','biz_line_id':sl,'item_id':pid,'quantity':count,'status':3},244,at(440))
| tx(pid,batch,18,-count,119,sales,prefix+'-CK',sl,at(450),244)
| links.append({'date':str(day.date()),'product':item['name'],'spec':item['specification'],'quantity':count,'unit_id':item['unit_measure_id'],'customer':customers[cid]['name'],'business':business,'quotation_id':quote,'quotation_no':quote_code,'contract_id':contract,'contract_no':contract_code,'sale_order_id':order,'sale_order_no':sale_code,'order_item_id':order_item,'total':total,'receivable_plans':plan_ids,'mps_id':mps,'mps_code':mps_code,'work_order_id':wo,'work_order_code':wo_code,'tasks':task_ids,'feedbacks':feedback_ids,'batches':all_batches,'final_batch_id':batch,'final_batch_code':batch_code,'receipt_id':rec,'notice_id':notice,'sales_id':sales,'oqc_id':oqc})
| energy=[('2026-10-08','WATER','原浆调配区','滋补品原浆','清洗、调配用水',D('32.600'),D('5.2000')),('2026-10-09','ELECTRICITY','燕窝加工区','燕窝','加工及包装设备用电',D('1268.500'),D('.8300')),('2026-10-12','STEAM','原浆杀菌区','滋补品原浆','加热杀菌用汽',D('1.850'),D('248.0000')),('2026-10-14','ELECTRICITY','动力站','公共能耗','动力设备运行用电',D('1586.200'),D('.8300'))]
| for day,kind,area,category,purpose,n,unit_price in energy:
| dt=datetime.fromisoformat(day);assert dt.weekday()<5
| insert('mes_energy_record',{'record_date':dt.date(),'energy_type':kind,'usage_area':area,'product_category':category,'usage_purpose':purpose,'quantity':n,'unit_price':unit_price,'amount':money(n*unit_price),'recorder':'赵磊','remark':NOTE+' 单价参考现有能耗台账,用量为演示补充;'+TAG},243,dt+timedelta(hours=16))
| # Complete business, quantity, traceability and preservation checks before commit.
| for link in links:
| q.execute('SELECT * FROM crm_sale_quotation WHERE id=%s',(link['quotation_id'],));quotation=q.fetchone();assert quotation['contract_id']==link['contract_id'] and quotation['total_price']==link['total']
| q.execute('SELECT * FROM crm_contract WHERE id=%s',(link['contract_id'],));contract=q.fetchone();assert contract['order_id']==link['sale_order_id'] and contract['total_price']==link['total'] and contract['audit_status']==20
| q.execute('SELECT * FROM crm_contract_product WHERE contract_id=%s',(link['contract_id'],));cp=q.fetchone();assert money(cp['contract_price']*cp['count'])==cp['total_price']
| q.execute('SELECT * FROM erp_sale_order WHERE id=%s',(link['sale_order_id'],));order=q.fetchone();assert order['total_price']==link['total'] and order['out_count']==link['quantity'] and order['production_status']==1 and order['out_status']==3
| q.execute('SELECT SUM(price) total FROM crm_receivable_plan WHERE contract_id=%s AND deleted=0',(link['contract_id'],));assert q.fetchone()['total']==link['total']
| q.execute('SELECT * FROM mes_pro_mps WHERE id=%s',(link['mps_id'],));mp=q.fetchone();assert mp['sale_order_id']==order['id'] and mp['sale_order_no']==order['no'] and mp['quantity']==link['quantity'] and mp['quantity_produced']==link['quantity']
| q.execute('SELECT * FROM erp_sale_order_items WHERE id=%s',(link['order_item_id'],));oi=q.fetchone();assert oi['mps_id']==mp['id'] and oi['out_count']==oi['count']==mp['quantity']
| q.execute('SELECT COUNT(*) n FROM mes_pro_task WHERE work_order_id=%s AND status=4 AND deleted=0',(link['work_order_id'],));assert q.fetchone()['n']==len(link['tasks'])
| # Same joins as the real batch forward/backward APIs.
| q.execute('SELECT COUNT(*) n FROM mes_wm_transaction t JOIN mes_wm_product_produce pp ON t.biz_id=pp.id AND t.biz_type=124 JOIN mes_pro_feedback f ON f.id=pp.feedback_id JOIN mes_wm_item_consume ic ON ic.feedback_id=f.id JOIN mes_wm_transaction raw ON raw.biz_id=ic.id AND raw.biz_type=123 WHERE t.batch_code=%s',(link['final_batch_code'],));assert q.fetchone()['n']>0
| q.execute('SELECT COUNT(*) n FROM mes_wm_product_sales_line sl JOIN mes_wm_product_sales ss ON ss.id=sl.sales_id JOIN mes_qc_oqc o ON o.id=sl.oqc_id WHERE sl.batch_code=%s AND ss.sale_order_id=%s AND o.check_result=1 AND o.status=4',(link['final_batch_code'],order['id']));assert q.fetchone()['n']==1
| for t,ids in inserted.items():
| q.execute('SELECT COUNT(*) n FROM `'+t+'` WHERE id IN ('+','.join(['%s']*len(ids))+')',ids);assert q.fetchone()['n']==len(ids)
| mappings={(x['template_id'],x['item_id']) for x in a['mes_qc_template_item']}|{(x['template_id'],x['item_id']) for x in rows['mes_qc_template_item'].values()}
| for check in qc_links:
| row=rows[check['table']][check['id']];kind={'mes_qc_iqc':'1','mes_qc_ipqc':'2','mes_qc_oqc':'3'}[check['table']]
| assert kind in templates[check['template_id']]['types'].split(',') and (check['template_id'],row['item_id']) in mappings,('Inapplicable quality template',check)
| for t,records in rows.items():
| for record in records.values():
| created=record.get('create_time')
| assert not created or (datetime(2026,10,8)<=created<=state['now'] and created.weekday()<5),('Invalid workday timestamp',t,record['id'])
| for record in rows['crm_receivable_plan'].values():assert record['return_time'].weekday()<5 and record['receivable_id'] is None
| q.execute('SELECT * FROM mes_wm_material_stock WHERE deleted=0 ORDER BY id');stock_after={x['id']:x for x in q.fetchall()}
| assert all(stock_after[x['id']]==x for x in old_stock),'Original inventory was modified'
| assert all(stock_after[x]['quantity']==0 and stock_after[x]['reserved_quantity']==0 for x in inserted['mes_wm_material_stock']),'Isolated demonstration stock not balanced'
| balances=collections.defaultdict(Decimal)
| for row in rows['mes_wm_transaction'].values():balances[row['material_stock_id']]+=row['quantity']
| assert all(v==0 for v in balances.values()),'Transaction ledger not balanced'
| q.execute('SELECT * FROM crm_receivable WHERE deleted=0 ORDER BY id');assert q.fetchall()==old_receivables,'Actual receivables changed'
| for t,before in protected.items():
| q.execute('SELECT * FROM `'+t+'` WHERE deleted=0 ORDER BY id');assert q.fetchall()==before,('Protected master changed',t)
| result={'mode':'apply' if apply else 'dry-run','committed':apply,'dates':['2026-10-08','2026-10-09','2026-10-12','2026-10-14'],'business_chains':4,'energy_records':4,'actual_receivables_added':0,'counts':{t:len(v) for t,v in inserted.items()},'traceability_verified':True,'original_inventory_unchanged':True,'new_stock_balanced':True,'links':links,'quality':qc_links}
| dump('plan-'+('applied' if apply else 'dry-run')+'.json',result)
| if apply:c.commit()
| else:c.rollback()
| dump('result-'+('applied' if apply else 'dry-run')+'.json',result)
| (P/('executed.sql' if apply else 'preview.sql')).write_text('START TRANSACTION;\n'+'\n'.join(sql_log)+'\n'+('COMMIT;' if apply else 'ROLLBACK;'),encoding='utf8')
| print(json.dumps({k:v for k,v in result.items() if k not in ['links','quality']},ensure_ascii=False))
| except Exception:
| c.rollback();raise
| finally:
| with c.cursor() as q:q.execute('SELECT RELEASE_LOCK(%s)',(TAG,))
| c.close()
|
|