gaoluyang
14 小时以前 e40a608ef9bf51db294d8eefada6a8e2e80cebca
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
"""Normalize only the isolated October supplement; retain source and measurement provenance."""
import sys,json,re,collections
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
sys.path.insert(0,str(Path('scripts/iqc-import-20261012/deps').resolve()))
import pymysql
P=Path(__file__).parent;TAG='OCT0814-DEMO';NOTE='10月8日至14日业务链演示补充'
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,autocommit=False,read_timeout=90)
schema=json.loads((P/'schema-snapshot.json').read_text(encoding='utf8'))
rules=json.loads((P/'number-rules.json').read_text(encoding='utf8'))
manifest=json.loads((P/'result-applied.json').read_text(encoding='utf8'))
def save(name,obj): (P/name).write_text(json.dumps(obj,ensure_ascii=False,indent=2,default=str),encoding='utf8')
def canon(obj):return json.loads(json.dumps(obj,default=str,ensure_ascii=False))
rows={};mapping={};reservations=[];changes=[];log=[]
rule_ids={'mes_wm_arrival_notice':13,'mes_wm_item_receipt':14,'mes_wm_batch':19,'mes_wm_product_sales':20,'mes_wm_sales_notice':28,'mes_pro_work_order':30,'mes_pro_task':31,'mes_pro_feedback':33,'mes_qc_iqc':40,'mes_qc_ipqc':41,'mes_qc_oqc':42,'mes_qc_indicator_result':45,'mes_wm_product_issue':64,'mes_pro_mps':65,'mes_qc_template':67}
q=c.cursor()
try:
 q.execute('SELECT GET_LOCK(%s,10)',(TAG+'-normalize',));assert list(q.fetchone().values())==[1]
 for t,s in schema.items():
  if not isinstance(s,dict) or 'columns' not in s:continue
  cols=[x['Field'] for x in s['columns'] if re.match(r'(?:var)?char|text',x['Type'])]
  if not cols:continue
  clause=' OR '.join('`'+k+'` LIKE %s' for k in cols)
  q.execute('SELECT * FROM `'+t+'` WHERE '+clause,tuple('%'+TAG+'%' for _ in cols));found={x['id']:x for x in q.fetchall()}
  if 'remark' in cols:
   q.execute('SELECT * FROM `'+t+'` WHERE remark LIKE %s',(NOTE+'%',));found.update({x['id']:x for x in q.fetchall()})
  if found:rows[t]=list(found.values())
 assert len(rows['erp_sale_order'])==4 and len(rows['mes_wm_batch'])==42
 save('before-normalize-'+('apply' if apply else 'dry-run')+'.json',rows)
 q.execute('SELECT id,name FROM mdm_item');items={x['id']:x['name'] for x in q.fetchall()}
 q.execute('SELECT id,name FROM mes_pro_process');processes={x['id']:x['name'] for x in q.fetchall()}
 q.execute('SELECT id,name FROM crm_customer');customers={x['id']:x['name'] for x in q.fetchall()}
 def occupied(t,col,prefix):
  q.execute('SELECT `'+col+'` v FROM `'+t+'` WHERE `'+col+'` LIKE %s',(prefix+'%',));return [x['v'] for x in q.fetchall()]
 def next_code(t,col,prefix,width,start=1,extra=()):
  vals=occupied(t,col,prefix)+list(extra)+list(mapping.values())
  nums=[int(v[len(prefix):]) for v in vals if v and v.startswith(prefix) and v[len(prefix):].isdigit()]
  n=max(nums+[start-1])+1;assert n<10**width
  return prefix+str(n).zfill(width),n
 # Marketing records follow the numbering conventions already used by the business data.
 for t,col,prefix,width in [('erp_sale_order','no','26',6),('crm_sale_quotation','no','BJ-202610-L',3),('crm_contract','no','HT-202610-L',3)]:
  for row in rows[t]:
   new,n=next_code(t,col,prefix,width);mapping[row[col]]=new
 for t,rid in rule_ids.items():
  rule=next(x for x in rules['mes_md_auto_code_rule'] if x['id']==rid)
  parts=sorted([x for x in rules['mes_md_auto_code_part'] if x['rule_id']==rid],key=lambda x:x['sort'])
  for row in rows[t]:
   day=row['create_time'].strftime('%Y%m%d');prefix='';width=None;start=1
   for part in parts:
    if part['type']==3:prefix+=part['fix_character'][:part['length']]
    elif part['type']==2:prefix+=day
    elif part['type']==4:width=part['length'];start=part['serial_start_no']
    else:raise ValueError((t,part))
   q.execute('SELECT result FROM mes_md_auto_code_record WHERE rule_id=%s',(rid,));used=[x['result'] for x in q.fetchall()]
   # IR_ is shared by receipts and indicator results: avoid cross-module string ambiguity.
   if prefix.startswith('IR_'):used+=occupied('mes_wm_item_receipt','code',prefix)+occupied('mes_qc_indicator_result','code',prefix)
   new,n=next_code(t,'code',prefix,width,start,used)
   if len(new)<rule['max_length']:new=new.rjust(rule['max_length'],rule['padded_char'])
   mapping[row['code']]=new
   reservations.append({'rule_id':rid,'result':new,'serial_no':n,'input_char':None,'creator':row['creator'],'updater':row['updater'],'create_time':row['create_time'],'update_time':row['create_time'],'deleted':0})
 for t,prefix,width in [('mes_wm_item_consume','LLD',4),('mes_wm_product_produce','RKD',4),('mes_wm_product_receipt','PR_',3)]:
  for row in rows[t]:
   new,n=next_code(t,'code',prefix+row['create_time'].strftime('%Y%m%d'),width);mapping[row['code']]=new
 for row in rows.get('mes_pro_work_order_param_record',[]):mapping[row['record_no']]='TASK-'+str(row['task_id'])
 for row in rows['mes_wm_product_sales']:mapping[row['shipping_number']]=mapping[row['code']]
 batch_codes={x['id']:mapping[x['code']] for x in rows['mes_wm_batch']}
 for t,rr in rows.items():
  for row in rr:
   v={}
   for k,old in row.items():
    if isinstance(old,str) and old in mapping:v[k]=mapping[old]
   if 'lot_number' in row and isinstance(row['lot_number'],str) and TAG in row['lot_number']:
    bid=row['id'] if t=='mes_wm_batch' else row.get('batch_id');v['lot_number']=batch_codes[bid]
   if 'name' in row and isinstance(row['name'],str) and '演示' in row['name']:
    item=items.get(row.get('item_id') or row.get('product_id'),'');proc=processes.get(row.get('process_id'),'')
    if t.startswith('crm_'):
     link=next(x for x in manifest['links'] if row['id']==x[{'crm_business':'business','crm_sale_quotation':'quotation_id','crm_contract':'contract_id'}[t]])
     suffix={'crm_business':'销售商机','crm_sale_quotation':'销售报价','crm_contract':'销售合同'}[t];name=link['product']+suffix
    elif t=='mes_qc_template':name={'OCT0814-DEMO-IQC':'来料外观及数量核验方案','OCT0814-DEMO-IPQC':'燕窝过程检验方案','OCT0814-DEMO-OQC':'燕窝出货检验方案'}[row['code']]
    elif t.startswith('mes_qc_'):name=item+{'mes_qc_iqc':'来料检验','mes_qc_ipqc':'过程检验','mes_qc_oqc':'出货检验'}[t]
    elif t=='mes_pro_task':name=item+'-'+proc
    elif t=='mes_pro_work_order':name=item+'生产工单'
    else:name=row['name'].replace('演示','').replace('工序投入消耗',proc+'工序投料').replace('工序产出',proc+'工序产出')
    assert name and '演示' not in name;v['name']=name
   if t=='mes_wm_product_sales':v['carrier']='自有车辆配送'
   if not v:continue
   # Only named fields and exact code references may change; quantities, money and audit times stay intact.
   assert not set(v)&{'remark','content','value','param_value','sampling_location','check_method','status','quantity','total_price'}
   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})
 for v in reservations:
  cols=list(v);sql='INSERT INTO mes_md_auto_code_record (`'+'`,`'.join(cols)+'`) VALUES ('+','.join(['%s']*len(cols))+')'
  q.execute(sql,tuple(v.values()));log.append(q.mogrify(sql,tuple(v.values()))+';')
 # Re-read every scoped row and prove that no unrelated field changed.
 bykey={(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'],));actual=q.fetchone();expected={**row,**bykey.get((t,row['id']),{})};assert actual==expected,(t,row['id'])
   for k,value in actual.items():
    if isinstance(value,str) and (k in ['code','no','record_no','lot_number','shipping_number'] or k.endswith('_code') or k.endswith('_no')):assert TAG not in value,(t,k,value)
   if isinstance(actual.get('name'),str):assert '演示' not in actual['name'],(t,actual['id'])
 summary={'applied':apply,'changed_rows':len(changes),'code_count':len(mapping),'number_reservations':len(reservations),'changes':changes,'mapping':mapping,'scoped_ids':{t:[x['id'] for x in rr] for t,rr in rows.items()}}
 save('normalize-'+('applied' if apply else 'preview')+'.json',summary)
 (P/('normalize-applied.sql' if apply else 'normalize-preview.sql')).write_text('\n'.join(log),encoding='utf8')
 if apply:c.commit()
 else:c.rollback()
 print(json.dumps({k:summary[k] for k in ['applied','changed_rows','code_count','number_reservations']},ensure_ascii=False))
except:
 c.rollback();raise
finally:
 q.execute('SELECT RELEASE_LOCK(%s)',(TAG+'-normalize',));c.close()