import json,hashlib,collections,csv,re,unicodedata
|
from pathlib import Path
|
from decimal import Decimal,ROUND_HALF_UP
|
import xml.etree.ElementTree as ET
|
P=Path('scripts/quotation-import')
|
source=json.loads((P/'source.json').read_text(encoding='utf-8'))
|
db=ET.parse(P/'local-snapshot.xml')
|
materials={x.findtext('code'):{e.tag:e.text or '' for e in x} for x in db.findall('./materials/row')}
|
units={x.findtext('name').strip():{e.tag:e.text for e in x} for x in db.findall('./units/row')}
|
# Explicit correspondence of source product + package specification to the TC product series.
|
mapping=[
|
('即食小罐洞燕窝','840克(30克×28)','TC0025'),
|
('即食小罐洞燕窝','360克(30克×12)','TC0024'),
|
('洞燕窝即食碗装燕窝','1.2kg(100g*12碗/盒)','TC0035'),
|
('即食碗装洞燕窝(怡碗燕)','720g(90g×8)','TC0037'),
|
('即食小罐洞燕窝','210克(30克×7)','TC0022'),
|
('即食小罐洞燕窝(尊享款)','480克(30克×16)','TC0027'),
|
('黑果枸杞','90g','TC0016'),
|
('即食小罐洞燕窝(尊享款)','600g(30g*20瓶/盒)','TC0026'),
|
('洞燕窝即食碗装燕窝','1.5千克(150克×10)','TC0032'),
|
('枸杞原浆罐头','1.44L(60ml*24瓶/盒)','TC0009'),
|
('即食碗装红枣枸杞洞燕窝(自热型)','600克(100克/碗*6)','TC0030'),
|
('有机黑果枸杞','80克(4克×20)','TC0003'),
|
('黑果枸杞','60g','TC0017'),
|
('黑果枸杞原浆罐头','1.44L(60ml*24瓶/盒)','TC0007'),
|
('沙棘原浆罐头','1.44L(60ml*24瓶/盒)','TC0011'),
|
('黑果枸杞袋泡茶','60g(2g/袋*30)','TC0006'),
|
('总统牌洞燕盏即食燕窝罐头','110g','TC0029'),
|
('即食碗装鱼胶洞燕窝(自热型)','600克(100克/碗*6)','TC0031'),
|
('有机黑果枸杞','120克(4克×30)','TC0004'),
|
('黑果枸杞','180克(90克×2)','TC0005'),
|
('有机黑果枸杞','48克(4克×12)','TC0002'),
|
('洞燕窝即食碗装燕窝','600g(100g*6碗/盒)','TC0036'),
|
('枸杞原浆罐头','360ml(60ml*6瓶/盒)','TC0010'),
|
('沙棘原浆罐头','360ml(60ml*6瓶/盒)','TC0012'),
|
('黑果枸杞原浆罐头','360ml(60ml*6瓶/盒)','TC0008'),
|
('即食碗装洞燕窝(自热型)','600g(100g*6)','TC0034'),
|
('冻干枸杞','200g/盒(8g/袋×25)','TC0001'),
|
('洞燕窝即食碗装燕窝','900克(150克*6)','TC0033'),
|
('枸杞原浆罐头','900ml(30ml/袋×30)','TC0014'),
|
('黑果枸杞玫瑰果膏','900ml(30ml/袋×30)','TC0015'),
|
('即食藜麦粉R型','450克(30克×15)','TC0013'),
|
('黑果枸杞','80克(4g/罐×20)','TC0019'),
|
]
|
mapped={(a,b):c for a,b,c in mapping}
|
newkeys={('鲜冬虫夏草','4A级 (1.01-1.3g)/ 根*9根'),('雪菊','50g'),('黑果枸杞玫瑰果膏-30ml/袋','30ml/袋'),('黑果枸杞','120克(4g/罐×30)')}
|
rows=[];newproducts={};productmap={};quotes=[]
|
canonical_customers={}
|
assert len(source['quotes'])==345 and sum(len(q['items']) for q in source['quotes'])==1702
|
assert source['excluded_sheets']==['报价单汇总总表','编制说明']
|
assert len({q['no'] for q in source['quotes']})==345
|
for q in source['quotes']:
|
assert q['tax'] is None and q['contact'] is None
|
out=dict(q);out['name']=q['customer']+' 2026年8-10月产品报价单';out['items']=[]
|
customer_key=unicodedata.normalize('NFKC',q['customer']).casefold().strip()
|
out['source_customer']=q['customer'];out['customer']=canonical_customers.setdefault(customer_key,q['customer'])
|
assert len(out['name'])<=128 and len(q['no'])<=32 and len(q['customer'])<=255
|
total=Decimal(0);count=Decimal(0)
|
for x in q['items']:
|
key=(x['name'],x['specification']);unit=units[x['unit']]
|
if key in mapped:
|
code=mapped[key];m=materials[code];assert m['status']=='0'
|
itemid=int(m['id']);itemname=m['name'];expected_base_unit=int(m['unit_measure_id'])
|
else:
|
assert key in newkeys,key
|
code='SQ20261012-'+hashlib.sha256('|'.join(key).encode()).hexdigest()[:10].upper()
|
itemid=0;itemname=x['name'];expected_base_unit=int(unit['id'])
|
newproducts[code]=dict(code=code,name=x['name'],specification=x['specification'],unit_id=int(unit['id']),unit_name=x['unit'],item_type=2)
|
vals=[Decimal(x[k]) for k in ['original_price','price','count','total']]
|
assert vals[0]>=0 and vals[1]>=0 and vals[2]>0 and vals[3]>=0
|
assert all(v==v.quantize(Decimal('0.000001')) for v in vals)
|
calculated=(vals[1]*vals[2]).quantize(Decimal('.01'),rounding=ROUND_HALF_UP)
|
remark=f"源表第{x['row']}行;{x['name']};规格{x['specification']};单位{x['unit']}"
|
if calculated!=vals[3]:remark+=f";原表均价已四舍五入,单价×数量={calculated},原表小计={vals[3]},按原表小计导入"
|
assert len(remark)<=256
|
y=dict(x,item_code=code,item_id=itemid,item_name=itemname,item_unit_id=int(unit['id']),expected_base_unit=expected_base_unit,remark=remark)
|
out['items'].append(y);total+=vals[3];count+=vals[2]
|
rows.append(dict(no=q['no'],customer=q['customer'],**y))
|
productmap[key]=dict(name=x['name'],specification=x['specification'],source_unit=x['unit'],item_code=code,item_name=itemname,action='补建销售产品' if not itemid else '使用已有产品')
|
foot=[f for f in q['footers'] if f['values'][0]=='合计'];assert len(foot)==1
|
assert Decimal(str(foot[0]['values'][9]))==total,(q['no'],total,foot)
|
assert Decimal(str(foot[0]['values'][8]))==count,(q['no'],count,foot)
|
out['total']=str(total)
|
out['remark']=q['remark']+f" 来源:销售报价单_2026.8.1-10.11.xlsx/{q['sheet']};原负责人:朱莉;原表客户:{q['customer']};原表商机名称:{q['business']};未填税率,保持空白。"
|
if any(Decimal(x['price'])*Decimal(x['count'])!=Decimal(x['total']) for x in q['items']):out['remark']+=' 原表报价为四舍五入的成交均价,金额按原表明细小计保存。'
|
assert len(out['remark'])<=1024
|
quotes.append(out)
|
assert sum(Decimal(q['total']) for q in quotes)==Decimal('15392805.71')
|
plan=dict(owner_username='zhuli',owner_nickname='朱莉',status=0,source_sha256=source['sha256'],quotes=quotes,new_products=list(newproducts.values()),total='15392805.71')
|
(P/'local-plan.json').write_text(json.dumps(plan,ensure_ascii=False,indent=2),encoding='utf-8')
|
for filename,data in [('报价明细清单',rows),('产品匹配清单',list(productmap.values()))]:
|
fields=list(data[0])
|
with (P/f'{filename}.csv').open('w',encoding='utf-8-sig',newline='') as f:
|
w=csv.DictWriter(f,fields);w.writeheader();w.writerows(data)
|
print('Prepared',len(quotes),'quotations,',len(rows),'lines,',len(productmap),'products, new products=',list(newproducts.values()))
|