← back to Dw Yolo Loop

scripts/price-sheets/load-price-sheets.py

138 lines

#!/usr/bin/env python3
"""
load-price-sheets.py — ingest every GDrive vendor price sheet into
dw_unified.vendor_price_sheets (staging). Keyword-detects the header row + the
SKU / wholesale / retail / unit columns per sheet (formats vary wildly: header in
row 1 or 2, Italian 'Listino', multi-sheet workbooks). Per-file vendor hint.

READ→STAGE only. Resolving staging → shopify_products.cost is a separate gated step
(and must carry unit, per the YARD/ROLL lesson). Idempotent per source_file (deletes
prior rows for that file before reload).
"""
import openpyxl, glob, os, re, json, subprocess, sys

SKU_KEYS   = ['sku','item','articolo','pattern number','js number','pattern_number','mfr','style']
WHLS_KEYS  = ['new wholesale','new cost','wholesale','whls','trade price','listino wholesale','cost of goods','current cost','mid year price','current price','cost','net','price']
RETAIL_KEYS= ['new map','new retail','map','listino retail','current retail','retail','msrp']
UNIT_KEYS  = ['unit of measure','unit','uom']
VENDOR_COL_KEYS = ['vendor','brand']

# per-file vendor hint (None => read a Vendor/Brand column per row) + sheets to skip
VENDOR_HINT = {
  'Kravet Price list 1-20-26.xlsx': None,          # already in kravet tables -> skip below
  'Designer Wallcoverings Exclusives Price Increase 2023.xlsx': 'Designer Wallcoverings',
  'MH 2023 PRICE LIST USD LANDED USD RETAIL.xlsx': 'MH-2023-UNIDENTIFIED',
  'MIssoni Price .xlsx': 'Missoni',
  'Mini Modern - Wallpaper price list.xlsx': None, # has per-row Vendor/Brand
  'Ralph Lauren Price Sheet.xlsx': 'Ralph Lauren',
  'Schumacher_2023 Master Price Increase List- MD 7.18.xlsx': 'Schumacher',
}
SKIP_FILES = {'Kravet Price list 1-20-26.xlsx'}  # redundant w/ kravet_authoritative_pricing

def norm(s): return re.sub(r'[^a-z0-9]','',str(s or '').lower())
def num(v):
    if v is None: return None
    try:
        x=float(str(v).replace('$','').replace(',','').strip())
        return x if x>0 else None
    except: return None
def matchcol(headers, keys):
    hl=[str(h).lower().strip() if h is not None else '' for h in headers]
    # pass 1: exact header == key  (so a column literally 'SKU' beats 'Item Description')
    for k in keys:
        for i,h in enumerate(hl):
            if h==k: return i
    # pass 2: header starts with key
    for k in sorted(keys, key=len, reverse=True):
        for i,h in enumerate(hl):
            if h.startswith(k): return i
    # pass 3: contains (longest key first to prefer 'new wholesale' over 'wholesale')
    for k in sorted(keys, key=len, reverse=True):
        for i,h in enumerate(hl):
            if k in h: return i
    return None

def find_header(rows):
    # the row (within first 8) that contains a SKU-like keyword AND a price-like keyword
    for ri in range(min(8,len(rows))):
        cells=[str(c).lower() if c is not None else '' for c in rows[ri]]
        has_sku=any(any(k in c for k in SKU_KEYS) for c in cells)
        has_price=any(any(k in c for k in WHLS_KEYS+RETAIL_KEYS) for c in cells)
        if has_sku and has_price: return ri
    return None

def best_sheet(wb):
    best=None
    for name in wb.sheetnames:
        ws=wb[name]
        rows=[]
        for i,r in enumerate(ws.iter_rows(values_only=True)):
            rows.append(r)
            if i>=12: break
        hr=find_header(rows)
        if hr is not None:
            # count data rows
            n=sum(1 for _ in ws.iter_rows(values_only=True))
            if not best or n>best[3]: best=(name,hr,rows,n)
    return best

def load_file(f):
    base=os.path.basename(f)
    if base in SKIP_FILES: return (base,'SKIP (redundant)',0)
    wb=openpyxl.load_workbook(f, read_only=True, data_only=True)
    bs=best_sheet(wb)
    if not bs: return (base,'NO HEADER FOUND',0)
    name,hr,_,_=bs
    ws=wb[name]
    it=ws.iter_rows(values_only=True)
    for _ in range(hr): next(it)
    headers=list(next(it))
    ci_sku=matchcol(headers,SKU_KEYS); ci_w=matchcol(headers,WHLS_KEYS)
    ci_r=matchcol(headers,RETAIL_KEYS); ci_u=matchcol(headers,UNIT_KEYS)
    ci_v=matchcol(headers,VENDOR_COL_KEYS)
    if ci_sku is None or (ci_w is None and ci_r is None):
        return (base,f'sheet={name} unmappable (sku={ci_sku} w={ci_w} r={ci_r})',0)
    vhint=VENDOR_HINT.get(base)
    rows_out=[]
    for r in it:
        if not r or ci_sku>=len(r): continue
        sku=r[ci_sku]
        if sku is None or str(sku).strip() in ('','None'): continue
        w=num(r[ci_w]) if ci_w is not None and ci_w<len(r) else None
        rt=num(r[ci_r]) if ci_r is not None and ci_r<len(r) else None
        if w is None and rt is None: continue
        unit=(str(r[ci_u]).upper() if ci_u is not None and ci_u<len(r) and r[ci_u] else None)
        vendor=vhint or (str(r[ci_v]) if ci_v is not None and ci_v<len(r) and r[ci_v] else 'UNKNOWN')
        rows_out.append((vendor,str(sku).strip(),norm(sku),w,rt,unit,base))
    return (base,f'sheet={name} hdr@{hr+1} sku@{ci_sku} w@{ci_w} r@{ci_r} u@{ci_u}',rows_out)

def psql(sql, stdin=None):
    return subprocess.run(['psql','-d','dw_unified','-v','ON_ERROR_STOP=1','-c',sql] if stdin is None
                          else ['psql','-d','dw_unified','-v','ON_ERROR_STOP=1','-c',sql],
                          capture_output=True, text=True)

total=0; report=[]
for f in sorted(glob.glob('/tmp/price-sheets/*.xlsx')):
    base,info,rows=load_file(f)
    if isinstance(rows,int) or not rows:
        report.append(f'  [skip] {base}: {info}'); continue
    # idempotent reload for this file
    subprocess.run(['psql','-d','dw_unified','-c',
                    f"delete from vendor_price_sheets where source_file = $${base}$$;"],
                   capture_output=True, text=True)
    # bulk insert via COPY
    import csv, io
    buf=io.StringIO(); w=csv.writer(buf)
    for row in rows: w.writerow(list(row[:-1])+[base])
    buf.seek(0)
    p=subprocess.run(['psql','-d','dw_unified','-c',
        "copy vendor_price_sheets(vendor,mfr_sku,mfr_sku_norm,wholesale,map_or_retail,unit_of_measure,source_file) from stdin with csv"],
        input=buf.getvalue(), capture_output=True, text=True)
    ok = 'COPY' in (p.stdout+p.stderr)
    n=len(rows)
    total += n if ok else 0
    report.append(f'  [{"OK " if ok else "ERR"}] {base}: {n} rows | {info}' + ('' if ok else ' | '+p.stderr.strip()[:120]))

print('\n'.join(report))
print(f'\nTOTAL loaded into vendor_price_sheets: {total}')