[object Object]

← back to Hollywood Price 2026

quote-only executor for the ~258 unpriceable Hollywood leftovers (Versa commercial has no public price)

fd8fbf83e8821f40eed8af7be0f99e29848e1e9b · 2026-07-12 09:51:02 -0700 · Steve Abrams

Files touched

Diff

commit fd8fbf83e8821f40eed8af7be0f99e29848e1e9b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 12 09:51:02 2026 -0700

    quote-only executor for the ~258 unpriceable Hollywood leftovers (Versa commercial has no public price)
---
 quote_only.py | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 91 insertions(+)

diff --git a/quote_only.py b/quote_only.py
new file mode 100644
index 0000000..67841f2
--- /dev/null
+++ b/quote_only.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+"""Quote-only the remaining unpriceable Hollywood products (Steve 2026-07-12).
+After pricing (856) + archiving (267), the leftover ACTIVE + Needs-Price Hollywood
+set is the ~246 unmatched + ~12 no-sku — no resolvable Momentum/Versa price
+(Versa commercial publishes none). Mark them quote-only: ADD `quotes` tag, REMOVE
+`Needs-Price`. Keeps active + Sample-only, no fabricated price. Reversible. $0."""
+import sys, os, csv, datetime, time
+sys.path.insert(0, "lib")
+from shopify import gql
+
+LEDGER = os.path.join("ledger", "quote_only.csv")
+M_ADD = """mutation($id:ID!,$tags:[String!]!){ tagsAdd(id:$id, tags:$tags){ userErrors{ message } } }"""
+M_RM = """mutation($id:ID!,$tags:[String!]!){ tagsRemove(id:$id, tags:$tags){ userErrors{ message } } }"""
+Q = """query($q:String!,$after:String){ products(first:150, after:$after, query:$q){
+  pageInfo{hasNextPage endCursor}
+  edges{ node{ id legacyResourceId title tags
+    variants(first:5){ edges{ node{ sku title } } } } } } }"""
+SEARCH = "vendor:'Hollywood Wallcoverings' status:active tag:Needs-Price"
+
+
+def has_sellable(n):
+    for e in n["variants"]["edges"]:
+        v = e["node"]; sk = (v["sku"] or "")
+        if v["title"] != "Sample" and not sk.lower().endswith("-sample"):
+            return True
+    return False
+
+
+def log(pid, title, status):
+    new = not (os.path.exists(LEDGER) and os.path.getsize(LEDGER) > 0)
+    with open(LEDGER, "a", newline="") as f:
+        w = csv.writer(f)
+        if new:
+            w.writerow(["ts", "product_id", "title", "status"])
+        w.writerow([datetime.datetime.now().isoformat(timespec="seconds"), pid, title, status])
+
+
+def seen():
+    s = set()
+    if os.path.exists(LEDGER):
+        for r in csv.DictReader(open(LEDGER)):
+            if r["status"].startswith("OK") and r.get("product_id"):
+                s.add(r["product_id"])
+    return s
+
+
+def fetch_all():
+    out, cur = [], None
+    while True:
+        d = gql(Q, {"q": SEARCH, "after": cur})
+        p = d["products"]; out += [e["node"] for e in p["edges"]]
+        if not p["pageInfo"]["hasNextPage"]:
+            break
+        cur = p["pageInfo"]["endCursor"]
+    return out
+
+
+def process(n):
+    pid = str(n["legacyResourceId"]); gid = n["id"]
+    # safety: never quote-only a product that actually has a real sellable variant
+    if has_sellable(n):
+        log(pid, n["title"], "SKIP:has-sellable"); return "SKIP:has-sellable"
+    if "quotes" in n["tags"] and "Needs-Price" not in n["tags"]:
+        log(pid, n["title"], "SKIP:already-quote-only"); return "SKIP:already"
+    a = gql(M_ADD, {"id": gid, "tags": ["quotes"]})["tagsAdd"]["userErrors"]
+    r = gql(M_RM, {"id": gid, "tags": ["Needs-Price"]})["tagsRemove"]["userErrors"]
+    if a or r:
+        log(pid, n["title"], "FAIL:" + str(a or r)[:80]); return "FAIL"
+    log(pid, n["title"], "OK"); return "OK"
+
+
+if __name__ == "__main__":
+    nodes = fetch_all()
+    print(f"remaining ACTIVE Needs-Price Hollywood: {len(nodes)}")
+    if "--canary" in sys.argv:
+        k = int(sys.argv[sys.argv.index("--canary") + 1])
+        for n in nodes[:k]:
+            print(f"  {n['title'][:44]:44} -> {process(n)}")
+        sys.exit(0)
+    done = seen()
+    todo = [n for n in nodes if str(n["legacyResourceId"]) not in done]
+    ok = sk = fl = 0
+    for i, n in enumerate(todo):
+        st = process(n)
+        if st == "OK": ok += 1
+        elif st.startswith("FAIL"): fl += 1
+        else: sk += 1
+        if (i + 1) % 40 == 0:
+            print(f"  ...{i+1}/{len(todo)} ok={ok} skip={sk} fail={fl}")
+        time.sleep(0.1)
+    print(f"QUOTE-ONLY DONE: ok={ok} skip={sk} fail={fl}")

← d602474 auto-save: 2026-07-12T09:47:28 (1 files) — unmatched_246.jso  ·  back to Hollywood Price 2026  ·  quote-only 258 unpriceable Hollywood leftovers (quotes tag + 11758ed →