"""Authorized historical shipment completion with explicitly paired stock compensation. No external carrier request; generated tracking numbers are simulation data. """ import sys,json,csv from pathlib import Path from decimal import Decimal P=Path(__file__).parent sys.path.insert(0,str(P.parent/'iqc-import-20261012/deps')) import pymysql D=Decimal canonical=lambda x:json.loads(json.dumps(x,ensure_ascii=False,default=str)) draft=json.loads((P/'shipping-drafts-result.json').read_text(encoding='utf8')) assert not (P/'shipping-complete-result.json').exists(),'Already completed; verify instead of repeating.' c=pymysql.connect(host='36.213.150.237',port=10000,user='root',password=sys.stdin.readline().strip(),database='mom-hm',charset='utf8mb4',autocommit=False,cursorclass=pymysql.cursors.DictCursor) result={'orders':[],'allocations':[],'new_stock_ids':[],'new_batch_ids':[], 'unit_basis':{}} try: with c.cursor() as q: q.execute('SELECT DATABASE() db,NOW() now,GET_LOCK(%s,10) locked',('sales-ledger-202609',));state=q.fetchone();assert state['db']=='mom-hm' and state['locked']==1;now=state['now'] # Reconcile the five columns required by the repository's receipt line DO. # DDL commits implicitly, so save its schema/rows separately BEFORE the data transaction. q.execute('SHOW CREATE TABLE mes_wm_misc_receipt_line');schema_before=q.fetchone() q.execute('SELECT * FROM mes_wm_misc_receipt_line ORDER BY id');schema_before['rows']=q.fetchall() if not (P/'shipping-receipt-schema-before.json').exists():(P/'shipping-receipt-schema-before.json').write_text(json.dumps(schema_before,ensure_ascii=False,default=str,indent=2),encoding='utf8') q.execute('SHOW COLUMNS FROM mes_wm_misc_receipt_line');fields={r['Field'] for r in q.fetchall()} missing={k:v for k,v in {'batch_code':'varchar(100) DEFAULT NULL','warehouse_id':'bigint DEFAULT NULL','location_id':'bigint DEFAULT NULL','area_id':'bigint DEFAULT NULL'}.items() if k not in fields} if missing:q.execute('ALTER TABLE mes_wm_misc_receipt_line '+','.join('ADD COLUMN `'+k+'` '+v for k,v in missing.items())) result['added_receipt_line_columns']=list(missing) q.execute('SHOW CREATE TABLE mes_wm_misc_receipt_detail');detail_schema=q.fetchone() if not (P/'shipping-receipt-detail-schema-before.json').exists():(P/'shipping-receipt-detail-schema-before.json').write_text(json.dumps(detail_schema,ensure_ascii=False,default=str,indent=2),encoding='utf8') q.execute('SHOW COLUMNS FROM mes_wm_misc_receipt_detail');detail_fields={r['Field'] for r in q.fetchall()} if 'batch_code' not in detail_fields:q.execute('ALTER TABLE mes_wm_misc_receipt_detail ADD COLUMN batch_code varchar(100) DEFAULT NULL') c.begin() tabs=['erp_sale_order','erp_sale_order_items','mes_wm_sales_notice','mes_wm_sales_notice_line','mes_wm_product_sales','mes_wm_product_sales_line','mes_wm_product_sales_detail','mes_wm_misc_receipt','mes_wm_misc_receipt_line','mes_wm_misc_receipt_detail','mes_wm_stock_reserve','mes_wm_transaction','mes_wm_material_stock','mes_wm_batch','mdm_item','mes_wm_warehouse','mes_wm_warehouse_location','mes_wm_warehouse_area','mes_qc_oqc','crm_contract','crm_contract_product','crm_sale_quotation','crm_sale_quotation_product','crm_invoice','crm_receivable','crm_receivable_plan','crm_customer','crm_permission'] data={};schemas={};backup={'state':state,'tables':{}} for t in tabs: q.execute('SELECT * FROM `'+t+'` WHERE deleted=0 ORDER BY id'+(' FOR UPDATE' if t in ['mes_wm_material_stock','erp_sale_order','erp_sale_order_items','mes_wm_product_sales'] else ''));rows=q.fetchall();data[t]={r['id']:r for r in rows};backup['tables'][t]=rows q.execute('SHOW COLUMNS FROM `'+t+'`');schemas[t]={r['Field'] for r in q.fetchall()} (P/'shipping-complete-locked-before.json').write_text(json.dumps(backup,ensure_ascii=False,default=str,indent=2),encoding='utf8') q.execute('SELECT status FROM system_users WHERE id=244 AND deleted=0');assert q.fetchone()['status']==0 def insert(t,v): v={**v,'creator':'244','updater':'244','create_time':now,'update_time':now,'deleted':0};assert set(v)<=schemas[t],(t,set(v)-schemas[t]) q.execute('INSERT INTO `'+t+'` ('+','.join('`'+k+'`' for k in v)+') VALUES ('+','.join(['%s']*len(v))+')',list(v.values()));return q.lastrowid def update(t,id,v): v={**v,'updater':'244','update_time':now};assert set(v)<=schemas[t] q.execute('UPDATE `'+t+'` SET '+','.join('`'+k+'`=%s' for k in v)+' WHERE id=%s AND deleted=0',list(v.values())+[id]);assert q.rowcount==1 false=lambda x:x in (None,0,False,b'\x00') note='九月历史销售流程补录;按用户要求等量补库存后出库,净库存不变;非真实采购或生产入库;汇总批次非真实生产批次;补录时间为本次执行时间。' rc_code='QTCKDC-202609-20261013' assert not any(x['code']==rc_code for x in data['mes_wm_misc_receipt'].values()) receipt=insert('mes_wm_misc_receipt',{'code':rc_code,'name':'九月销售出库等量库存补充','receipt_date':now,'status':0,'remark':note}) result.update(receipt_id=receipt,receipt_code=rc_code) selected={} mismatch_ids={538,539,1001,1002,497,509} for order in draft['orders']: h=data['erp_sale_order'][order['order_id']];sales=data['mes_wm_product_sales'][order['sales_id']];notice=data['mes_wm_sales_notice'][order['notice_id']] assert h['no']==order['no'] and h['status']==20 and h['out_count']==0 and h['out_status']==1 and sales['status']==0 and notice['status']==3 assert not any(x['sales_id']==sales['id'] for x in data['mes_wm_product_sales_detail'].values()) for ln in order['lines']: item=data['mdm_item'][ln['item_id']];qty=D(ln['quantity']);assert qty>0 and item['status']==0 ol=data['erp_sale_order_items'][ln['order_item_id']];sl=data['mes_wm_product_sales_line'][ln['sales_line_id']] assert ol['count']==qty and ol['out_count']==0 and sl['quantity']==qty and false(sl['oqc_check_flag']) basis='销售单位与物料主单位一致' if not ln['unit_match']: assert item['id'] in mismatch_ids # This is same finished SKU/package count, never a generic unit conversion. basis='同SKU完整销售包装计数按1:1补录;瓶/盒/个为原台账计数称谓;规格及物料匹配一致;不新增通用单位换算' result['unit_basis'][str(item['id'])]={'name':item.get('name'),'source_unit_id':ln['sale_unit_id'],'primary_unit_id':ln['primary_unit_id'],'basis':basis} if item['id'] not in selected: eligible=[] for stock in data['mes_wm_material_stock'].values(): if stock['item_id']!=item['id'] or not false(stock['frozen']):continue wh=data['mes_wm_warehouse'].get(stock['warehouse_id']);loc=data['mes_wm_warehouse_location'].get(stock['location_id']);area=data['mes_wm_warehouse_area'].get(stock['area_id']) if not wh or not loc or not area:continue if any(not false(x['frozen']) for x in [wh,loc,area]):continue if loc['warehouse_id']!=wh['id'] or area['location_id']!=loc['id']:continue batch=data['mes_wm_batch'].get(stock['batch_id']) if not batch or batch['item_id']!=item['id'] or batch['code']!=stock['batch_code']:continue # Existing initialization summaries are appropriate for a historical offset. if not stock['batch_code'].startswith('INIT202609-'):continue if stock['quantity']-D(stock['frozen_quantity'] or 0)-D(stock['reserved_quantity'] or 0)<0:continue eligible.append(stock) if eligible:selected[item['id']]=min(eligible,key=lambda x:x['id']) else: assert item['id'] in {1995,1996,1997},('No valid summary stock',item['id']) whid=17 if item['id']==1995 else 18 loc=next(x for x in data['mes_wm_warehouse_location'].values() if x['warehouse_id']==whid and false(x['frozen'])) area=next(x for x in data['mes_wm_warehouse_area'].values() if x['location_id']==loc['id'] and false(x['frozen'])) assert false(data['mes_wm_warehouse'][whid]['frozen']) bc='HIS-SALES-202609-'+str(item['id']) bid=insert('mes_wm_batch',{'code':bc,'item_id':item['id'],'receipt_date':now,'remark':note});result['new_batch_ids'].append(bid) values={'item_id':item['id'],'item_type_id':item.get('item_type_id'),'batch_id':bid,'batch_code':bc,'warehouse_id':whid,'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':now,'remark':note} sid=insert('mes_wm_material_stock',values);values['id']=sid;selected[item['id']]=values;result['new_stock_ids'].append(sid) stock=selected[item['id']];dims={k:stock[k] for k in ['batch_code','warehouse_id','location_id','area_id']} remark='对应销售原单'+order['no']+'、出库行'+str(sl['id'])+';等量补入后出库,净库存不变;'+basis rid=insert('mes_wm_misc_receipt_line',{'receipt_id':receipt,'item_id':item['id'],'quantity':qty,**dims,'remark':remark}) detailvals={'receipt_id':receipt,'line_id':rid,'item_id':item['id'],'quantity':qty,**dims,'remark':remark} insert('mes_wm_misc_receipt_detail',detailvals) tx={'item_id':item['id'],'material_stock_id':stock['id'],'quantity':qty,'batch_id':stock['batch_id'],**dims,'transaction_time':now,'receipt_time':now} itx=insert('mes_wm_transaction',{'type':1,'biz_type':115,'biz_id':receipt,'biz_code':rc_code,'biz_line_id':rid,**tx}) q.execute('UPDATE mes_wm_material_stock SET quantity=quantity+%s,updater=%s,update_time=%s WHERE id=%s',(qty,'244',now,stock['id']));assert q.rowcount==1 did=insert('mes_wm_product_sales_detail',{'sales_id':sales['id'],'line_id':sl['id'],'item_id':item['id'],'material_stock_id':stock['id'],'quantity':qty,'batch_id':stock['batch_id'],**dims,'remark':remark}) update('mes_wm_product_sales_line',sl['id'],{'batch_id':stock['batch_id'],'batch_code':stock['batch_code'],'material_stock_id':stock['id'],'remark':remark}) update('mes_wm_sales_notice_line',ln['notice_line_id'],{'batch_id':stock['batch_id'],'batch_code':stock['batch_code'],'remark':remark}) result['allocations'].append({**ln,'sales_id':sales['id'],'no':order['no'],'stock_id':stock['id'],'detail_id':did,'receipt_line_id':rid,'in_transaction_id':itx,'batch_id':stock['batch_id'],'batch_code':stock['batch_code'],'warehouse_id':stock['warehouse_id'],'location_id':stock['location_id'],'area_id':stock['area_id']}) update('mes_wm_misc_receipt',receipt,{'status':3});update('mes_wm_misc_receipt',receipt,{'status':4}) for order in draft['orders']: alloc=[x for x in result['allocations'] if x['sales_id']==order['sales_id']] update('mes_wm_product_sales',order['sales_id'],{'status':2}) for a in alloc: qty=D(a['quantity']) q.execute('SELECT * FROM mes_wm_material_stock WHERE id=%s FOR UPDATE',(a['stock_id'],));stock=q.fetchone() assert stock['quantity']-D(stock['frozen_quantity'] or 0)-D(stock['reserved_quantity'] or 0)>=qty a['reserve_id']=insert('mes_wm_stock_reserve',{'material_stock_id':stock['id'],'biz_type':1,'biz_id':order['sales_id'],'biz_code':order['sales_code'],'biz_line_id':a['sales_line_id'],'item_id':a['item_id'],'quantity':qty,'status':1,'remark':note}) q.execute('UPDATE mes_wm_material_stock SET reserved_quantity=COALESCE(reserved_quantity,0)+%s,updater=%s,update_time=%s WHERE id=%s',(qty,'244',now,stock['id'])) update('mes_wm_product_sales',order['sales_id'],{'status':10});update('erp_sale_order',order['order_id'],{'out_status':2}) ecommerce=order['customer_id'] in {1609,1381} carrier='京东' if ecommerce else '自有车辆配送(车牌前缀:青H)' tracking=('JDV20261013'+order['no']) if ecommerce else ('ZPSD20261013'+order['no']) shipment_note=note+('京东运单号为按用户要求模拟生成,非真实物流凭证。' if ecommerce else '开车送货,车牌只提供青H前缀;配送编号为内部补录编号。') update('mes_wm_product_sales',order['sales_id'],{'name':'销售订单['+order['no']+']出库单','status':3,'carrier':carrier,'shipping_number':tracking,'remark':shipment_note}) for a in alloc: qty=D(a['quantity']);stock=selected[a['item_id']] update('mes_wm_stock_reserve',a['reserve_id'],{'status':3}) q.execute('UPDATE mes_wm_material_stock SET reserved_quantity=reserved_quantity-%s,quantity=quantity-%s,updater=%s,update_time=%s WHERE id=%s AND quantity>=%s AND reserved_quantity>=%s',(qty,qty,'244',now,stock['id'],qty,qty));assert q.rowcount==1 a['out_transaction_id']=insert('mes_wm_transaction',{'type':2,'biz_type':119,'biz_id':order['sales_id'],'biz_code':order['sales_code'],'biz_line_id':a['sales_line_id'],'material_stock_id':stock['id'],'item_id':a['item_id'],'quantity':-qty,'batch_id':stock['batch_id'],'batch_code':stock['batch_code'],'warehouse_id':stock['warehouse_id'],'location_id':stock['location_id'],'area_id':stock['area_id'],'transaction_time':now,'receipt_time':stock['receipt_time']}) update('mes_wm_sales_notice_line',a['notice_line_id'],{'out_count':qty}) update('erp_sale_order_items',a['order_item_id'],{'out_count':qty}) update('mes_wm_product_sales',order['sales_id'],{'status':4}) update('mes_wm_sales_notice',order['notice_id'],{'status':4,'out_status':2,'remark':shipment_note}) update('erp_sale_order',order['order_id'],{'out_status':3,'out_count':sum(D(a['quantity']) for a in alloc)}) result['orders'].append({**{k:v for k,v in order.items() if k!='lines'},'carrier':carrier,'shipping_number':tracking,'jd':ecommerce,'status':4}) # Verify before commit: no financial changes, no QC/master changes, exact stock conservation. for t in ['mdm_item','mes_wm_warehouse','mes_wm_warehouse_location','mes_wm_warehouse_area','mes_qc_oqc','crm_contract','crm_contract_product','crm_sale_quotation','crm_sale_quotation_product','crm_invoice','crm_receivable','crm_receivable_plan','crm_customer','crm_permission']: q.execute('SELECT * FROM `'+t+'` WHERE deleted=0 ORDER BY id');assert q.fetchall()==backup['tables'][t],('Protected data changed',t) q.execute('SELECT * FROM mes_wm_material_stock WHERE deleted=0 ORDER BY id');after={r['id']:r for r in q.fetchall()} for old in backup['tables']['mes_wm_material_stock']: diffs={k for k in old if old[k]!=after[old['id']][k]};assert diffs<={'updater','update_time'},('Stock changed',old['id'],diffs) for sid in result['new_stock_ids']:assert after[sid]['quantity']==0 and after[sid]['reserved_quantity']==0 assert len(result['orders'])==23 and len(result['allocations'])==75 and sum(x['jd'] for x in result['orders'])==10 c.commit();result.update(committed=True,at=str(now),original_stock_rows_unchanged=len(backup['tables']['mes_wm_material_stock']),counts={'completed_orders':23,'jd_orders':10,'self_drive_orders':13,'receipt_documents':1,'receipt_lines':75,'outbound_lines':75,'transactions':150,'reserves_closed':75}) (P/'shipping-complete-result.json').write_text(json.dumps(result,ensure_ascii=False,default=str,indent=2),encoding='utf8') print(json.dumps({k:v for k,v in result.items() if k not in ['orders','allocations','unit_basis']},ensure_ascii=False)) except Exception:c.rollback();raise finally: with c.cursor() as q:q.execute('SELECT RELEASE_LOCK(%s)',('sales-ledger-202609',)) c.close()