"""Raise the four October supplemental chains to 200, atomically across all modules."""
|
import sys,json,re,collections
|
from pathlib import Path
|
from decimal import Decimal,ROUND_HALF_UP
|
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;apply='--apply' in sys.argv
|
normalize=json.loads((P/'normalize-applied.json').read_text(encoding='utf8'))
|
manifest=json.loads((P/'result-applied.json').read_text(encoding='utf8'))
|
for link in manifest['links']:
|
for k,v in list(link.items()):
|
if isinstance(v,str) and v in normalize['mapping']:link[k]=normalize['mapping'][v]
|
schema=json.loads((P/'schema-snapshot.json').read_text(encoding='utf8'))
|
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,autocommit=False,read_timeout=90)
|
def dump(name,obj):(P/name).write_text(json.dumps(obj,ensure_ascii=False,indent=2,default=str),encoding='utf8')
|
def money(v):return D(v).quantize(D('.01'),rounding=ROUND_HALF_UP)
|
rows={};changes=[];log=[];summaries=[]
|
with c.cursor() as q:
|
try:
|
q.execute('SELECT GET_LOCK(%s,10)',('october-chain-production-floor',));assert list(q.fetchone().values())==[1]
|
for t,ids in normalize['scoped_ids'].items():
|
q.execute('SELECT * FROM `'+t+'` WHERE id IN ('+','.join(['%s']*len(ids))+') FOR UPDATE',tuple(ids));rows[t]=q.fetchall();assert len(rows[t])==len(ids)
|
dump('before-production-floor-'+('apply' if apply else 'preview')+'.json',rows)
|
byid={t:{x['id']:x for x in rr} for t,rr in rows.items()}
|
day_factors={};new_totals={}
|
for link in manifest['links']:
|
mp=byid['mes_pro_mps'][link['mps_id']];wo=byid['mes_pro_work_order'][link['work_order_id']];order=byid['erp_sale_order'][link['sale_order_id']];oi=byid['erp_sale_order_items'][link['order_item_id']]
|
old=mp['quantity'];assert old>0 and mp['quantity_produced']==wo['quantity']==wo['quantity_produced']==oi['count']==oi['out_count']==order['total_count']==order['out_count']==old
|
assert old==D(link['quantity']),('Dataset already changed',link['sale_order_no'],old)
|
target=max(old,D(200));factor=target/old;day_factors[link['date']]=factor
|
net=money(oi['product_price']*target);tax=money(net*oi['tax_percent']/100);total=net+tax
|
new_totals[link['date']]=(net,tax,total)
|
summaries.append({'sales_order':order['no'],'product':link['product'],'unit_id':link['unit_id'],'old_quantity':old,'new_quantity':target,'old_total':order['total_price'],'new_total':total})
|
link['quantity']=target;link['total']=total
|
# Scale only transaction quantities, never prices, percentages, sample readings or template limits.
|
quantity_tables={'crm_business_product','crm_sale_quotation_product','crm_contract_product','erp_sale_order','erp_sale_order_items','mes_pro_mps','mes_pro_work_order','mes_pro_work_order_bom','mes_pro_task','mes_pro_feedback','mes_pro_task_issue','mes_wm_arrival_notice_line','mes_qc_iqc','mes_qc_ipqc','mes_qc_oqc','mes_wm_item_receipt_line','mes_wm_item_receipt_detail','mes_wm_product_issue_line','mes_wm_product_issue_detail','mes_wm_item_consume_line','mes_wm_item_consume_detail','mes_wm_product_produce_line','mes_wm_product_produce_detail','mes_wm_product_receipt_line','mes_wm_product_receipt_detail','mes_wm_sales_notice_line','mes_wm_product_sales_line','mes_wm_product_sales_detail','mes_wm_stock_reserve','mes_wm_transaction','mes_wm_material_stock'}
|
for t,rr in rows.items():
|
decimals={x['Field']:int(re.search(r'decimal\(\d+,(\d+)\)',x['Type']).group(1)) for x in schema[t]['columns'] if x['Type'].startswith('decimal')}
|
for row in rr:
|
day=row['create_time'].date().isoformat();factor=day_factors.get(day,D(1));v={}
|
if factor==1:continue
|
if t in quantity_tables:
|
for k,old in row.items():
|
isquantity=k=='quantity' or k.endswith('_quantity') or k in ['count','total_count','out_count','quantity_produced','quantity_scheduled','quantity_changed']
|
if not isquantity or k.startswith(('min_','max_')) or old is None or old==0:continue
|
assert isinstance(old,(D,int)),(t,k)
|
value=D(old)*factor
|
value=value.quantize(D(1).scaleb(-decimals[k]),rounding=ROUND_HALF_UP) if k in decimals else int(value)
|
if value!=old:v[k]=value
|
net,tax,total=new_totals[day]
|
if t=='crm_business':v.update(total_product_price=net,total_price=total)
|
elif t=='crm_sale_quotation':v.update(total_product_price=net,tax_price=tax,total_price=total)
|
elif t=='crm_contract':v.update(total_product_price=total,total_price=total)
|
elif t=='erp_sale_order':v.update(total_product_price=net,total_tax_price=tax,total_price=total)
|
elif t in ['crm_business_product','crm_sale_quotation_product','erp_sale_order_items']:
|
v['total_price']=net
|
if 'tax_price' in row:v['tax_price']=tax
|
elif t=='crm_contract_product':
|
v['total_price']=total;assert money(row['contract_price']*v['count'])==total
|
elif t=='crm_receivable_plan':
|
assert row['receivable_id'] is None and row['period'] in [1,2]
|
first=money(total*D('.3'));v['price']=first if row['period']==1 else total-first
|
if not v:continue
|
sql='UPDATE `'+t+'` SET '+','.join('`'+k+'`=%s' for k in v)+',update_time=update_time WHERE id=%s'
|
args=tuple(v.values())+(row['id'],);q.execute(sql,args);assert q.rowcount==1
|
log.append(q.mogrify(sql,args)+';');changes.append({'table':t,'id':row['id'],'before':{k:row[k] for k in v},'after':v});byid[t][row['id']]={**row,**v}
|
for link in manifest['links']:
|
target=link['quantity'];day=link['date'];net,tax,total=new_totals[day]
|
for t,key,field in [('mes_pro_mps','mps_id','quantity_produced'),('mes_pro_work_order','work_order_id','quantity_produced'),('erp_sale_order','sale_order_id','out_count'),('erp_sale_order_items','order_item_id','out_count')]:assert byid[t][link[key]][field]==target
|
for tid in link['tasks']:
|
task=byid['mes_pro_task'][tid];assert task['quantity']==task['produced_quantity']==task['qualify_quantity']==target
|
for fid in link['feedbacks']:
|
fb=byid['mes_pro_feedback'][fid];assert fb['scheduled_quantity']==fb['feedback_quantity']==fb['qualified_quantity']==target
|
assert sum(byid['crm_receivable_plan'][pid]['price'] for pid in link['receivable_plans'])==total
|
assert byid['mes_qc_oqc'][link['oqc_id']]['out_quantity']==byid['mes_qc_oqc'][link['oqc_id']]['qualified_quantity']==target
|
balances=collections.defaultdict(lambda:D(0))
|
for row in byid['mes_wm_transaction'].values():balances[row['material_stock_id']]+=row['quantity']
|
assert len(balances)==62 and all(x==0 for x in balances.values()),'Inventory flows must balance'
|
for row in byid['mes_wm_material_stock'].values():assert row['quantity']==row['reserved_quantity']==0
|
# All fields outside the explicit change set must remain unchanged.
|
expected_changes={(x['table'],x['id']):x['after'] for x in changes}
|
for t,rr in rows.items():
|
for row in rr:
|
q.execute('SELECT * FROM `'+t+'` WHERE id=%s',(row['id'],));assert q.fetchone()=={**row,**expected_changes.get((t,row['id']),{})},(t,row['id'])
|
result={'applied':apply,'minimum_quantity':200,'chains':summaries,'changed_rows':len(changes),'changes':changes,'inventory_balanced':True,'unit_prices_unchanged':True}
|
dump('production-floor-'+('applied' if apply else 'preview')+'.json',result)
|
(P/('production-floor-applied.sql' if apply else 'production-floor-preview.sql')).write_text('\n'.join(log),encoding='utf8')
|
if apply:
|
c.commit();dump('result-current.json',manifest)
|
else:c.rollback()
|
print(json.dumps({k:result[k] for k in ['applied','minimum_quantity','chains','changed_rows','inventory_balanced','unit_prices_unchanged']},ensure_ascii=False,default=str))
|
except:
|
c.rollback();raise
|
finally:
|
q.execute('SELECT RELEASE_LOCK(%s)',('october-chain-production-floor',))
|
c.close()
|