← back to Dw Photo Capture

create_22_new.py

81 lines

#!/usr/bin/env python3
"""Bulk-create the net-new GRS sheet items as DRAFT Shopify products with full info
   (no image yet — they go live once photographed). DRY default; CONFIRM=1 writes."""
import os,sys,json,re,time,urllib.request
SHOP="designer-laboratory-sandbox.myshopify.com";API="2024-10";DRY=os.environ.get("CONFIRM","0")!="1"
TOKEN=""
for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
    if l.startswith("SHOPIFY_ADMIN_TOKEN="): TOKEN=l.strip().split("=",1)[1];break
H={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"}
def api(m,p,pl=None):
    d=json.dumps(pl).encode() if pl is not None else None
    r=urllib.request.Request(f"https://{SHOP}/admin/api/{API}"+p,data=d,headers=H,method=m)
    with urllib.request.urlopen(r,timeout=60) as x: b=x.read().decode();return x.status,(json.loads(b) if b.strip() else {})
SHEET={x["grs"].upper():x for x in json.load(open(os.path.expanduser("~/Projects/dw-photo-capture/data/sheet_grs.json")))}
# the net-new list straight from the app
import urllib.request as u, base64
req=u.Request("http://127.0.0.1:9890/api/new",headers={"Authorization":"Basic "+base64.b64encode(b"admin:DW2024!").decode()})
NEW=json.loads(u.urlopen(req,timeout=20).read())["items"]
def title(s):
    name=(s.get("name") or "").strip(); color=(s.get("color") or "").strip()
    nl,cl=name.lower(),color.lower()
    col=(" "+color) if (color and nl!=cl and cl not in nl and nl not in cl) else ""
    if name: return f"{name}{col} Grasscloth Wallcovering | Fentucci"
    if s.get("material"): return f"Fentucci {s['material']} Grasscloth Wallcovering"+((" — "+color) if color else "")
    return "Fentucci Grasscloth Wallcovering"+((" — "+color) if color else "")
def body(s):
    lead=s.get("name") or (("Fentucci "+s["material"]+" grasscloth") if s.get("material") else "This Fentucci grasscloth")
    de=(s.get("desc") or "").strip()
    if de and de.lower().startswith("is "): return f"{lead} {de}"
    return de or f"{lead} is a natural woven grasscloth wallcovering with handcrafted natural-fiber texture."
def mf(ns,k,v): return {"namespace":ns,"key":k,"value":str(v),"type":"single_line_text_field"} if v else None

# ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
# A $0 variant must NEVER receive positive stock: positive stock is what flips availableForSale
# to true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 -> 11140 -> 11299 ->
# 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL, so the remedy is NEVER
# to write a placeholder price - it is "do not stock it".
# THIS FILE'S SPECIFIC DEFECT: the Roll payload omitted the `price` key entirely when the sheet
# had no cost (`if price: roll["price"]=str(price)`), and Shopify DEFAULTS an absent price to
# 0.00 - while the same payload set inventory_quantity 2026. So a "Needs-Cost" row minted a $0
# Roll already stocked at 2026, i.e. orderable at $0 the moment the draft is activated.
# The draft is still CREATED (that is the point of the Needs-Cost workflow) - it is just not
# stocked until a real price lands. Steve's 2026-06-20 "active products are never out of stock"
# rule is preserved for PRICED goods: a priced roll still gets `desired`.
# Python twin of shopify/scripts/lib/inventory-stamp-guard.mjs safeStampQuantity(); kept as a
# local function because this is the only Python writer on the path and a JS import is impossible.
def safe_quantity(price, desired=2026):
    try:
        p = float(price)
    except (TypeError, ValueError):
        return 0
    return desired if p > 0 else 0
# ── GUARD TK-11357 END ────────────────────────────────────────────

print(f"{len(NEW)} net-new to create as DRAFT (full info, no image)")
created=0
for it in NEW:
    s=SHEET.get(it["dw_sku"].upper())
    if not s: print(f"   skip {it['dw_sku']} (no sheet row)"); continue
    grs=s["grs"]; price=s.get("price"); col=(s.get("color") or "").strip()
    tags=["Grasscloth","Natural Wallcovering","Fentucci","TWIL Naturals","display_variant","Needs-Image"]+([f"color:{col}"] if col else [])+([] if price else ["Needs-Cost"])
    roll={"option1":"Roll","sku":grs,"inventory_management":"shopify","inventory_quantity":safe_quantity(price)}
    if price: roll["price"]=str(price)
    if safe_quantity(price)==0: print(f"   \u26d4 {grs}: no price -> Roll created UNSTOCKED (qty 0), not orderable (TK-11357 guard)")
    payload={"product":{"title":title(s),"body_html":body(s),"vendor":"Fentucci","product_type":"Wallcovering",
      "status":"draft","published_scope":"global","tags":", ".join(tags),"options":[{"name":"Size"}],
      "variants":[roll,{"option1":"Sample","sku":grs+"-Sample","price":"4.25","inventory_management":"shopify","inventory_quantity":2026}],
      "metafields":[x for x in [mf("global","width",s.get("width") or "36 Inches"),mf("global","length",s.get("length") or "8 Yards"),
        mf("global","unit_of_measure","Priced Per Single Roll"),mf("global","Content","Natural Grasscloth"),
        mf("global","Brand","Fentucci"),mf("global","Collection","TWIL Naturals"),mf("global","Color-Way",col),mf("custom","color",col),
        mf("custom","manufacturer_sku",s.get("mfr")),mf("dwc","manufacturer_sku",s.get("mfr")),
        mf("custom","pattern_name",s.get("name")),mf("dwc","pattern_name",s.get("name"))] if x]}}
    if DRY: print(f"   [dry] {grs:12} {('$'+str(price)) if price else 'no-price':9} {title(s)[:46]}"); continue
    try:
        st,r=api("POST","/products.json",payload); created+=1
        print(f"   created {grs} -> {r.get('product',{}).get('id')} (draft)"); time.sleep(0.5)
    except Exception as e:
        bd=getattr(e,'read',lambda:b'')() if hasattr(e,'read') else b''; print(f"   ERR {grs}: {e} {bd[:140]}")
if DRY: print(f"\n[DRY] would create {len(NEW)} drafts. CONFIRM=1 to create.")
else: print(f"\nDONE — created {created} draft products (Needs-Image; photograph to go live).")