gaoluyang
12 小时以前 e71d71bf43c2307c89d3592adf08c1b32fcd0907
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
import collections, csv, json, re, unicodedata, xml.etree.ElementTree as ET
from decimal import Decimal
from pathlib import Path
 
ROOT = Path('scripts/stock-import-202609/material-repair')
ROOT.mkdir(exist_ok=True)
db = ET.parse(ROOT.parent / 'remote-duplicate-references-snapshot.xml').getroot()
def rows(tag): return [{x.tag: x.text for x in row} for row in db.find(tag)]
materials = rows('materials')
source = json.loads((ROOT.parent / 'source.json').read_text(encoding='utf-8'))
raw = {str(r[0]).strip(): r for r in source[0]['rows'][1:] if r[0]}
units = {u['id']: u for u in rows('units')}
warehouses = {w['id']: w['code'] for w in rows('warehouses')}
stock_warehouses = collections.defaultdict(set)
for s in rows('stock'): stock_warehouses[s['item_id']].add(warehouses[s['warehouse_id']])
def norm(value):
    return re.sub(r'\s+', '', unicodedata.normalize('NFKC', value or '')).lower().replace('克', 'g').replace('×', '*')
def q(value):
    if value is None: return 'NULL'
    return "'" + str(value).replace('\\', '\\\\').replace("'", "''") + "'"
fixes = []
for m in materials:
    if m['creator'] != 'stock-import-202609': continue
    r = raw[m['code']]
    name, spec = str(r[18] or '').strip(), str(r[19] or '').strip() or None
    # The source's brand/name column contains inconsistent packaging labels in seven rows.
    # Preserve the ledger description for these instead of changing a box/film into a product.
    if not norm(str(r[1])).startswith(norm(name)): name = m['name']
    wh = stock_warehouses[m['id']]
    if m['code'].startswith('74') or 'BCPK' in wh: type_code = 'MAT06'
    elif m['code'].startswith('81'):
        type_code = 'MAT05' if any(t in name for t in ['标签', '底贴', '防伪', '二维码']) else ('MAT03' if 'NBCK' in wh else 'MAT04')
    elif m['code'].startswith(('61', '62', '71')) or wh == {'YLK'}: type_code = 'MAT01'
    else: type_code = 'FG'
    fixes.append(dict(id=int(m['id']), code=m['code'], old_name=m['name'], old_spec=m['specification'], name=name, specification=spec, type_code=type_code))
    m['name'], m['specification'] = name, spec
assert len(fixes) == 582
 
groups = collections.defaultdict(list)
for m in materials: groups[norm(m['name']), norm(m['specification'])].append(m)
merges = []
for group in groups.values():
    if len(group) < 2 or not any(m['creator'] == 'stock-import-202609' for m in group): continue
    group.sort(key=lambda m: (m['create_time'], int(m['id'])))
    original = group[0]
    for duplicate in group[1:]:
        assert duplicate['creator'] == 'stock-import-202609', 'Do not merge pre-existing independent records'
        from_unit, to_unit = units[duplicate['unit_measure_id']], units[original['unit_measure_id']]
        mass = {'GM': Decimal('1'), 'KG': Decimal('1000')}
        if from_unit['code'] in mass and to_unit['code'] in mass:
            factor = mass[from_unit['code']] / mass[to_unit['code']]
        else:
            assert from_unit['code'] == to_unit['code'] or {from_unit['code'], to_unit['code']} <= {'BT', 'BX', 'EA'}, 'Unknown unit conversion'
            factor = Decimal('1')  # User instructed ignoring packaging-unit differences; identical name/spec.
        r = raw[duplicate['code']]
        provenance = f"归并前物料编码:{duplicate['code']};名称:{duplicate['name']};规格:{duplicate['specification'] or '原表未填写'};单位:{from_unit['name'].strip()};产地:{str(r[36] or '').strip()};原描述:{str(r[1] or '').strip()};归并至:{original['code']};数量换算系数:{factor}"
        merges.append(dict(old_id=int(duplicate['id']), old_code=duplicate['code'], new_id=int(original['id']), new_code=original['code'], name=original['name'], specification=original['specification'], old_unit=from_unit['name'].strip(), new_unit=to_unit['name'].strip(), factor=str(factor), provenance=provenance))
assert len(merges) == 10
assert {m['old_id'] for m in merges} == {969,970,971,972,1003,1004,1006,1009,1043,1528}
(ROOT / 'plan.json').write_text(json.dumps(dict(fixes=fixes, merges=merges), ensure_ascii=False, indent=2), encoding='utf-8')
for name, data in [('名称规格分类修正清单.csv', fixes), ('重复物料归并清单.csv', merges)]:
    with (ROOT / name).open('w', encoding='utf-8-sig', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=list(data[0])); writer.writeheader(); writer.writerows(data)
sql = ["CREATE TEMPORARY TABLE stage_material_fix(id BIGINT PRIMARY KEY,code VARCHAR(100),old_name VARCHAR(200),old_spec VARCHAR(255),name VARCHAR(200),specification VARCHAR(255),type_code VARCHAR(64)) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"]
for m in fixes: sql.append('INSERT INTO stage_material_fix VALUES(' + ','.join(q(m[k]) for k in ['id','code','old_name','old_spec','name','specification','type_code']) + ')')
sql.append("CREATE TEMPORARY TABLE stage_material_merge(old_id BIGINT PRIMARY KEY,old_code VARCHAR(100),new_id BIGINT,new_code VARCHAR(100),factor DECIMAL(24,9),provenance VARCHAR(700)) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
for m in merges: sql.append('INSERT INTO stage_material_merge VALUES(' + ','.join(q(m[k]) for k in ['old_id','old_code','new_id','new_code','factor','provenance']) + ')')
(ROOT / 'repair-stage.sql').write_text(';\n'.join(sql) + ';\n', encoding='utf-8')
print(f'Prepared {len(fixes)} field/class repairs, {sum(bool(m["specification"]) for m in fixes)} source specifications, {len(merges)} duplicate merges; source batch provenance retained.')