Spaces:
Paused
Paused
Download catalog_engine.py from ThatandMoore/quote-gen-api: direct link, hf CLI and curl.
- Browser
- Download file 7.69 kB
-
https://huggingface.co/spaces/ThatandMoore/quote-gen-api/resolve/main/catalog_engine.py
- Command line
-
hf download hf://spaces/ThatandMoore/quote-gen-api/catalog_engine.py
-
curl -L -o catalog_engine.py https://huggingface.co/spaces/ThatandMoore/quote-gen-api/resolve/main/catalog_engine.py
7.69 kB
| """ | |
| CATALOG ENGINE β prices a job against the shop's price book | |
| ============================================================ | |
| What this file IS: the pricing brain for Phase 2. Where pricing_engine.py | |
| builds a price up from materials + labor + margin, THIS engine looks the | |
| price up in your price book (the price_items table in Supabase) and then | |
| applies the shop's three business rules on top: | |
| 1. Catalog items: the book price IS the sell price. No margin math. | |
| (+ glass_add if the customer wants glass doors) | |
| 2. Buy-out items: entered at what they cost you, marked up 40%. | |
| 3. GC / referral: the whole job gets a finder fee (8-10%) added. | |
| Tax is applied last, on top of everything including the finder fee. | |
| job (what the customer wants) ββ | |
| price_book (SKU -> price) βββ> build_quote() ββ> quote dict | |
| settings (shop percentages) ββ | |
| Shop analogy: pricing_engine.py is the CNC that cuts from raw stock. | |
| This file is the parts counter β you hand it a SKU, it hands you a price. | |
| Nothing in this file touches the network or the database. It only works | |
| on plain dicts, so it can be tested with a fake price book in a fraction | |
| of a second. The database plumbing lives in db.py; the web layer in api.py. | |
| Percentages here are WHOLE NUMBERS, matching the database: | |
| 10 means 10%, not 0.10. (pricing_engine.py uses 0.10 β different file, | |
| different convention. Both are documented; don't mix them up.) | |
| """ | |
| # --------------------------------------------------------------------- | |
| # STEP 1 β INDEX THE PRICE BOOK | |
| # The database hands us a LIST of rows. Looking up a SKU in a list means | |
| # scanning every row. Turning it into a dict keyed by SKU makes each | |
| # lookup instant β like sorting parts into labeled bins once, instead of | |
| # digging through a pile every time. | |
| # --------------------------------------------------------------------- | |
| def index_price_book(rows: list) -> dict: | |
| """Turn a list of price_items rows into {sku: item} with numeric prices.""" | |
| book = {} | |
| for row in rows: | |
| book[row["sku"]] = { | |
| "sku": row["sku"], | |
| "category": row.get("category"), | |
| "description": row.get("description"), | |
| # The database sends numbers as strings sometimes ("700.00"); | |
| # float() makes them real numbers we can do math on. | |
| "price": float(row["price"]), | |
| "glass_add": float(row.get("glass_add") or 0), | |
| "needs_verification": bool(row.get("needs_verification", False)), | |
| } | |
| return book | |
| # --------------------------------------------------------------------- | |
| # STEP 2 β PRICE ONE CATALOG LINE | |
| # Rule 1: the book price is final. The only adjustment is the glass | |
| # upcharge, and only when the customer asked for glass. | |
| # --------------------------------------------------------------------- | |
| def price_catalog_line(line: dict, price_book: dict) -> dict: | |
| """Price one catalog line ({sku, qty, with_glass}) from the price book.""" | |
| sku = line["sku"] | |
| if sku not in price_book: | |
| # A clear message beats a mysterious KeyError three layers up. | |
| raise LookupError(f"Unknown SKU: {sku!r} is not in the price book") | |
| item = price_book[sku] | |
| with_glass = bool(line.get("with_glass", False)) | |
| unit_price = item["price"] + (item["glass_add"] if with_glass else 0.0) | |
| qty = float(line.get("qty", 1)) | |
| return { | |
| "kind": "catalog", | |
| "sku": sku, | |
| "description": item["description"], | |
| "with_glass": with_glass, | |
| "qty": qty, | |
| "unit_price": round(unit_price, 2), | |
| "extended_price": round(unit_price * qty, 2), | |
| # Passed through so the estimator sees a flag on iffy prices. | |
| "needs_verification": item["needs_verification"], | |
| } | |
| # --------------------------------------------------------------------- | |
| # STEP 3 β PRICE ONE BUY-OUT LINE | |
| # Rule 2: you type in your COST; the engine marks it up. A 40% MARKUP | |
| # means price = cost * 1.40 (this is markup, not margin β see the note | |
| # in pricing_engine.py for why those differ). Your shop's rule is | |
| # stated as a markup, so that's what we use here. | |
| # --------------------------------------------------------------------- | |
| def price_buyout_line(line: dict, markup_pct: float) -> dict: | |
| """Price one buy-out line ({description, cost, qty}) with a markup.""" | |
| cost = float(line["cost"]) | |
| qty = float(line.get("qty", 1)) | |
| unit_price = cost * (1.0 + markup_pct / 100.0) | |
| return { | |
| "kind": "buyout", | |
| "description": line["description"], | |
| "qty": qty, | |
| "unit_cost": round(cost, 2), | |
| "markup_pct": markup_pct, | |
| "unit_price": round(unit_price, 2), | |
| "extended_price": round(unit_price * qty, 2), | |
| } | |
| # --------------------------------------------------------------------- | |
| # STEP 4 β BUILD THE WHOLE QUOTE | |
| # Price every line, add them up, then apply Rule 3 (finder fee) and tax. | |
| # | |
| # `settings` are the shop defaults from the database: | |
| # {"finder_fee_pct": 10, "buyout_markup_pct": 40, "tax_pct": 6} | |
| # A job may override any of them (e.g. an 8% fee for a particular GC). | |
| # If the job doesn't say, the shop default wins. | |
| # | |
| # Sales tax is a TOGGLE, not just a number: `apply_tax: false` on a job | |
| # (tax-exempt customer, resale certificate, out-of-state) zeroes the tax | |
| # line while leaving the shop's rate untouched for the next job. | |
| # --------------------------------------------------------------------- | |
| def build_quote(job: dict, price_book: dict, settings: dict) -> dict: | |
| """Price every line of a job and return the complete quote dict.""" | |
| # Job-level override if given, otherwise shop default. | |
| def setting(job_key: str, settings_key: str) -> float: | |
| value = job.get(job_key) | |
| return float(value if value is not None else settings[settings_key]) | |
| gc_referral = bool(job.get("gc_referral", False)) | |
| apply_tax = bool(job.get("apply_tax", True)) # default: taxed | |
| gc_fee_pct = setting("gc_fee_pct", "finder_fee_pct") | |
| markup_pct = setting("buyout_markup_pct", "buyout_markup_pct") | |
| tax_pct = setting("tax_pct", "tax_pct") | |
| priced_lines = [] | |
| for line in job["lines"]: | |
| if line["kind"] == "catalog": | |
| priced_lines.append(price_catalog_line(line, price_book)) | |
| elif line["kind"] == "buyout": | |
| priced_lines.append(price_buyout_line(line, markup_pct)) | |
| else: | |
| raise ValueError(f"Unknown line kind: {line['kind']!r}") | |
| subtotal = sum(ln["extended_price"] for ln in priced_lines) | |
| # Rule 3: finder fee only on GC/referral jobs, as a % of the subtotal. | |
| finder_fee = subtotal * gc_fee_pct / 100.0 if gc_referral else 0.0 | |
| # Tax is charged on the PRE-TAX TOTAL: subtotal + finder fee. | |
| taxable = subtotal + finder_fee | |
| tax = taxable * tax_pct / 100.0 if apply_tax else 0.0 | |
| return { | |
| "customer": job.get("customer"), | |
| "project": job.get("project"), | |
| "lines": priced_lines, | |
| "subtotal": round(subtotal, 2), | |
| "gc_referral": gc_referral, | |
| "finder_fee_pct": gc_fee_pct if gc_referral else 0.0, | |
| "finder_fee": round(finder_fee, 2), | |
| "apply_tax": apply_tax, | |
| "tax_pct": tax_pct if apply_tax else 0.0, | |
| "tax": round(tax, 2), | |
| "total": round(taxable + tax, 2), | |
| "assumptions": { | |
| "buyout_markup_pct": markup_pct, | |
| "finder_fee_pct": gc_fee_pct, | |
| "tax_pct": tax_pct, | |
| }, | |
| } | |