import json,csv,re,sys,collections from pathlib import Path from decimal import Decimal,ROUND_HALF_UP import xml.etree.ElementTree as ET P=Path('scripts/bom-import') def load(target): r=ET.parse(P/f'{target}-snapshot.xml') return {tag:[{e.tag:e.text or '' for e in x} for x in r.findall(f'./{tag}/row')] for tag in ['materials','units','bom']} def norm(s):return re.sub(r'\s+','',s or '').replace('(','(').replace(')',')') source=[] for s in json.loads((P/'xls-source.json').read_text(encoding='utf-8')): parent=name=None for n,r in enumerate(s['rows'],1): if n<=2:continue if r[0]:name=str(r[0]).strip();parent=str(r[1]).strip() assert parent and len(r)>=6,(s['sheet'],n,r) source.append(dict(file='电商产品BOM(1).xls',sheet=s['sheet'],row=n,parent_code=parent,parent_name=name,child_code=str(r[2]).strip(),child_name=str(r[3]).strip(),quantity=str(r[4]),unit=str(r[5] or '').strip(),section='')) sections={'红枣单碗':'71500052','鱼胶单碗':'71500051'} for s in json.loads((P/'xlsx-source.json').read_text(encoding='utf-8')): parent_name=s['sheet'];parent_code='';section='';cols={} prior=[] for n,r in enumerate(s['rows'],1): if not any(str(x or '').strip() for x in r):continue if norm(str(r[0]))=='项目号': cols={norm(str(v)):i for i,v in enumerate(r) if v is not None};continue if not str(r[0] or '').isdigit(): section=str(r[0]);candidates=[x['child_code'] for x in prior if x['child_code'].startswith(('715','740'))] if section in sections:parent_code=sections[section] else: assert len(set(candidates))==1,(s['sheet'],section,candidates) parent_code=candidates[0] continue row=dict(file='传渠产品BOMxml(1).xlsx',sheet=s['sheet'],row=n,parent_code=parent_code,parent_name=parent_name if not section else section,child_code=str(r[0]),child_name=str(r[1]).strip(),quantity=str(r[cols['数量']]),unit=str(r[cols['计量单位']] or '').strip() if '计量单位' in cols else '',section=section) source.append(row) if not section:prior.append(row) (P/'source-lines.json').write_text(json.dumps(source,ensure_ascii=False,indent=2),encoding='utf-8') unit_evidence=collections.defaultdict(set) for x in source: if x['unit']:unit_evidence[x['child_code']].add(x['unit']) for target in ['local','remote']: db=load(target);codes=collections.defaultdict(list);names=collections.defaultdict(list);units={x['id']:x for x in db['units']} for x in db['materials']:codes[x['code']].append(x);names[norm(x['name'])].append(x) accepted=[];issues=[];notes=[] for raw in source: x=dict(raw) if not x['parent_code'] and x['file'].endswith('.xlsx'): # Each top-level sheet corresponds to the imported TC product series, in sheet order. sheets=json.loads((P/'xlsx-source.json').read_text(encoding='utf-8')) tc='TC'+str(next(i for i,s in enumerate(sheets,1) if s['sheet']==x['sheet'])).zfill(4) x['parent_code']=tc parents=codes[x['parent_code']] if x['parent_code'] else names[norm(x['parent_name'])];children=codes[x['child_code']] reason=[] if len(parents)!=1:reason.append('父物料未找到或不唯一') if len(children)!=1:reason.append('子物料未找到或不唯一') if reason:issues.append(dict(x,reason=';'.join(reason)));continue p,c=parents[0],children[0];x.update(parent_code=p['code'],parent_name=p['name'],child_name=c['name'],parent_id=int(p['id']),child_id=int(c['id'])) if p['status']!='0' or c['status']!='0':reason.append('物料未启用') if p['id']==c['id']:reason.append('BOM自引用') u=units.get(c['unit_measure_id']);base=u['code'] if u else '' x['system_unit']=base if not x['unit']: if len(unit_evidence[x['child_code']])==1: x['unit']=next(iter(unit_evidence[x['child_code']]));notes.append(dict(x,reason='单位取自同编码其他源表行')) elif '--use-system-unit' in sys.argv: x['unit']=base;notes.append(dict(x,reason='源表缺少单位,经用户确认按系统基本单位录入')) else:reason.append('源表缺少单位且无同编码单位依据') q=Decimal(x['quantity']);factor=Decimal(1) if q<=0:reason.append('用量非正数') if x['unit'] and x['unit']!=base: masses={'KG':Decimal(1000),'GM':Decimal(1),'MG':Decimal('0.001')} if x['unit'] in masses and base in masses:factor=masses[x['unit']]/masses[base] else:reason.append('源表单位与系统单位不一致,缺少换算依据') if reason:issues.append(dict(x,reason=';'.join(reason)));continue exact=q*factor;stored=exact.quantize(Decimal('0.000001'),rounding=ROUND_HALF_UP) if exact!=stored:notes.append(dict(x,reason=f'单位换算后为 {exact} {base},按数据库6位小数保存为 {stored};原始用量保留在备注')) x['system_quantity']=str(stored);x['factor']=str(factor);accepted.append(x) groups=collections.defaultdict(list) for x in accepted:groups[(x['parent_id'],x['child_id'])].append(x) unique=[];duplicates=[] for pair,rows in groups.items(): if len({Decimal(x['system_quantity']) for x in rows})>1: issues.extend(dict(x,reason='同一父子项存在不同用量') for x in rows);continue x=rows[0];x['sources']=[f"{r['file']}/{r['sheet']}/{r['row']}" for r in rows] if len(rows)>1:duplicates.extend(dict(r,reason='相同父子项、用量的重复行,不重复累加') for r in rows[1:]) unique.append(x) existing=collections.defaultdict(list) for x in db['bom']:existing[(int(x['item_id']),int(x['bom_item_id']))].append(x) final=[] for x in unique: old=existing[(x['parent_id'],x['child_id'])] if old and len(old)!=1: issues.append(dict(x,reason='已有BOM有重复记录,保留已有数据'));continue if old and Decimal(old[0]['quantity'])!=Decimal(x['system_quantity']): if '--update-existing' not in sys.argv: issues.append(dict(x,reason='已有BOM用量不同,保留已有数据'));continue x['action']='更新';x['existing_id']=int(old[0]['id']);x['old_quantity']=old[0]['quantity'] else:x['action']='已存在' if old else '新增' x['parent_unit_id']=codes[x['parent_code']][0]['unit_measure_id'];x['child_unit_id']=codes[x['child_code']][0]['unit_measure_id'] final.append(x) graph=collections.defaultdict(set) for x in db['bom']:graph[int(x['item_id'])].add(int(x['bom_item_id'])) for x in final:graph[x['parent_id']].add(x['child_id']) def visit(n,stack,done): if n in stack:raise ValueError(f'BOM环路 {n}') if n in done:return stack.add(n) for child in graph.get(n,set()):visit(child,stack,done) stack.remove(n);done.add(n) done=set() for n in list(graph):visit(n,set(),done) plan=dict(target=target,source_count=len(source),lines=final,issues=issues,duplicates=duplicates,notes=notes) (P/f'{target}-plan.json').write_text(json.dumps(plan,ensure_ascii=False,indent=2),encoding='utf-8') fields=['file','sheet','row','parent_code','parent_name','child_code','child_name','quantity','unit','system_quantity','system_unit','action','reason'] for kind,rows in [('导入清单',final),('待核对',issues),('重复行',duplicates),('单位补全',notes)]: with (P/f'{target}-{kind}.csv').open('w',encoding='utf-8-sig',newline='') as f: w=csv.DictWriter(f,fields,extrasaction='ignore');w.writeheader();w.writerows(rows) print(target,'源行',len(source),'BOM关系',len(final),'父项',len({x['parent_id'] for x in final}),'重复',len(duplicates),'待核对',len(issues),'单位补全',len(notes),collections.Counter(x['reason'] for x in issues))