"""Frozen name mapping, optional customer, validated draft OQC import only. Default: local review; --apply: locked transaction and committed verification. Password is read from stdin, never written to audit files. """ import sys, json, csv, collections, datetime from decimal import Decimal from pathlib import Path P = Path(__file__).parent sys.path.insert(0, str(P.parent / 'iqc-import-20261012/deps')) import pymysql rows = [x for x in json.loads((P/'mapping-plan.json').read_text(encoding='utf8')) if x['kind']=='oqc'] snapshot = json.loads((P/'remote-snapshot.json').read_text(encoding='utf8')) saved = json.loads((P/'random-selections.json').read_text(encoding='utf8')) assert len(rows)==496 def build(s): templates = {t['id']:t for t in s['mes_qc_template']} items = {t['code']:t for t in s['mdm_item']} users = {t['nickname'].strip():t['id'] for t in s['system_users'] if t['status']==0} results = {t['label']:int(t['value']) for t in s['system_dict_data'] if t['dict_type']=='mes_qc_check_result' and t['status']==0} bindings = {(t['template_id'],t['item_id']) for t in s['mes_qc_template_item']} indicators = collections.defaultdict(list) for t in s['mes_qc_template_indicator']: indicators[t['template_id']].append(t) codes = collections.Counter(r['source']['检验单编号'] for r in rows) review = [] for x in rows: r=x['source']; tid=x['mapping']['selected_id']; issues=[] assert saved['oqc|'+r['检验单名称']]['selected_id']==tid t=templates.get(tid); item=items.get(r['产品物料编码']); code=r['检验单编号'] if not code:issues.append('检验单编号为空') elif codes[code]>1:issues.append('原表检验单编号重复,未擅自改号') if not item:issues.append('物料编码为空或系统不存在') elif item['status']!=0:issues.append('物料已停用') if not t:issues.append('无已保存的名称匹配方案') elif t['status']!=0:issues.append('质检方案停用,暂缓导入') elif not indicators[tid]:issues.append('质检方案缺少检验指标') if t and item and (tid,item['id']) not in bindings:issues.append('产品未绑定所选方案') quantities={} for key,col in [('out_quantity','本次出货数量'),('check_quantity','检测数量'),('qualified_quantity','合格品数量'),('unqualified_quantity','不合格品数量')]: value=r[col] if value is None:issues.append(col+'为空');quantities[key]=None else: try: value=Decimal(str(value));assert value.is_finite() and value>=0 quantities[key]=value except Exception:issues.append(col+'非合法非负数');quantities[key]=None if all(quantities[k] is not None for k in ['check_quantity','qualified_quantity','unqualified_quantity']): if quantities['check_quantity'] != quantities['qualified_quantity']+quantities['unqualified_quantity']:issues.append('合格与不合格数量合计不等于检测数量') dates={} for key,col in [('out_date','出货日期'),('inspect_date','检测日期')]: try:dates[key]=datetime.datetime.fromisoformat(r[col]).strftime('%Y-%m-%d %H:%M:%S') except Exception:issues.append(col+'为空或格式无效');dates[key]=None inspector=users.get((r['检测人员'] or '').strip()) if r['检测人员'] and not inspector:issues.append('检测人员未找到') result=results.get(r['检测结果']) if r['检测结果'] and result is None:issues.append('检测结果未匹配字典') header=dict(code=code,name=r['检验单名称'],template_id=tid,item_id=item['id'] if item else None, client_id=None,batch_code=r['批次号'],inspector_user_id=inspector,check_result=result, status=0,remark=r['备注'],**quantities,**dates) for key,size in [('code',100),('name',100),('batch_code',100),('remark',255)]: if header[key] is not None and len(str(header[key]))>size:issues.append(key+'超出字段长度') review.append(dict(excel_row=x['excel_row'],header=header,template_name=t['name'] if t else None, item_name=item['name'] if item else None,issues=issues)) keys=collections.defaultdict(list) for x in review: if not x['issues']: h=x['header'];keys[(h['item_id'],h['client_id'],h['batch_code'],h['out_date'])].append(x) for group in keys.values(): if len(group)>1: for x in group:x['issues'].append('原表物料、客户、批次、出货日期重复') return review def dump(name,value): (P/name).write_text(json.dumps(value,ensure_ascii=False,default=str,indent=2),encoding='utf8') review=build(snapshot) dump('oqc-import-review.json',review) summary={'source_rows':len(review),'eligible':sum(not x['issues'] for x in review),'pending':sum(bool(x['issues']) for x in review), 'reasons':dict(collections.Counter(i for x in review for i in x['issues']))} print(json.dumps(summary,ensure_ascii=False)) if '--apply' not in sys.argv:sys.exit() 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=10,read_timeout=60) log=[];inserted=[];reused=[];changes=[] try: with c.cursor() as q: q.execute('SELECT DATABASE() db');assert q.fetchone()['db']=='mom-hm' q.execute("SELECT GET_LOCK('oqc-import-20261012',10) ok");assert q.fetchone()['ok']==1 c.begin();live={} for table in ['mes_qc_oqc','mes_qc_oqc_line','mes_qc_iqc','mes_qc_ipqc','mes_qc_ipqc_line','mes_qc_template','mes_qc_template_indicator','mes_qc_template_item','mes_qc_indicator','mdm_item','system_users','system_dict_data']: q.execute('SELECT ENGINE FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=%s',(table,));assert q.fetchone()['ENGINE']=='InnoDB' q.execute('SELECT * FROM '+table+' WHERE deleted=0 FOR UPDATE');live[table]=list(q.fetchall()) dump('before-oqc-import.json',live) current=build(live) assert [(x['excel_row'],x['issues']) for x in current]==[(x['excel_row'],x['issues']) for x in review], 'Live validation changed; regenerate review' original_templates=[dict(t) for t in live['mes_qc_template']] templates={t['id']:t for t in live['mes_qc_template']};inds={t['id']:t for t in live['mes_qc_indicator']} criteria=collections.defaultdict(list) for t in live['mes_qc_template_indicator']:criteria[t['template_id']].append(t) def execute(sql,args): log.append(q.mogrify(sql,args)+';');q.execute(sql,args) for x in current: if x['issues']:continue h=x['header'];tid=h['template_id'] q.execute('SELECT * FROM mes_qc_oqc WHERE code=%s AND deleted=0',(h['code'],));existing=q.fetchall() if existing: assert len(existing)==1 e=existing[0] for key,val in h.items(): actual=e[key] if isinstance(actual,datetime.datetime):actual=actual.strftime('%Y-%m-%d %H:%M:%S') assert actual==val, 'Existing code has different data: '+h['code']+' '+key x['id']=e['id'];x['action']='已存在,复用';reused.append(x);continue q.execute('SELECT id FROM mes_qc_oqc WHERE item_id=%s AND client_id IS NULL AND batch_code <=> %s AND out_date=%s AND deleted=0',(h['item_id'],h['batch_code'],h['out_date'])) if q.fetchone():x['issues'].append('系统已存在同物料、空客户、批次和出货日期的检验单');continue t=templates[tid];types=(t['types'] or '').split(',') if '3' not in types: newtypes=','.join(types+['3']) execute("UPDATE mes_qc_template SET types=%s,updater='1' WHERE id=%s AND status=0 AND deleted=0",(newtypes,tid)) assert q.rowcount==1;changes.append(dict(id=tid,name=t['name'],before=t['types'],after=newtypes));t['types']=newtypes cols=list(h) execute('INSERT INTO mes_qc_oqc (`'+'`,`'.join(cols)+"`,creator,updater) VALUES ("+','.join(['%s']*len(cols))+",'1','1')",tuple(h[k] for k in cols)) oid=q.lastrowid;x['id']=oid;x['action']='新增草稿' for ti in criteria[tid]: indicator=inds.get(ti['indicator_id']) line=dict(oqc_id=oid,indicator_id=ti['indicator_id'],tool=indicator['tool'] if indicator else None, check_method=ti['check_method'],standard_value=ti['standard_value'],unit_measure_id=ti['unit_measure_id'], max_threshold=ti['threshold_max'],min_threshold=ti['threshold_min'],critical_quantity=0,major_quantity=0,minor_quantity=0) lc=list(line) execute('INSERT INTO mes_qc_oqc_line (`'+'`,`'.join(lc)+"`,creator,updater) VALUES ("+','.join(['%s']*len(lc))+",'1','1')",tuple(line[k] for k in lc)) x['lines']=len(criteria[tid]);inserted.append(x) for table in ['mes_qc_iqc','mes_qc_ipqc','mes_qc_ipqc_line']: q.execute('SELECT * FROM '+table+' WHERE deleted=0 ORDER BY id');assert list(q.fetchall())==sorted(live[table],key=lambda x:x['id']) q.execute('SELECT id,status,types FROM mes_qc_template WHERE deleted=0');after={x['id']:x for x in q.fetchall()} changed={x['id']:x for x in changes} for t in original_templates: assert after[t['id']]['status']==t['status'] assert after[t['id']]['types']==(changed[t['id']]['after'] if t['id'] in changed else t['types']) (P/'oqc-import-executed.sql').write_text('START TRANSACTION;\n'+'\n'.join(log)+'\nCOMMIT;\n',encoding='utf8') c.commit() for x in inserted+reused: q.execute('SELECT * FROM mes_qc_oqc WHERE id=%s',(x['id'],));e=q.fetchone() for key,val in x['header'].items(): actual=e[key] if isinstance(actual,datetime.datetime):actual=actual.strftime('%Y-%m-%d %H:%M:%S') assert actual==val,(x['id'],key) q.execute('SELECT COUNT(*) n FROM mes_qc_oqc_line WHERE oqc_id=%s AND deleted=0',(x['id'],));assert q.fetchone()['n']==len(criteria[x['header']['template_id']]) q.execute('SELECT COUNT(*) n FROM mes_qc_oqc WHERE deleted=0');assert q.fetchone()['n']==len(live['mes_qc_oqc'])+len(inserted) result=dict(target='36.213.150.237:10000/mom-hm',source_rows=496,inserted=len(inserted),reused=len(reused),pending=sum(bool(x['issues']) for x in current), inserted_lines=sum(x['lines'] for x in inserted),template_type_changes=changes,customer=None,status='草稿',post_commit_verified=True, reasons=dict(collections.Counter(i for x in current for i in x['issues']))) dump('oqc-import-result.json',result);dump('oqc-import-review.json',current) with (P/'出货检验-录入结果及暂缓清单.csv').open('w',encoding='utf-8-sig',newline='') as f: w=csv.writer(f);w.writerow(['原表行号','检验单编号','检验单名称','系统ID','方案ID','方案名称','物料ID','物料名称','客户ID','处理结果','暂缓原因']) for x in current: h=x['header'];w.writerow([x['excel_row'],h['code'],h['name'],x.get('id'),h['template_id'],x['template_name'],h['item_id'],x['item_name'],'',x.get('action','暂缓导入'),';'.join(x['issues'])]) print(json.dumps(result,ensure_ascii=False)) q.execute("SELECT RELEASE_LOCK('oqc-import-20261012')") except Exception:c.rollback();raise finally:c.close()