feat: initial release v1.0.0 (CW-031)
This commit is contained in:
@@ -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])
|
||||
@@ -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])
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user