gaoluyang
11 小时以前 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
import json,csv,collections,xml.etree.ElementTree as ET
from pathlib import Path
from decimal import Decimal,ROUND_HALF_UP
root=Path('scripts/stock-import-202609')
data=json.loads((root/'source.json').read_text(encoding='utf-8'),parse_float=Decimal)
plan=json.loads((root/'remote-plan.json').read_text(encoding='utf-8'))
db=ET.parse(root/'remote-snapshot.xml').getroot()
materials={m.findtext('code'):{c.tag:c.text for c in m} for m in db.find('materials')}
units={u.findtext('name').strip():int(u.findtext('id')) for u in db.find('units')}
unitcodes={u.findtext('code'):u.findtext('name').strip() for u in db.find('units')}
ledger=next(s for s in data if s['sheet']=='Sheet1')['rows']
detail=collections.defaultdict(list)
for r in next(s for s in data if s['sheet']=='整理表')['rows'][1:]:
    if len(r)>5 and isinstance(r[5],(int,Decimal)):detail[str(r[0]).strip()].append(r)
warehouses=[
 ('CPK01','成品仓','销售成品及完成包装的产品,按批次和效期管理;对应原表CPK01'),
 ('YLK','原料仓','生产原辅料、食品添加剂及散装原料;对应原表YLK'),
 ('NBCK','内包材仓','直接接触产品的内袋、瓶、盖、膜等包材;对应原表NBCK'),
 ('WBCK','外包材仓','外盒、外箱、标签、手提袋等外包装;对应原表WBCK'),
 ('CCCP','虫草冷链仓','鲜草、冻干草及虫草散料专用;对应原表CCCP;实际温湿度条件按品种储存标准执行'),
 ('BCPK','半成品仓','待继续加工或包装的中间产品;对应原表BCPK'),
 ('QC_PENDING','待检仓','到货待检、复检及质量待确认物料;合格后办理移库'),
 ('THK','退货及不合格品仓','退货、拒收及不合格品隔离存放;对应原表THK,检验处置后再移库'),
 ('WIP_VIRTUAL_WAREHOUSE','虚拟线边仓','已发至车间但尚未消耗的在制库存;对应原表CCCJ、YFK,生产领料与实际消耗分开记账')]
mapping={k:k for k,_,_ in warehouses};mapping.update(CCCJ='WIP_VIRTUAL_WAREHOUSE',YFK='WIP_VIRTUAL_WAREHOUSE')
def fallback(r):
    major=str(ledger[r['row']-1][2])
    if major=='8':
        name=r['name']
        return 'NBCK' if any(k in name for k in ('复合膜','卷膜','玻璃瓶','瓶盖','内袋','铝箔袋','封口膜','垫片','光片')) else 'WBCK'
    if major in ('6','7'):return 'CCCP' if any(k in r['name'] for k in ('虫草','鲜草','断草','肉节')) else 'YLK'
    if major in ('1','2','Q','U','T'):return 'CPK01'
    raise ValueError(('Unclassified',r))
def fmt(v):return format(Decimal(v).quantize(Decimal('.000001'),rounding=ROUND_HALF_UP),'f')
out=[];allocation=[]
for r in plan:
    m=materials.get(r['code']);quantity=Decimal(r['quantity']);srcunit=r['unit'];note=''
    if r['code']=='81013938':srcunit='克';note='台账单位个与整理表、包材盘点表GM冲突,按原表GM及系统克计量'
    targetunit=unitcodes.get(next((u.findtext('code') for u in db.find('units') if m and u.findtext('id')==m['unit_measure_id']),''),srcunit) if m else srcunit
    factor=Decimal(1)
    if srcunit!=targetunit:
        if srcunit=='公斤' and targetunit=='克':factor=Decimal(1000)
        elif srcunit=='克' and targetunit=='公斤':factor=Decimal('.001')
        else:
            assert srcunit in ('盒','套','罐','袋','个') and targetunit in ('个','张'),(r,targetunit)
            note+=';按同编码包装成品/包材的计数单位1:1录入'
    if r['code'] in ('Q2100196','Q2100197'):note+=';用户确认按原编码2992个,原表900ml散货袋装,与系统规格不一致'
    final=Decimal(fmt(quantity*factor));assert final>=0
    groups=collections.defaultdict(Decimal)
    sourceqty=Decimal(0)
    for dr in detail[r['code']]:
        loc=str(dr[4]).strip();qty=Decimal(dr[5])
        # Allocation uses source quantities as proportions, preserving ledger target quantity.
        sourceqty+=qty
        if not loc:loc=fallback(r)
        assert loc in mapping,(r['code'],loc)
        groups[mapping[loc]]+=qty
    if not groups:groups[fallback(r)]=Decimal(0)
    if any(v<0 for v in groups.values()):
        assert final==0,(r,groups)
        groups={max(groups,key=groups.get):Decimal(0)}
    positive={k:v for k,v in groups.items() if v>0}
    if final==0:
        groups={max(groups,key=groups.get):Decimal(0)}
    else:
        assert positive,(r,groups)
        groups=positive
    allocations={}
    if len(groups)==1:allocations[next(iter(groups))]=final
    else:
        weight=sum(groups.values());assert weight>0
        allocations={k:Decimal(fmt(final*v/weight)) for k,v in groups.items()}
        allocations[max(groups,key=groups.get)]+=final-sum(allocations.values())
    assert sum(allocations.values())==final
    row=dict(r,target_unit=targetunit,unit_id=int(m['unit_measure_id']) if m else units[srcunit],target_quantity=fmt(final),expected_id=int(m['id']) if m else 0,expected_name=m['name'] if m else '',expected_batch=int(m['is_batch_managed']) if m else 1,note=note,source_locations=','.join(sorted({str(dr[4]).strip() or '空' for dr in detail[r['code']]})))
    row['batch_code']='INIT202609-'+r['code']
    out.append(row)
    for wh,qty in allocations.items():allocation.append(dict(code=r['code'],name=r['name'],warehouse=wh,quantity=fmt(qty),unit=targetunit,batch_code=row['batch_code'],row=r['row'],note=note))
(root/'import-plan.json').write_text(json.dumps(dict(materials=out,stock=allocation,warehouses=warehouses),ensure_ascii=False,indent=2),encoding='utf-8')
with (root/'仓库分配清单.csv').open('w',encoding='utf-8-sig',newline='') as f:
    w=csv.DictWriter(f,fieldnames=list(allocation[0]));w.writeheader();w.writerows(allocation)
def q(v):
    if v is None:return 'NULL'
    if isinstance(v,(int,Decimal)):return str(v)
    return "'"+str(v).replace('\\','\\\\').replace("'","''").replace('\n','\\n').replace('\r','\\r')+"'"
sql=[]
sql.append('CREATE TEMPORARY TABLE stage_inventory_material(code VARCHAR(50) PRIMARY KEY,name VARCHAR(200),unit_id BIGINT,quantity DECIMAL(24,6),expected_id BIGINT,expected_name VARCHAR(200),batch_managed INT,batch_code VARCHAR(64),remark VARCHAR(500)) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci')
for r in out:
    remark=f"来源2026年9月份盘点表(虫草核对确认).xls Sheet1第{r['row']}行;结存{r['quantity']}{r['unit']};{r['note']}"
    sql.append('INSERT INTO stage_inventory_material VALUES('+','.join(q(v) for v in (r['code'],r['name'],r['unit_id'],Decimal(r['target_quantity']),r['expected_id'],r['expected_name'],r['expected_batch'],r['batch_code'],remark)) +')')
sql.append('CREATE TEMPORARY TABLE stage_inventory_warehouse(code VARCHAR(100) PRIMARY KEY,name VARCHAR(100),remark VARCHAR(255)) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci')
for r in warehouses:sql.append('INSERT INTO stage_inventory_warehouse VALUES('+','.join(q(v) for v in r)+')')
sql.append('CREATE TEMPORARY TABLE stage_inventory_stock(code VARCHAR(50),warehouse_code VARCHAR(100),quantity DECIMAL(24,6),remark VARCHAR(255),PRIMARY KEY(code,warehouse_code)) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci')
for r in allocation:
    remark=f"2026年9月台账Sheet1第{r['row']}行,按结存覆盖;按原表库位分仓;INIT202609为期初汇总批次,不代表真实生产批次;{r['note']}"
    assert len(remark)<=255
    sql.append('INSERT INTO stage_inventory_stock VALUES('+','.join(q(v) for v in (r['code'],r['warehouse'],Decimal(r['quantity']),remark))+')')
(root/'remote-import.sql').write_text(';\n'.join(sql)+';\n',encoding='utf-8')
print('materials',len(out),'new',sum(not r['expected_id'] for r in out),'stock',len(allocation),'positive',sum(Decimal(r['quantity'])>0 for r in allocation),'warehouse allocation',dict(collections.Counter(r['warehouse'] for r in allocation)))