feat: initial release v1.0.0 (CW-031)

This commit is contained in:
2026-09-23 15:39:02 -05:00
commit 270378854c
5 changed files with 1229 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Build the MPM project budget workbook (Summary / Project Budget / Actuals).
Usage: python3 build_budget.py budget_input.json out.xlsx
Every cost figure is a live formula; blue cells are inputs; yellow cells on Actuals get filled as bills arrive."""
import json, sys
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.worksheet.datavalidation import DataValidation
from openpyxl.utils import get_column_letter as CL
SECTIONS = ["Hardware", "MPM Professional Services", "Travel", "DBE Installation", "Warranty", "Annual Recurring"]
RATE_ROWS = [("base", "Base Tariff"), ("s122", "Section 122 Reciprocal"), ("liberation", "Liberation Tariff"),
("customs_fee", "Customs Fee"), (None, "Total Tariff"), ("roger_lu", "Roger Lu Fee"),
("customs_agent", "Customs Agent ($/unit)"), ("air", "Air"), ("express", "Express"), ("ocean", "Ocean"),
("overhead", "Overhead / Spoilage")]
R0, HDR = 4, 18 # rate-table header row, line-table header row
DARK, GOLD = "1C1D22", "D4AF37"
F_HDR = PatternFill("solid", fgColor=DARK); F_SEC = PatternFill("solid", fgColor="E7E6E6")
F_SUB = PatternFill("solid", fgColor="F2F2F2"); F_IN = PatternFill("solid", fgColor="DDEBF7")
F_ACT = PatternFill("solid", fgColor="FFF2CC"); F_TOT = PatternFill("solid", fgColor=GOLD)
WHITE_B = Font(bold=True, color="FFFFFF"); B = Font(bold=True); BLUE = Font(color="0000FF")
USD = '$#,##0.00;[Red]-$#,##0.00'; PCT = '0.00%'; THIN = Border(bottom=Side(style="thin", color="BFBFBF"))
def rate_table(ws, rates, title):
ws.cell(R0 - 1, 1, title).font = B
ws.cell(R0, 1, "Rate (by HS column)").font = WHITE_B
cols = rates["columns"]
for j, c in enumerate(cols):
ws.cell(R0, 2 + j, str(c)).font = WHITE_B
ws.cell(R0, 6, "Source / notes").font = WHITE_B
for j in range(1, 7):
ws.cell(R0, j).fill = F_HDR
for i, (key, label) in enumerate(RATE_ROWS, start=1):
r = R0 + i
ws.cell(r, 1, rates.get("labels", {}).get(key, label) if key else label)
for j in range(4):
c = ws.cell(r, 2 + j)
if key is None:
c.value = f"=SUM({CL(2+j)}{R0+1}:{CL(2+j)}{R0+4})"; c.font = B
else:
c.value = rates[key][j]; c.font = BLUE; c.fill = F_IN
c.number_format = USD if key in ("customs_agent", "air", "express", "ocean") else PCT
note = rates.get("notes", {}).get(key or "total", "")
ws.cell(r, 6, note)
fr = R0 + len(RATE_ROWS) + 1 # freight-mode row (16)
ws.cell(fr, 1, "Freight mode").font = B
c = ws.cell(fr, 2, rates.get("freight_mode", "Ocean")); c.font = BLUE; c.fill = F_IN
dv = DataValidation(type="list", formula1='"Air,Express,Ocean"', allow_blank=False); ws.add_data_validation(dv); dv.add(c)
ws.cell(fr, 6, rates.get("source", ""))
return fr
def refs():
h = f"$B${R0}:$E${R0}"
row = lambda n: f"$B${R0+n}:$E${R0+n}"
return dict(hdr=h, tariff=row(5), roger=row(6), agent=row(7), ovh=row(11),
frt_block=f"$B${R0+8}:$E${R0+10}", frt_lbl=f"$A${R0+8}:$A${R0+10}", mode=f"$B${R0+12}")
def header(ws, cols, widths):
for j, (name, w) in enumerate(zip(cols, widths), start=1):
c = ws.cell(HDR, j, name); c.font = WHITE_B; c.fill = F_HDR
c.alignment = Alignment(wrap_text=True, vertical="center"); ws.column_dimensions[CL(j)].width = w
ws.row_dimensions[HDR].height = 42; ws.freeze_panes = ws.cell(HDR + 1, 4)
def main(src, out):
d = json.load(open(src)); p = d["project"]; lines = d["lines"]
wb = Workbook(); sm = wb.active; sm.title = "Summary"
bud = wb.create_sheet("Project Budget"); act = wb.create_sheet("Actuals")
for ws, t in ((bud, "Project Budget"), (act, "Actuals")):
ws.cell(1, 1, f"{t} — {p.get('name','')}").font = Font(bold=True, size=14)
ws.cell(2, 1, f"SO {p.get('so','')} | PO {p.get('po','')} | {p.get('agency','')} | Reseller: {p.get('reseller','')}")
rate_table(bud, {**d["budget_rates"], "freight_mode": d.get("freight_mode", "Ocean")},
"Budget rates — Hardware Calculator Inputs (the rates the quote was built on)")
rate_table(act, {**d["actual_rates"], "freight_mode": d.get("freight_mode", "Ocean")},
f"Actual rates — prevailing HS-code tariffs as of {d['actual_rates'].get('as_of','')}")
rf = refs()
hs = lambda col, r: f"MATCH(${col}{r},{rf['hdr']},0)"
header(bud, ["Section", "Product Code", "Description", "Qty", "Unit Price (MSRP)", "Disc %", "Sale Subtotal",
"SO Unit Cost", "Cost Source", "HS Col", "HS Code", "Factory Cost", "Gross KG", "Tariff %",
"Roger Lu %", "Overhead %", "Customs Agent $", "Freight $/unit", "Budget Unit Cost",
"Budget Ext Cost", "SO Ext Cost", "Budget − SO", "Margin $", "Margin %"],
[16, 22, 44, 7, 14, 8, 15, 13, 24, 9, 14, 12, 9, 9, 9, 9, 11, 11, 14, 15, 15, 13, 14, 9])
header(act, ["Section", "Product Code", "Description", "Qty", "HS Col", "HS Code", "Factory Cost (budget)",
"Gross KG", "Tariff % (current)", "Roger Lu %", "Overhead %", "Customs Agent $", "Freight $/unit",
"Forecast Unit Cost @ current tariff", "Budget Unit Cost", "ACTUAL factory / unit cost (enter)",
"Actual Unit Cost", "Actual Ext Cost", "Budget Ext − Actual", "Vendor / Bill Ref", "Date", "Notes"],
[16, 22, 44, 7, 9, 14, 13, 9, 10, 9, 9, 11, 11, 15, 14, 16, 14, 15, 15, 20, 12, 30])
r = HDR + 1; sub = {}
for sec in SECTIONS:
items = [l for l in lines if l["section"] == sec]
if not items:
continue
for ws in (bud, act):
ws.cell(r, 1, sec).font = B
for j in range(1, 25 if ws is bud else 23):
ws.cell(r, j).fill = F_SEC
r += 1; first = r
for l in items:
calc = l.get("calc")
b = lambda j, v: bud.cell(r, j, v)
b(1, sec); b(2, l.get("code", "")); b(3, l.get("name", "")); b(4, l["qty"])
b(5, l["unit_price"]).number_format = USD
b(6, (l.get("discount_pct") or 0) / 100).number_format = PCT
b(7, f"=D{r}*E{r}*(1-F{r})").number_format = USD
if l.get("warranty") and not l.get("so_unit_cost"):
b(8, f"=E{r}*0.7569"); b(9, "Warranty formula (MSRP × 0.7569)")
else:
c = b(8, l.get("so_unit_cost", 0)); b(9, l.get("cost_source", "Odoo SO line"))
if l.get("cost_source", "").lower().startswith("user"):
c.font = BLUE; c.fill = F_IN
bud.cell(r, 8).number_format = USD
if calc:
b(10, str(calc["hs_col"])); b(11, calc.get("hs_code", ""))
b(12, calc["factory_cost"]).font = BLUE; b(13, calc["gross_kg"]).font = BLUE
b(14, f"=INDEX({rf['tariff']},{hs('J', r)})"); b(15, f"=INDEX({rf['roger']},{hs('J', r)})")
b(16, f"=INDEX({rf['ovh']},{hs('J', r)})"); b(17, f"=INDEX({rf['agent']},{hs('J', r)})")
b(18, f"=M{r}*INDEX({rf['frt_block']},MATCH({rf['mode']},{rf['frt_lbl']},0),{hs('J', r)})")
b(19, f"=L{r}*(1+N{r}+O{r}+P{r})+Q{r}+R{r}")
else:
b(19, f"=H{r}")
b(20, f"=D{r}*S{r}"); b(21, f"=D{r}*H{r}"); b(22, f"=T{r}-U{r}"); b(23, f"=G{r}-T{r}")
b(24, f'=IF(G{r}=0,"",W{r}/G{r})')
for j, fmt in ((12, USD), (14, PCT), (15, PCT), (16, PCT), (17, USD), (18, USD), (19, USD), (20, USD),
(21, USD), (22, USD), (23, USD), (24, PCT)):
bud.cell(r, j).number_format = fmt
# ---- Actuals row (same row number) ----
a = lambda j, v: act.cell(r, j, v)
BP = "'Project Budget'!"
a(1, sec); a(2, f"={BP}B{r}"); a(3, f"={BP}C{r}"); a(4, f"={BP}D{r}")
a(15, f"={BP}S{r}")
if calc:
a(5, f"={BP}J{r}"); a(6, f"={BP}K{r}"); a(7, f"={BP}L{r}"); a(8, f"={BP}M{r}")
a(9, f"=INDEX({rf['tariff']},{hs('E', r)})"); a(10, f"=INDEX({rf['roger']},{hs('E', r)})")
a(11, f"=INDEX({rf['ovh']},{hs('E', r)})"); a(12, f"=INDEX({rf['agent']},{hs('E', r)})")
a(13, f"=H{r}*INDEX({rf['frt_block']},MATCH({rf['mode']},{rf['frt_lbl']},0),{hs('E', r)})")
a(14, f"=G{r}*(1+I{r}+J{r}+K{r})+L{r}+M{r}")
a(17, f'=IF(P{r}="","",P{r}*(1+I{r}+J{r}+K{r})+L{r}+M{r})')
else:
a(14, f"=O{r}"); a(17, f'=IF(P{r}="","",P{r})')
act.cell(r, 16).fill = F_ACT; act.cell(r, 20).fill = F_ACT; act.cell(r, 21).fill = F_ACT
act.cell(r, 22).fill = F_ACT
a(18, f'=IF(Q{r}="","",Q{r}*D{r})'); a(19, f'=IF(R{r}="","",{BP}T{r}-R{r})')
for j, fmt in ((7, USD), (9, PCT), (10, PCT), (11, PCT), (12, USD), (13, USD), (14, USD), (15, USD),
(16, USD), (17, USD), (18, USD), (19, USD)):
act.cell(r, j).number_format = fmt
for ws in (bud, act):
ws.cell(r, 3).alignment = Alignment(wrap_text=True, vertical="top")
for j in range(1, 25): ws.cell(r, j).border = THIN
r += 1
last = r - 1
# subtotal rows
bud.cell(r, 1, f"{sec} subtotal").font = B
for j in (7, 20, 21, 22, 23):
c = bud.cell(r, j, f"=SUM({CL(j)}{first}:{CL(j)}{last})"); c.font = B; c.number_format = USD
c = bud.cell(r, 24, f'=IF(G{r}=0,"",W{r}/G{r})'); c.font = B; c.number_format = PCT
act.cell(r, 1, f"{sec} subtotal").font = B
c = act.cell(r, 14, f"=SUMPRODUCT(D{first}:D{last},N{first}:N{last})"); c.font = B; c.number_format = USD
c = act.cell(r, 15, f"=SUMPRODUCT(D{first}:D{last},O{first}:O{last})"); c.font = B; c.number_format = USD
for j in (18, 19):
c = act.cell(r, j, f"=SUM({CL(j)}{first}:{CL(j)}{last})"); c.font = B; c.number_format = USD
for ws in (bud, act):
for j in range(1, 25 if ws is bud else 23): ws.cell(r, j).fill = F_SUB
sub[sec] = r; r += 2
# ---------------- Summary ----------------
sm.cell(1, 1, f"Project Financial Summary — {p.get('name','')}").font = Font(bold=True, size=16)
info = [("End-customer agency", p.get("agency")), ("Reseller / customer", p.get("reseller")),
("Sales Order", p.get("so")), ("Customer PO", p.get("po")), ("PO date", p.get("po_date")),
("Project Manager", p.get("pm")), ("Salesperson", p.get("salesperson")), ("Budget built", p.get("built"))]
for i, (k, v) in enumerate(info, start=3):
sm.cell(i, 1, k).font = B; sm.cell(i, 2, v or "")
h = 12
cols = ["Category", "Sale Price", "Projected Cost", "Projected Margin $", "Projected Margin %",
"Forecast Cost @ Current Tariffs", "Actual Cost to Date", "Remaining (Projected − Actual)", "% of Budget Spent"]
for j, n in enumerate(cols, start=1):
c = sm.cell(h, j, n); c.font = WHITE_B; c.fill = F_HDR; c.alignment = Alignment(wrap_text=True)
sm.column_dimensions[CL(j)].width = 28 if j == 1 else 18
sm.row_dimensions[h].height = 36
BP, AC = "'Project Budget'!", "Actuals!"
r = h + 1; rows = {}
for sec in SECTIONS:
sm.cell(r, 1, sec)
s = sub.get(sec)
vals = [f"={BP}G{s}", f"={BP}T{s}", f"={AC}N{s}", f"={AC}R{s}"] if s else [0, 0, 0, 0]
sm.cell(r, 2, vals[0]); sm.cell(r, 3, vals[1]); sm.cell(r, 6, vals[2]); sm.cell(r, 7, vals[3])
rows[sec] = r; r += 1
def fill_derived(rr):
sm.cell(rr, 4, f"=B{rr}-C{rr}"); sm.cell(rr, 5, f'=IF(B{rr}=0,"",D{rr}/B{rr})')
sm.cell(rr, 8, f"=C{rr}-G{rr}"); sm.cell(rr, 9, f'=IF(C{rr}=0,"",G{rr}/C{rr})')
for j in range(2, 10):
sm.cell(rr, j).number_format = PCT if j in (5, 9) else USD
for rr in rows.values():
fill_derived(rr)
one_time = [rows[s] for s in SECTIONS if s != "Annual Recurring"]
for label, rng, fill in (("One-time project total (excl. recurring)", one_time, F_SUB),
("Total incl. Year-1 annual recurring", list(rows.values()), F_TOT)):
sm.cell(r, 1, label).font = B
for j in (2, 3, 6, 7):
sm.cell(r, j, "=" + "+".join(f"{CL(j)}{x}" for x in rng)).font = B
fill_derived(r)
for j in range(1, 10): sm.cell(r, j).fill = fill
r += 1
sm.cell(r + 1, 1, "Projected = Hardware Calculator rates the quote was built on. Forecast = same formulas at today's "
"HS-code tariffs (Actuals rate table). Actual = costs entered on the Actuals tab as bills arrive.").font = Font(italic=True, color="595959")
sm.freeze_panes = "B13"
wb.save(out); print("saved", out, "| section subtotal rows:", sub)
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""Build the MPM internal kickoff deck from the Kickoff Deck.pptx template look.
Usage: python3 build_deck.py "Kickoff Deck.pptx" deck_input.json out.pptx
Template slide 1 = cover, slide 2 = content master (dark left title panel + right body). All other template
slides are dropped. Each JSON slide becomes one or more content slides (long tables/text auto-continue)."""
import copy, json, sys
from pptx import Presentation
from pptx.util import Emu, Pt
from pptx.dml.color import RGBColor
DARK, GOLD, GREY = RGBColor(0x1C, 0x1D, 0x22), RGBColor(0xD4, 0xAF, 0x37), RGBColor(0xF2, 0xF2, 0xF2)
BODY_L, BODY_T, BODY_W, BODY_H = 9596190, 1660731, 13594280, 11700000
CHARS_PER_LINE = 60
R_NS = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}"
def dup_slide(prs, src):
new = prs.slides.add_slide(src.slide_layout)
for shp in list(new.shapes):
shp._element.getparent().remove(shp._element)
for el in src.shapes._spTree.iterchildren():
if el.tag.endswith(("}nvGrpSpPr", "}grpSpPr")):
continue
new.shapes._spTree.append(copy.deepcopy(el))
for node in new.shapes._spTree.iter(): # re-point image relationships
for attr in ("embed", "link", "id"):
rid = node.get(R_NS + attr)
if rid and rid in src.part.rels:
rel = src.part.rels[rid]
if not rel.is_external:
node.set(R_NS + attr, new.part.relate_to(rel.target_part, rel.reltype))
return new
def drop_slide(prs, slide):
lst = prs.slides._sldIdLst
for sid in list(lst):
if prs.part.related_part(sid.rId) is slide.part:
prs.part.drop_rel(sid.rId); lst.remove(sid); return
def shape(slide, name, nth=0):
"""Find a template shape by name; fall back to the nth text box if the template was edited."""
hit = [s for s in slide.shapes if s.name == name]
return hit[0] if hit else [s for s in slide.shapes if s.has_text_frame and s.text_frame.text.strip()][nth]
def set_lines(tf, lines):
"""Replace text keeping the first run's formatting."""
p0 = tf.paragraphs[0]; r0 = copy.deepcopy(p0.runs[0]._r) if p0.runs else None
for p in tf.paragraphs[1:]:
p._p.getparent().remove(p._p)
for r in list(p0.runs):
r._r.getparent().remove(r._r)
for i, line in enumerate(lines):
p = p0 if i == 0 else tf.add_paragraph()
if i:
p._p.insert(0, copy.deepcopy(p0._p.pPr)) if p0._p.pPr is not None else None
if r0 is not None:
rr = copy.deepcopy(r0); p._p.append(rr); p.runs[-1].text = line
else:
p.text = line
def est_lines(block):
n = 0
if block.get("heading"): n += 1.3
for t in ([block["text"]] if block.get("text") else []) + block.get("bullets", []):
n += max(1, -(-len(t) // CHARS_PER_LINE))
return n + 0.6
def write_body(tb, blocks):
tf = tb.text_frame; tf.word_wrap = True
head_r = copy.deepcopy(tf.paragraphs[0].runs[0]._r) # Playfair heading run
body_r = copy.deepcopy(next(r for p in tf.paragraphs[1:] for r in p.runs)._r) # Karla body run
for p in tf.paragraphs[1:]:
p._p.getparent().remove(p._p)
p0 = tf.paragraphs[0]
for r in list(p0.runs):
r._r.getparent().remove(r._r)
first = [True]
def para(run_xml, text, size):
p = p0 if first[0] else tf.add_paragraph(); first[0] = False
r = copy.deepcopy(run_xml); p._p.append(r); p.runs[-1].text = text; p.runs[-1].font.size = Pt(size)
for b in blocks:
if b.get("heading"): para(head_r, b["heading"].upper(), 36)
if b.get("text"):
for line in b["text"].split("\n"): para(body_r, line, 30)
for t in b.get("bullets", []): para(body_r, "• " + t, 30)
para(body_r, "", 18)
def table_rows_emu(cols, rows):
"""Estimated height of each row, allowing for text wrapping in narrow columns."""
per_col = max(6, int(92 / max(1, len(cols)))) # usable chars per column at 20pt, after word-wrap loss
return [max(1, max(-(-len(str(v or "")) // per_col) for v in r)) * 310000 + 160000 for r in [cols] + rows]
def table_emu(b, with_heading=True):
return (950000 if with_heading and b.get("heading") else 0) + sum(table_rows_emu(b["table"]["columns"], b["table"]["rows"])) + 450000
def add_table(slide, heading, cols, rows, top):
"""Draw a table at `top`; return the y position just below it."""
if heading:
tb = slide.shapes.add_textbox(BODY_L, top, BODY_W, Emu(700000))
r = tb.text_frame.paragraphs[0].add_run(); r.text = heading.upper()
r.font.size, r.font.bold, r.font.underline, r.font.name, r.font.color.rgb = Pt(36), True, True, "Playfair Display", DARK
top += 950000
heights = table_rows_emu(cols, rows)
shp = slide.shapes.add_table(len(rows) + 1, len(cols), BODY_L, top, BODY_W, Emu(sum(heights)))
t = shp.table
for i, hgt in enumerate(heights):
t.rows[i].height = Emu(hgt)
for j, c in enumerate(cols):
cell = t.cell(0, j); cell.text = str(c); cell.fill.solid(); cell.fill.fore_color.rgb = DARK
for i, row in enumerate(rows, start=1):
for j, v in enumerate(row):
cell = t.cell(i, j); cell.text = "" if v is None else str(v)
cell.fill.solid(); cell.fill.fore_color.rgb = GREY if i % 2 else RGBColor(0xFF, 0xFF, 0xFF)
for i, row in enumerate(t.rows):
for cell in row.cells:
for p in cell.text_frame.paragraphs:
for r in p.runs:
r.font.size = Pt(20); r.font.name = "Karla"
r.font.bold = i == 0
r.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) if i == 0 else DARK
if i and str(r.text).upper() in ("HIGH", "CRITICAL"): r.font.color.rgb = RGBColor(0xC0, 0x00, 0x00)
return top + sum(heights) + 450000
def paginate(spec):
"""Split one JSON slide into pages that fit. Text and tables share a page while they fit;
text never follows a table on the same page (the body text box sits at the top)."""
pages, cur, used, has_table = [], [], 0, False
for b in spec.get("blocks", []):
if "table" in b:
rows = b["table"]["rows"] or [["—"] * len(b["table"]["columns"])]
chunk, k = [], 0
while k < len(rows): # split long tables by height, not a fixed row count
chunk = [rows[k]]; k += 1
while k < len(rows) and table_emu({**b, "table": {**b["table"], "rows": chunk + [rows[k]]}}) <= BODY_H:
chunk.append(rows[k]); k += 1
part = {**b, "table": {**b["table"], "rows": chunk}}
need = table_emu(part)
if cur and used + need > BODY_H:
pages.append(cur); cur, used = [], 0
cur.append(part); used += need; has_table = True
continue
need = est_lines(b) * 560000
if cur and (has_table or used + need > BODY_H):
pages.append(cur); cur, used, has_table = [], 0, False
cur.append(b); used += need
if cur: pages.append(cur)
return pages or [[]]
def main(template, src, out):
d = json.load(open(src)); prs = Presentation(template)
cover, master = prs.slides[0], prs.slides[1]
originals = list(prs.slides)[1:]
set_lines(shape(cover, "Neoride…").text_frame, [d["agency"]])
tf = shape(cover, "Neoride…").text_frame
sub = tf.add_paragraph(); r = sub.add_run(); r.text = d.get("subtitle", "Internal Project Kickoff")
r.font.size = Pt(40); r.font.color.rgb = GOLD
for spec in d["slides"]:
pages = paginate(spec)
for k, blocks in enumerate(pages):
s = dup_slide(prs, master)
title = spec["title"].split("\n") + (["(cont.)"] if k else [])
set_lines(shape(s, "Transitpoint Software and Services").text_frame, title)
body = shape(s, "TextBox 6", 1)
text_blocks = [b for b in blocks if "table" not in b]
if text_blocks:
write_body(body, text_blocks)
else:
body._element.getparent().remove(body._element)
top = BODY_T + (Emu(int(sum(est_lines(b) for b in text_blocks) * 560000)) if text_blocks else 0)
for b in blocks:
if "table" in b:
top = add_table(s, b.get("heading", ""), b["table"]["columns"], b["table"]["rows"], top)
for s in originals:
drop_slide(prs, s)
prs.save(out); print("saved", out, "slides:", len(prs.slides))
if __name__ == "__main__":
main(*sys.argv[1:4])
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Pull budget rates + per-SKU factory cost / gross KG / HS column out of Hardware Calculator.xlsx.
Usage: python3 read_calculator.py "Hardware Calculator.xlsx" calc.json"""
import json, sys
from openpyxl import load_workbook
wb = load_workbook(sys.argv[1], data_only=False)
inp = wb["Inputs"]
cols = [str(inp.cell(1, c).value).replace(".0", "") for c in range(2, 6)] # 8471, 8528, Steel, Other HS Code
rowmap = {"base": 2, "s122": 3, "liberation": 4, "customs_fee": 5, "roger_lu": 8, "customs_agent": 9,
"air": 11, "express": 12, "ocean": 13, "overhead": 15}
rates = {k: [float(inp.cell(r, c).value or 0) for c in range(2, 6)] for k, r in rowmap.items()}
rates["columns"] = ["8471", "8528", "Steel", "Other"]
rates["labels"] = {k: inp.cell(r, 1).value for k, r in rowmap.items() if k in ("base", "s122", "liberation", "customs_fee")}
rates["reseller_b_margin"] = inp["B17"].value; rates["msrp_margin"] = inp["B18"].value
col_of = lambda hs: {8471: "8471", 8528: "8528"}.get(int(float(hs)) if str(hs).replace(".", "").isdigit() else 0,
"Steel" if str(hs).lower().startswith("steel") else "Other")
skus = {}
pe = wb["Pricing Engine"]
for r in range(2, pe.max_row + 1):
code = pe.cell(r, 2).value
if code:
skus[code.strip().upper()] = {"name": pe.cell(r, 3).value, "category": pe.cell(r, 4).value,
"hs_col": col_of(pe.cell(r, 5).value or 0), "hs_code": pe.cell(r, 6).value,
"factory_cost": pe.cell(r, 8).value, "gross_kg": pe.cell(r, 9).value,
"source": f"Pricing Engine row {r}"}
models = {}
md = wb["Models"]
for r in range(2, md.max_row + 1):
m = md.cell(r, 1).value
if m:
models[str(m).strip().upper()] = {"hs_col": col_of(md.cell(r, 2).value or "other"), "gross_kg": md.cell(r, 3).value,
"factory_cost": md.cell(r, 4).value, "source": f"Models row {r}"}
json.dump({"rates": rates, "skus": skus, "models": models}, open(sys.argv[2], "w"), indent=1, default=str)
print(f"rates for {cols}; {len(skus)} SKUs ({sum(1 for s in skus.values() if s['factory_cost'])} with factory cost); {len(models)} models")