Spaces:
Paused
Paused
Download pricing_engine.py from ThatandMoore/quote-gen-api: direct link, hf CLI and curl.
- Browser
- Download file 8.61 kB
-
https://huggingface.co/spaces/ThatandMoore/quote-gen-api/resolve/main/pricing_engine.py
- Command line
-
hf download hf://spaces/ThatandMoore/quote-gen-api/pricing_engine.py
-
curl -L -o pricing_engine.py https://huggingface.co/spaces/ThatandMoore/quote-gen-api/resolve/main/pricing_engine.py
8.61 kB
| """ | |
| PRICING ENGINE v1 β the core of the Quote Stack | |
| ================================================ | |
| What this file IS: a machine that turns a job spec (what the customer | |
| wants) plus a config (your shop's numbers) into a priced quote. | |
| job_example.json ββ | |
| βββ> pricing_engine.py ββ> quote.json + printed summary | |
| pricing_config.jsonβ | |
| Shop analogy: this file is the CNC. The config is your tool library and | |
| material database. The job file is the work order. You never modify the | |
| machine to run a new job β you change the work order. You never modify | |
| the machine when material prices change β you update the database. | |
| THAT separation is the whole architecture. | |
| Run it: | |
| python3 pricing_engine.py job_example.json | |
| Every function below follows one rule: data in, data out, no surprises. | |
| Like a jig β same input, same output, every time. That's called a | |
| "pure function" and it's why each piece can be tested by itself. | |
| """ | |
| import json # reads/writes .json files (structured data as text) | |
| import sys # lets us read what was typed after the script name | |
| from pathlib import Path # a clean way to handle file paths | |
| # --------------------------------------------------------------------- | |
| # STEP 1 β LOADING | |
| # Read a .json file from disk and turn it into a Python dict. | |
| # A dict is just a labeled bin system: dict["tax_rate"] pulls the value | |
| # out of the bin labeled "tax_rate". JSON and dicts are the same idea β | |
| # one lives in a file, one lives in memory. | |
| # --------------------------------------------------------------------- | |
| def load_json(path: str) -> dict: | |
| """Open a JSON file and return its contents as a dict.""" | |
| return json.loads(Path(path).read_text()) | |
| # --------------------------------------------------------------------- | |
| # STEP 2 β MATERIAL COST (per single cabinet) | |
| # Walk the recipe for one cabinet type and price its ingredients: | |
| # sheet goods + edgebanding + hardware. The escalation dial multiplies | |
| # all material costs β your oil-shock lever from the config. | |
| # --------------------------------------------------------------------- | |
| def material_cost(cab: dict, config: dict) -> dict: | |
| """Return the material cost breakdown for ONE cabinet of this type.""" | |
| materials = config["materials"] | |
| hardware_prices = config["hardware"] | |
| escalation = 1.0 + config["escalation"]["material_escalation_pct"] | |
| # Sheet goods: cost per square foot * square feet this cabinet uses | |
| ply = materials["PLY_34_PREFIN"] | |
| cost_per_sqft = ply["cost_per_sheet"] / ply["sqft_per_sheet"] | |
| sheet_cost = cost_per_sqft * cab["sheet_sqft"] | |
| # Edgebanding: cost per linear foot * feet used | |
| band_cost = materials["EDGE_PVC"]["cost_per_lf"] * cab["edgeband_lf"] | |
| # Hardware: for each item in the cabinet's hardware recipe, | |
| # look up its unit price and multiply by how many it needs. | |
| hw_cost = 0.0 | |
| for item_name, qty_needed in cab["hardware"].items(): | |
| hw_cost += hardware_prices[item_name] * qty_needed | |
| return { | |
| "sheet_goods": round(sheet_cost * escalation, 2), | |
| "edgebanding": round(band_cost * escalation, 2), | |
| "hardware": round(hw_cost * escalation, 2), | |
| "total": round((sheet_cost + band_cost + hw_cost) * escalation, 2), | |
| } | |
| # --------------------------------------------------------------------- | |
| # STEP 3 β LABOR + MACHINE COST (per single cabinet) | |
| # Hours * rate. Bench labor and CNC time are separate lines because | |
| # they cost different amounts and you'll want to see them separately. | |
| # --------------------------------------------------------------------- | |
| def labor_cost(cab: dict, config: dict) -> dict: | |
| """Return labor + machine cost for ONE cabinet of this type.""" | |
| rates = config["rates"] | |
| bench = cab["build_hrs"] * rates["shop_labor_per_hr"] | |
| cnc = cab["cnc_hrs"] * rates["cnc_machine_per_hr"] | |
| return { | |
| "bench_labor": round(bench, 2), | |
| "cnc_time": round(cnc, 2), | |
| "total": round(bench + cnc, 2), | |
| } | |
| # --------------------------------------------------------------------- | |
| # STEP 4 β PRICE ONE LINE ITEM | |
| # Cost -> overhead -> margin -> sell price. Note the margin math: | |
| # | |
| # MARKUP: price = cost * (1 + 35%) -> 35% ON TOP of cost | |
| # MARGIN: price = cost / (1 - 35%) -> 35% OF THE SELL PRICE | |
| # | |
| # These are NOT the same. A 35% markup is only a 25.9% margin. | |
| # Shops that confuse them slowly bleed out. We use true margin. | |
| # --------------------------------------------------------------------- | |
| def price_line(line: dict, config: dict) -> dict: | |
| """Take one job line ({type, qty}) and return it fully priced.""" | |
| cab = config["cabinet_types"][line["type"]] | |
| mat = material_cost(cab, config) | |
| lab = labor_cost(cab, config) | |
| # Direct cost = what one cabinet costs you in materials + labor | |
| unit_direct = mat["total"] + lab["total"] | |
| # Overhead = rent, insurance, electricity... spread as a % of direct | |
| unit_overhead = unit_direct * config["shop"]["overhead_pct"] | |
| unit_cost = unit_direct + unit_overhead | |
| # Sell price using TRUE margin (see note above) | |
| margin = config["shop"]["target_margin_pct"] | |
| unit_price = unit_cost / (1.0 - margin) | |
| qty = line["qty"] | |
| return { | |
| "type": line["type"], | |
| "description": cab["desc"], | |
| "qty": qty, | |
| "unit_materials": mat["total"], | |
| "unit_labor": lab["total"], | |
| "unit_overhead": round(unit_overhead, 2), | |
| "unit_cost": round(unit_cost, 2), | |
| "unit_price": round(unit_price, 2), | |
| "extended_price": round(unit_price * qty, 2), | |
| } | |
| # --------------------------------------------------------------------- | |
| # STEP 5 β BUILD THE WHOLE QUOTE | |
| # Loop over every line in the job, price each one, then total up. | |
| # --------------------------------------------------------------------- | |
| def build_quote(job: dict, config: dict) -> dict: | |
| """Price every line of a job and return the complete quote dict.""" | |
| priced_lines = [price_line(line, config) for line in job["lines"]] | |
| subtotal = sum(line["extended_price"] for line in priced_lines) | |
| tax = subtotal * config["shop"]["tax_rate"] | |
| return { | |
| "customer": job["customer"], | |
| "project": job["project"], | |
| "lines": priced_lines, | |
| "subtotal": round(subtotal, 2), | |
| "tax": round(tax, 2), | |
| "total": round(subtotal + tax, 2), | |
| "assumptions": { | |
| "margin_pct": config["shop"]["target_margin_pct"], | |
| "overhead_pct": config["shop"]["overhead_pct"], | |
| "material_escalation_pct": config["escalation"]["material_escalation_pct"], | |
| }, | |
| } | |
| # --------------------------------------------------------------------- | |
| # STEP 6 β HUMAN-READABLE OUTPUT | |
| # The quote dict is for machines (PandaDoc, your app, the SaaS). | |
| # This printout is for you. | |
| # --------------------------------------------------------------------- | |
| def print_summary(quote: dict) -> None: | |
| """Print a readable quote summary to the screen.""" | |
| print(f"\nQUOTE β {quote['project']}") | |
| print(f"Customer: {quote['customer']}") | |
| print("-" * 68) | |
| for ln in quote["lines"]: | |
| print(f"{ln['qty']:>3} x {ln['description']:<38} " | |
| f"@ ${ln['unit_price']:>8,.2f} = ${ln['extended_price']:>10,.2f}") | |
| print("-" * 68) | |
| print(f"{'Subtotal':>56}: ${quote['subtotal']:>10,.2f}") | |
| print(f"{'Tax':>56}: ${quote['tax']:>10,.2f}") | |
| print(f"{'TOTAL':>56}: ${quote['total']:>10,.2f}\n") | |
| # --------------------------------------------------------------------- | |
| # STEP 7 β THE MAIN SEQUENCE (runs when you execute this file) | |
| # Load config -> load job -> build quote -> print it -> save quote.json | |
| # --------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| # sys.argv is the list of words you typed on the command line. | |
| # sys.argv[0] is the script name; sys.argv[1] is the job file. | |
| job_path = sys.argv[1] if len(sys.argv) > 1 else "job_example.json" | |
| config = load_json("pricing_config.json") | |
| job = load_json(job_path) | |
| quote = build_quote(job, config) | |
| print_summary(quote) | |
| # Save the machine-readable version next to the job file | |
| out_path = Path(job_path).with_name("quote.json") | |
| out_path.write_text(json.dumps(quote, indent=2)) | |
| print(f"Saved machine-readable quote to: {out_path}") | |