""" quote.py — rate card + quote math (ported from sketch-to-quote src/lib/pricing.js). Every cost-side number is measured off real gold POs (guts.vendors_pos_poskus, trailing 120-180 days), not assumed. Customer side: gold at the live rate, diamonds at a flat Rs.55,000/ct, making Rs.900/g (calibrated: solving listed_price/1.03 = gold + making*gm + 55000*ct across live-priced BOM SKUs fit best at 900/g, median abs error 8.5%). The quote is built on the DESIGNED piece's spec (goldGm + stone lines), not on the matched catalogue SKU. """ from sieve import SIEVE_TABLE, sieve_by_name, sieve_for_ct DEFAULT_RATES = { "gold24": 14450, # fine 24K Rs/g; replaced at runtime by live rate "ktPurity": {9: 0.375, 14: 0.585, 18: 0.75}, "labourBase": {9: 718, 14: 820, 18: 872}, "labourMult": [ {"maxPcs": 0, "m": 1.9}, {"maxPcs": 3, "m": 1.31}, {"maxPcs": 10, "m": 1.0}, {"maxPcs": 20, "m": 1.36}, {"maxPcs": 40, "m": 1.82}, {"maxPcs": float("inf"), "m": 3.1}, ], "diaCostBands": [ {"maxCps": 0.004, "rate": 12900}, {"maxCps": 0.007, "rate": 11850}, {"maxCps": 0.012, "rate": 8250}, {"maxCps": 0.02, "rate": 6720}, {"maxCps": 0.05, "rate": 6320}, {"maxCps": 0.15, "rate": 7510}, {"maxCps": 0.40, "rate": 8560}, {"maxCps": float("inf"), "rate": 9990}, ], "hallmarkPerPc": 45, "wastagePct": 2, "making": 900, "diaRetailPerCt": 55000, "gstPct": 3, } PLACEHOLDER_PRICES = {9769} def real_price(p): try: v = float(p) except (TypeError, ValueError): return None return v if v > 0 and round(v) not in PLACEHOLDER_PRICES else None def purity_for(kt, rates=DEFAULT_RATES): return rates["ktPurity"].get(kt, 0.585) def gold_rate_per_g(kt, rates=DEFAULT_RATES): return rates["gold24"] * purity_for(kt, rates) def dia_cost_rate(ct_per_stone, rates=DEFAULT_RATES): cps = float(ct_per_stone or 0) for b in rates["diaCostBands"]: if cps <= b["maxCps"]: return b["rate"] return rates["diaCostBands"][-1]["rate"] def labour_per_g(kt, pcs, rates=DEFAULT_RATES): base = rates["labourBase"].get(kt, rates["labourBase"][14]) n = float(pcs or 0) band = next((b for b in rates["labourMult"] if n <= b["maxPcs"]), {"m": 1}) return base * band["m"] def ct_for_sieve(sieve): hit = sieve_by_name(sieve) return hit["ct"] if hit else 0.01 def normalize_lines(lines): """Rebuild line carats from pcs x sieve, so an edited stone count flows through.""" out = [] for l in (lines or []): if float(l.get("pcs") or 0) <= 0: continue cps = float(l.get("ctPerStone") or ct_for_sieve(l.get("sieve"))) pcs = max(0, round(float(l.get("pcs") or 0))) out.append({**l, "pcs": pcs, "ctPerStone": cps, "ct": round(pcs * cps, 3)}) return out def total_pcs(lines): return sum(l["pcs"] for l in normalize_lines(lines)) def total_ct(lines): return round(sum(l["ct"] for l in normalize_lines(lines)), 3) def compute_quote(spec, kt, rates=DEFAULT_RATES): """spec: {goldGm, lines, units, category} — the DESIGNED piece.""" if not spec or not (float(spec.get("goldGm") or 0) > 0): return None gm = float(spec["goldGm"]) lines = normalize_lines(spec.get("lines")) pcs = sum(l["pcs"] for l in lines) ct = sum(l["ct"] for l in lines) units = max(1, int(spec.get("units") or 1)) gold_rate_g = gold_rate_per_g(kt, rates) gold_cost = gm * gold_rate_g wastage = gold_cost * (rates["wastagePct"] / 100) lab_rate = labour_per_g(kt, pcs, rates) labour = gm * lab_rate dia_lines = [{**l, "rate": dia_cost_rate(l["ctPerStone"], rates), "cost": l["ct"] * dia_cost_rate(l["ctPerStone"], rates)} for l in lines] dia_cost = sum(l["cost"] for l in dia_lines) hallmark = rates["hallmarkPerPc"] * units cogs = gold_cost + wastage + labour + dia_cost + hallmark making = gm * rates["making"] dia_retail = ct * rates["diaRetailPerCt"] price_ex_gst = gold_cost + making + dia_retail price = price_ex_gst * (1 + rates["gstPct"] / 100) margin = price_ex_gst - cogs return { "kt": kt, "gm": gm, "pcs": pcs, "ct": ct, "units": units, "lines": lines, "goldRateG": gold_rate_g, "goldCost": gold_cost, "wastage": wastage, "labRate": lab_rate, "labour": labour, "diaLines": dia_lines, "diaCost": dia_cost, "hallmark": hallmark, "cogs": cogs, "making": making, "diaRetail": dia_retail, "priceExGst": price_ex_gst, "price": price, "margin": margin, "marginPct": (margin / price_ex_gst * 100) if price_ex_gst else 0, } def _gm_for_price(target, spec, kt, rates): """Gold grams that land exactly on a target all-in price, stack fixed.""" ct = total_ct(spec.get("lines")) per_g = gold_rate_per_g(kt, rates) + rates["making"] fixed = ct * rates["diaRetailPerCt"] gm = (target / (1 + rates["gstPct"] / 100) - fixed) / per_g return round(gm, 2) if gm > 0.05 else None def _shrink_stones(lines): out = [] for l in normalize_lines(lines): i = next((k for k, s in enumerate(SIEVE_TABLE) if s["sieve"] == l["sieve"]), 0) nxt = SIEVE_TABLE[i - 1] if i > 0 else SIEVE_TABLE[0] out.append({**l, "sieve": nxt["sieve"], "mm": nxt["mm"], "ctPerStone": nxt["ct"]}) return out def budget_levers(spec, kt, rates, budget): """Concrete, fully-costed ways to hit a customer's number.""" if not spec or not budget or not (budget > 0): return [] now = compute_quote(spec, kt, rates) if not now: return [] gap = now["price"] - budget out = [] def add(label, s, k, note): q = compute_quote(s, k, rates) if not q: return out.append({"label": label, "note": note, "kt": k, "spec": s, "price": q["price"], "quote": q, "fits": q["price"] <= budget * 1.02, "delta": q["price"] - now["price"]}) if gap > 0: for k in (14, 9): if k < kt: add(f"{k}KT instead of {kt}KT", spec, k, "same design, lighter alloy value") gm = _gm_for_price(budget, spec, kt, rates) if gm and gm < spec["goldGm"] * 0.98: add(f"Slimmer build — {gm} g of gold", {**spec, "goldGm": gm}, kt, f"{round((1 - gm / spec['goldGm']) * 100)}% less metal: thinner band or hollow section") shrunk = _shrink_stones(spec.get("lines")) if total_ct(shrunk) < total_ct(spec.get("lines")): add("One sieve smaller on every stone", {**spec, "lines": shrunk}, kt, f"{total_ct(spec.get('lines')):.3f} to {total_ct(shrunk):.3f} ct, same stone count") combo = {**spec, "lines": shrunk} combo_kt = 14 if kt > 14 else 9 combo_gm = _gm_for_price(budget, combo, combo_kt, rates) if combo_gm: add(f"{combo_kt}KT + smaller stones + {combo_gm} g", {**combo, "goldGm": combo_gm}, combo_kt, "stacked changes to land on budget exactly") else: gm = _gm_for_price(budget, spec, kt, rates) if gm and gm > spec["goldGm"] * 1.02: add(f"Heavier build — {gm} g of gold", {**spec, "goldGm": gm}, kt, "more substantial piece within the same budget") if kt < 18: add("Upgrade to 18KT", spec, 18, "richer colour, higher resale value") bigger = [] for l in normalize_lines(spec.get("lines")): i = next((k for k, s in enumerate(SIEVE_TABLE) if s["sieve"] == l["sieve"]), 0) nxt = SIEVE_TABLE[min(len(SIEVE_TABLE) - 1, i + 1)] bigger.append({**l, "sieve": nxt["sieve"], "mm": nxt["mm"], "ctPerStone": nxt["ct"]}) add("One sieve larger on every stone", {**spec, "lines": bigger}, kt, f"{total_ct(spec.get('lines')):.3f} to {total_ct(bigger):.3f} ct") seen, uniq = set(), [] for o in out: key = round(o["price"]) if key not in seen: seen.add(key) uniq.append(o) uniq.sort(key=lambda o: abs(o["price"] - budget)) return uniq[:4] def estimate_timeline(spec): pcs = total_pcs((spec or {}).get("lines")) or 0 extra = 1 if pcs > 15 else 0 stages = [ {"name": "Casting & goldwork", "lo": 5, "hi": 7}, {"name": "Stone setting & polish", "lo": 4 + extra, "hi": 6 + extra}, {"name": "QC & IGI certification", "lo": 2, "hi": 3}, {"name": "Dispatch & delivery", "lo": 4, "hi": 7}, ] return {"stages": stages, "lo": sum(s["lo"] for s in stages), "hi": sum(s["hi"] for s in stages)} def inr(v, digits=0): if v is None: return "—" return "₹" + f"{float(v):,.{digits}f}"