[object Object]

← back to Designer Wallcoverings

Stage full Winfield Thybony line (2,613 SKUs) into kravet_catalog for cadence — yard-priced foils recovered at MAP; WDW2310 incl

aaf83b4229903cb6813c8bbbf619a9b83db1f798 · 2026-06-25 16:55:23 -0700 · Steve

Files touched

Diff

commit aaf83b4229903cb6813c8bbbf619a9b83db1f798
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jun 25 16:55:23 2026 -0700

    Stage full Winfield Thybony line (2,613 SKUs) into kravet_catalog for cadence — yard-priced foils recovered at MAP; WDW2310 incl
---
 shopify/scripts/cadence/stage-winfield-thybony.py | 147 ++++++++++++++++++++++
 1 file changed, 147 insertions(+)

diff --git a/shopify/scripts/cadence/stage-winfield-thybony.py b/shopify/scripts/cadence/stage-winfield-thybony.py
new file mode 100644
index 00000000..255e7250
--- /dev/null
+++ b/shopify/scripts/cadence/stage-winfield-thybony.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+"""
+stage-winfield-thybony.py — stage the full Winfield Thybony line into kravet_catalog
+so the metered DW cadence (cadence-import.js, Kravet entry) can publish it at MAP.
+
+WHY: Winfield Thybony foils/grasscloths are sold per YARD. The Kravet master loader's
+old ROLL-only filter dropped all ~2,402 yard SKUs (incl. WDW2310 / 321 WDW foils), so
+they never got a price record and never published — 2,858 SKUs prepped in the
+2026-04-20 Kravet export but 0 sellable on Shopify. (Diagnosed 2026-06-25.)
+
+SOURCE (Steve 2026-06-25 "use April sheet now"):
+  ~/Projects/kravet-sheet-sync-2026-04-20/kravet_new_2026-04-20.csv  (DW SKU + WHLS + MAP)
+  ~/Projects/kravet-sheet-sync-2026-04-20/kravet_image_catalog.jsonl (mfr_sku -> Brandfolder CDN URL)
+
+SAFE: DRY-RUN by default — reads only, prints a stage/dedup/gate-readiness report.
+  --commit UPSERTs into kravet_catalog with shopify_product_id NULL ("Staged for New");
+  it NEVER creates Shopify products (that is the metered cadence's job, cadence-only rule)
+  and NEVER touches rows that already carry a shopify_product_id (never-duplicate rule).
+
+Usage:
+  python3 stage-winfield-thybony.py            # DRY-RUN report
+  python3 stage-winfield-thybony.py --commit   # upsert kravet_catalog staging rows
+"""
+import argparse, csv, json, os, subprocess, sys
+
+CSV   = os.path.expanduser('~/Projects/kravet-sheet-sync-2026-04-20/kravet_new_2026-04-20.csv')
+IMGS  = os.path.expanduser('~/Projects/kravet-sheet-sync-2026-04-20/kravet_image_catalog.jsonl')
+BRAND = 'WINFIELD THYBONY'
+
+def num(v):
+    try: return float(str(v).replace('$','').replace(',','').strip())
+    except (ValueError, TypeError): return None
+
+def psql(sql):
+    r = subprocess.run(['psql','-d','dw_unified','-tAF','\t','-c',sql], capture_output=True, text=True)
+    if r.returncode != 0: sys.exit(f"psql error: {r.stderr[:300]}")
+    return r.stdout
+
+def load_image_index():
+    idx = {}
+    if not os.path.exists(IMGS): return idx
+    for line in open(IMGS, encoding='utf-8', errors='replace'):
+        line = line.strip()
+        if not line: continue
+        try: o = json.loads(line)
+        except json.JSONDecodeError: continue
+        a = o.get('algolia') or {}
+        url = a.get('full') or a.get('image') or a.get('thumbnail')
+        if o.get('sku') and url: idx[o['sku']] = url
+    return idx
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--commit', action='store_true')
+    args = ap.parse_args()
+
+    img_idx = load_image_index()
+
+    # Pull the Winfield rows from the April export.
+    rows = []
+    with open(CSV, newline='', encoding='utf-8', errors='replace') as fh:
+        for r in csv.DictReader(fh):
+            if BRAND not in (r.get('brand') or '').upper(): continue
+            dw, mfr = (r.get('new_dw_sku') or '').strip(), (r.get('mfr_sku') or '').strip()
+            if not dw or not mfr: continue
+            mp, whls = num(r.get('map')), num(r.get('wholesale'))
+            rows.append({
+                'dw_sku': dw, 'mfr_sku': mfr,
+                'pattern': (r.get('pattern_name') or '').strip(),
+                'color':   (r.get('color_name') or '').strip(),
+                'unit':    (r.get('unit_of_measure') or '').strip().upper(),
+                'width':   (r.get('width') or '').strip(),
+                'content': (r.get('content') or '').strip(),
+                'cost': whls, 'map': mp,
+                'status': (r.get('display_status') or '').strip(),
+                'img': img_idx.get(mfr),
+            })
+
+    # Dedup signals from the live DB.
+    have_pid = set(psql(
+        "SELECT dw_sku FROM kravet_catalog WHERE coalesce(shopify_product_id::text,'')<>'';").split())
+    on_shop = set(x for x in psql(
+        "SELECT sku FROM shopify_products UNION SELECT replace(sku,'-Sample','') FROM shopify_products "
+        "WHERE sku LIKE '%-Sample';").split())
+    in_cat = set(psql("SELECT dw_sku FROM kravet_catalog;").split())
+
+    total = len(rows)
+    priced   = [r for r in rows if (r['map'] or 0) > 0 and (r['cost'] or 0) > 0]
+    has_img  = [r for r in priced if r['img']]
+    has_wid  = [r for r in has_img if r['width']]
+    gate_ok  = has_wid                                  # price + image + width = activation-eligible
+    already  = [r for r in rows if r['dw_sku'] in have_pid or r['dw_sku'] in on_shop]
+    to_stage = [r for r in rows if r['dw_sku'] not in have_pid and r['dw_sku'] not in on_shop]
+    unit_split = {}
+    for r in to_stage: unit_split[r['unit'] or '(blank)'] = unit_split.get(r['unit'] or '(blank)',0)+1
+
+    print(f"\n=== Winfield Thybony staging report (source: {os.path.basename(CSV)}) ===")
+    print(f"  rows in sheet (brand={BRAND}):            {total}")
+    print(f"  priced (WHLS>0 AND MAP>0):                {len(priced)}")
+    print(f"  + resolvable image URL:                   {len(has_img)}")
+    print(f"  + width (full activation gate eligible):  {len(gate_ok)}")
+    print(f"  already on Shopify / linked (skip):       {len(already)}")
+    print(f"  --> NET to stage into kravet_catalog:     {len(to_stage)}")
+    print(f"      unit split of staged rows:            {unit_split}")
+    # spot-check the SKU that started this
+    wd = next((r for r in rows if r['mfr_sku'].startswith('WDW2310')), None)
+    if wd: print(f"  WDW2310 -> {wd['dw_sku']} {wd['pattern']}/{wd['color']} unit={wd['unit']} cost={wd['cost']} MAP={wd['map']} img={'Y' if wd['img'] else 'N'}")
+
+    if not args.commit:
+        print("\nDRY-RUN — no writes. Re-run with --commit to upsert these staging rows.")
+        return
+
+    # COMMIT: upsert staging rows (shopify_product_id stays NULL = Staged for New).
+    import tempfile
+    tmp = tempfile.mkdtemp(prefix='wt-stage-')
+    tsv = os.path.join(tmp, 'wt.tsv')
+    with open(tsv, 'w') as fh:
+        for r in to_stage:
+            fh.write('\t'.join('' if x in (None,'') else str(x) for x in [
+                r['mfr_sku'], r['dw_sku'], r['pattern'], r['color'], BRAND,
+                r['cost'], r['map'], r['map'], r['unit'], r['width'], r['content'],
+                r['img'] or '', 'WINFIELD THYBONY', r['status'] or 'Active']) + '\n')
+    sql = f"""
+    CREATE TEMP TABLE _wt(mfr_sku text,dw_sku text,pattern_name text,color_name text,brand text,
+      cost_price numeric,price_retail numeric,price_retail_new numeric,unit_of_measure text,
+      width text,content text,image_url text,collection text,sheet_display_status text);
+    \\copy _wt FROM '{tsv}'
+    INSERT INTO kravet_catalog (mfr_sku,dw_sku,pattern_name,color_name,brand,cost_price,
+      price_retail,price_retail_new,unit_of_measure,width,composition,image_url,collection,
+      sheet_display_status,product_type)
+    SELECT mfr_sku,dw_sku,pattern_name,color_name,brand,cost_price,price_retail,price_retail_new,
+      unit_of_measure,width,content,image_url,collection,sheet_display_status,'Wallcovering'
+    FROM _wt
+    ON CONFLICT (mfr_sku) DO UPDATE SET
+      cost_price=EXCLUDED.cost_price, price_retail=EXCLUDED.price_retail,
+      price_retail_new=EXCLUDED.price_retail_new, unit_of_measure=EXCLUDED.unit_of_measure,
+      image_url=COALESCE(NULLIF(kravet_catalog.image_url,''),EXCLUDED.image_url)
+    WHERE coalesce(kravet_catalog.shopify_product_id::text,'')='';
+    SELECT count(*) FROM kravet_catalog WHERE brand='WINFIELD THYBONY' AND coalesce(shopify_product_id::text,'')='';
+    """
+    r = subprocess.run(['psql','-d','dw_unified','-v','ON_ERROR_STOP=1'], input=sql,
+                       capture_output=True, text=True)
+    if r.returncode != 0: sys.exit(f"COMMIT failed: {r.stderr[:400]}")
+    print(f"\n✅ staged Winfield Thybony rows (pending cadence): {r.stdout.strip().splitlines()[-1]}")
+
+if __name__ == '__main__':
+    main()

← 59dba9ea docs(filter-topbar): G3 LIVE publish done — surgical push to  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-25T17:05:58 (13 files) — pending-approval dd45cbf0 →