mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 04:13:12 +00:00
fix(pdf): fit weekly summaries on one page without a broken title (#138)
Some checks failed
Build Lambda Layer / build (push) Has been cancelled
Some checks failed
Build Lambda Layer / build (push) Has been cancelled
Helvetica cannot encode an em dash, so the header rendered a question mark. Tighter single-column spacing keeps a 12-person week on page 1 while still paginating large weeks.
This commit is contained in:
parent
10c86f2de6
commit
58b9d4f83b
2 changed files with 53 additions and 20 deletions
|
|
@ -50,37 +50,37 @@ def build_weekly_summary_pdf(
|
|||
Raw PDF bytes. Contains no prices — strictly employee, item, quantity.
|
||||
"""
|
||||
pdf = FPDF(orientation="P", unit="mm", format="A4")
|
||||
pdf.set_auto_page_break(auto=True, margin=15)
|
||||
pdf.set_auto_page_break(auto=True, margin=10)
|
||||
pdf.add_page()
|
||||
|
||||
# ─── Header ───────────────────────────────────────────────────
|
||||
pdf.set_font("Helvetica", "B", 18)
|
||||
pdf.set_font("Helvetica", "B", 14)
|
||||
pdf.cell(
|
||||
0,
|
||||
10,
|
||||
_latin1(f"Weekly Meal Order Summary — {week}"),
|
||||
6,
|
||||
_latin1(f"Weekly Meal Order Summary - {week}"),
|
||||
new_x="LMARGIN",
|
||||
new_y="NEXT",
|
||||
)
|
||||
|
||||
pdf.set_font("Helvetica", "", 10)
|
||||
pdf.set_font("Helvetica", "", 9)
|
||||
pdf.set_text_color(110, 110, 110)
|
||||
pdf.cell(
|
||||
0,
|
||||
6,
|
||||
4,
|
||||
_latin1(f"Generated {_format_generated_at(generated_at)}"),
|
||||
new_x="LMARGIN",
|
||||
new_y="NEXT",
|
||||
)
|
||||
pdf.cell(
|
||||
0,
|
||||
6,
|
||||
4,
|
||||
_latin1(f"{int(total_employees)} employees - {int(total_meals)} meals"),
|
||||
new_x="LMARGIN",
|
||||
new_y="NEXT",
|
||||
)
|
||||
pdf.set_text_color(0, 0, 0)
|
||||
pdf.ln(4)
|
||||
pdf.ln(1.5)
|
||||
|
||||
# ─── Per-employee sections ────────────────────────────────────
|
||||
ordered = sorted(orders, key=lambda o: str(o.get("employee_name", "")).lower())
|
||||
|
|
@ -88,21 +88,21 @@ def build_weekly_summary_pdf(
|
|||
name = order.get("employee_name", "")
|
||||
email = order.get("employee_email", "")
|
||||
|
||||
pdf.set_font("Helvetica", "B", 12)
|
||||
pdf.cell(0, 7, _latin1(name), new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_font("Helvetica", "B", 10)
|
||||
pdf.cell(0, 4.5, _latin1(name), new_x="LMARGIN", new_y="NEXT")
|
||||
if email:
|
||||
pdf.set_font("Helvetica", "", 9)
|
||||
pdf.set_font("Helvetica", "", 8)
|
||||
pdf.set_text_color(110, 110, 110)
|
||||
pdf.cell(0, 5, _latin1(email), new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.cell(0, 3, _latin1(email), new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_text_color(0, 0, 0)
|
||||
|
||||
# Item / Qty table
|
||||
pdf.set_font("Helvetica", "B", 9)
|
||||
pdf.set_font("Helvetica", "B", 8)
|
||||
pdf.set_fill_color(243, 244, 246)
|
||||
pdf.cell(140, 7, "Item", border="B", fill=True)
|
||||
pdf.cell(140, 4, "Item", border="B", fill=True)
|
||||
pdf.cell(
|
||||
30,
|
||||
7,
|
||||
4,
|
||||
"Qty",
|
||||
border="B",
|
||||
align="R",
|
||||
|
|
@ -111,18 +111,18 @@ def build_weekly_summary_pdf(
|
|||
new_y="NEXT",
|
||||
)
|
||||
|
||||
pdf.set_font("Helvetica", "", 10)
|
||||
pdf.set_font("Helvetica", "", 9)
|
||||
for item in order.get("items", []):
|
||||
pdf.cell(140, 7, _latin1(item.get("name", "")), border="B")
|
||||
pdf.cell(140, 4, _latin1(item.get("name", "")), border="B")
|
||||
pdf.cell(
|
||||
30,
|
||||
7,
|
||||
4,
|
||||
str(int(item.get("quantity", 0))),
|
||||
border="B",
|
||||
align="R",
|
||||
new_x="LMARGIN",
|
||||
new_y="NEXT",
|
||||
)
|
||||
pdf.ln(5)
|
||||
pdf.ln(1.5)
|
||||
|
||||
return bytes(pdf.output())
|
||||
|
|
|
|||
|
|
@ -10,10 +10,16 @@ class _NoCompressFPDF(_RealFPDF):
|
|||
"""FPDF subclass with stream compression off, so the content stream holds
|
||||
readable text we can assert against without a PDF parser."""
|
||||
|
||||
last_pages_count = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_compression(False)
|
||||
|
||||
def output(self, *args, **kwargs):
|
||||
_NoCompressFPDF.last_pages_count = self.pages_count
|
||||
return super().output(*args, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def readable_pdf(monkeypatch):
|
||||
|
|
@ -50,6 +56,20 @@ def _build(orders, **overrides):
|
|||
return build_weekly_summary_pdf(orders, **kwargs)
|
||||
|
||||
|
||||
def _volume_orders(count, items_per_employee=2):
|
||||
"""Synthetic orders for layout/page-count checks."""
|
||||
return [
|
||||
{
|
||||
"employee_name": f"Employee {i:02d}",
|
||||
"employee_email": f"emp{i:02d}@seahavenind.com",
|
||||
"items": [
|
||||
{"name": f"Meal {j}", "quantity": 1} for j in range(items_per_employee)
|
||||
],
|
||||
}
|
||||
for i in range(count)
|
||||
]
|
||||
|
||||
|
||||
class TestPdfStructure:
|
||||
def test_returns_pdf_bytes(self):
|
||||
pdf = _build(_orders())
|
||||
|
|
@ -66,7 +86,7 @@ class TestPdfContent:
|
|||
def test_header_shows_week_and_counts(self, readable_pdf):
|
||||
pdf = _build(_orders())
|
||||
text = pdf.decode("latin-1")
|
||||
assert "2026-W19" in text
|
||||
assert "Weekly Meal Order Summary - 2026-W19" in text
|
||||
assert "2 employees" in text
|
||||
assert "6 meals" in text
|
||||
|
||||
|
|
@ -103,3 +123,16 @@ class TestPdfContent:
|
|||
]
|
||||
pdf = _build(orders)
|
||||
assert pdf[:5] == b"%PDF-"
|
||||
|
||||
|
||||
class TestPdfLayout:
|
||||
def test_typical_week_fits_on_one_page(self, readable_pdf):
|
||||
"""12 employees with 2 items each overflowed the pre-DEV-210 layout."""
|
||||
pdf = _build(_volume_orders(12))
|
||||
assert pdf[:5] == b"%PDF-"
|
||||
assert _NoCompressFPDF.last_pages_count == 1
|
||||
|
||||
def test_high_volume_still_paginates(self, readable_pdf):
|
||||
pdf = _build(_volume_orders(40))
|
||||
assert pdf[:5] == b"%PDF-"
|
||||
assert _NoCompressFPDF.last_pages_count >= 2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue