gaoluyang
18 小时以前 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
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()))