"""Generate the Colsys 'Objednávka' order form as a PDF, replicating the ERP layout, followed by the line-items table. Output is one combined PDF. Layout reference: samples/Objednavky_TU_2025_1576.PDF """ from __future__ import annotations import os from reportlab.lib.pagesizes import A4 from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas PAGE_W, PAGE_H = A4 # 595.27 x 841.89 pt L = 40 # left margin R = PAGE_W - 40 # right margin # ── Fonts (DejaVu = full Czech diacritics) ── _FONT, _BOLD = "Helvetica", "Helvetica-Bold" for name, path in ( ("DejaVu", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"), ("DejaVu-Bold", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"), ): if os.path.exists(path): try: pdfmetrics.registerFont(TTFont(name, path)) except Exception: pass if "DejaVu" in pdfmetrics.getRegisteredFontNames(): _FONT, _BOLD = "DejaVu", "DejaVu-Bold" def _ty(y: float) -> float: """Top-origin → reportlab bottom-origin.""" return PAGE_H - y def _wrap(c, text, font, size, max_w): text = str(text or "") if not text: return [""] out, line = [], "" for word in text.split(" "): trial = (line + " " + word).strip() if pdfmetrics.stringWidth(trial, font, size) <= max_w or not line: line = trial else: out.append(line) line = word out.append(line) return out def _money(v) -> str: if v is None or v == "": return "" try: s = f"{float(v):,.2f}" # CZ formatting: 1 234,56 return s.replace(",", " ").replace(".", ",") except (TypeError, ValueError): return str(v) def _qty(v) -> str: if v is None or v == "": return "" try: f = float(v) return str(int(f)) if f.is_integer() else f"{f:.3f}".rstrip("0").rstrip(".").replace(".", ",") except (TypeError, ValueError): return str(v) # ─────────────────────────── page 1: the form ─────────────────────────── def _party_box(c, x0, x1, top, label, party): """Draw one bordered party box (Objednatel / Dodavatel): the label on its own line, a blank row, then the company name + address below.""" name = party.get("name") or "" addr = party.get("address") or "" parts = [p.strip() for p in addr.split(",") if p.strip()] ico, dic = party.get("ico") or "", party.get("dic") or "" email = party.get("email") c.setFont(_FONT, 8) c.drawString(x0 + 6, _ty(top + 14), label) # blank row, then the company block on its own lines yy = top + 36 c.setFont(_BOLD, 9.5) c.drawString(x0 + 6, _ty(yy), name) yy += 13 c.setFont(_FONT, 8) for ln in parts: c.drawString(x0 + 6, _ty(yy), ln) yy += 11 idline = " ".join(s for s in (f"IČO: {ico}" if ico else "", f"DIČ: {dic}" if dic else "") if s) if idline: c.drawString(x0 + 6, _ty(yy + 2), idline) yy += 13 if email: c.drawString(x0 + 6, _ty(yy + 2), f"e-mail: {email}") yy += 12 return yy def _kv_two(c, y, lx, ll, lv, rx, rl, rv): c.setFont(_FONT, 8) c.drawString(lx, _ty(y), ll) c.setFont(_BOLD, 8) c.drawString(lx + 82, _ty(y), str(lv or "")) c.setFont(_FONT, 8) c.drawString(rx, _ty(y), rl) c.setFont(_BOLD, 8) c.drawString(rx + 92, _ty(y), str(rv or "")) def _draw_form_page(c, o): objednatel = o.get("objednatel") or {} dodavatel = o.get("dodavatel") or {} # Terms may arrive flat on the order or nested under "terms". _terms = o.get("terms") or {} terms = {k: (o.get(k) if o.get(k) not in (None, "") else _terms.get(k)) for k in ("zpusob_platby", "zpusob_odberu", "zpusob_dopravy", "forma_objednavky", "termin_dodani", "fakturujte")} num = o.get("objednavka_number") or "" # Title (with top breathing room) c.setFont(_BOLD, 14) c.drawRightString(R, _ty(44), f"Objednávka: {num}") # ── Header: two party boxes (with space above the boxes) ── top = 78 box_h = 104 mid = 300 c.setLineWidth(0.6) c.rect(L, _ty(top + box_h), R - L, box_h, stroke=1, fill=0) c.line(mid, _ty(top + box_h), mid, _ty(top)) _party_box(c, L, mid, top, "Objednatel", objednatel) _party_box(c, mid, R, top, "Dodavatel", dodavatel) # ── Dodací a platební podmínky ── t2 = top + box_h h2 = 60 c.rect(L, _ty(t2 + h2), R - L, h2, stroke=1, fill=0) c.setFont(_BOLD, 8) c.drawString(L + 4, _ty(t2 + 11), "Dodací a platební podmínky") yy = t2 + 26 _kv_two(c, yy, L + 4, "Způsob platby:", terms.get("zpusob_platby"), mid + 4, "Forma objednávky:", terms.get("forma_objednavky")) _kv_two(c, yy + 12, L + 4, "Způsob odběru:", terms.get("zpusob_odberu") or "-", mid + 4, "Termín dodání:", terms.get("termin_dodani")) _kv_two(c, yy + 24, L + 4, "Způsob dopravy:", terms.get("zpusob_dopravy") or "-", mid + 4, "Fakturujte:", terms.get("fakturujte")) # ── Datum vystavení / Popis ── t3 = t2 + h2 h3 = 22 c.rect(L, _ty(t3 + h3), R - L, h3, stroke=1, fill=0) c.line(mid, _ty(t3 + h3), mid, _ty(t3)) c.setFont(_FONT, 8) c.drawString(L + 4, _ty(t3 + 14), "Datum vystavení") c.setFont(_BOLD, 8) c.drawString(L + 95, _ty(t3 + 14), str(o.get("datum_vystaveni") or "")) c.setFont(_FONT, 8) c.drawString(mid + 4, _ty(t3 + 14), "Popis:") c.setFont(_BOLD, 8) c.drawString(mid + 50, _ty(t3 + 14), str(o.get("popis") or "")) # ── Sub-header: order no + page ── t4 = t3 + h3 + 16 c.setFont(_BOLD, 9) c.drawString(L, _ty(t4), f"Objednávka: {num}") c.drawRightString(R, _ty(t4), "Strana: 1/1") c.setLineWidth(0.6) c.line(L, _ty(t4 + 4), R, _ty(t4 + 4)) # ── Body fields ── y = t4 + 22 def field(label, value, gap_after=14, bold_label=True): nonlocal y c.setFont(_BOLD if bold_label else _FONT, 8.5) c.drawString(L, _ty(y), label) lw = pdfmetrics.stringWidth(label + " ", _BOLD, 8.5) c.setFont(_FONT, 8.5) for i, ln in enumerate(_wrap(c, value, _FONT, 8.5, R - L - lw)): c.drawString(L + (lw if i == 0 else 0), _ty(y), ln) if i > 0: y += 11 y += gap_after field("NÁZEV ZAKÁZKY:", o.get("nazev_zakazky")) field("KÓD ZAKÁZKY/NS:", o.get("kod_zakazky"), gap_after=20) field("TEXT:", o.get("text")) field("CENA CELKEM (bez DPH):", o.get("cena_celkem_text") or _money(o.get("cena_celkem")) + (" Kč" if o.get("cena_celkem") not in (None, "") else "")) field("TERMÍN DODÁNÍ:", o.get("termin_dodani_text"), gap_after=20) field("MÍSTO DODÁNÍ:", o.get("misto_dodani")) field("KONTAKTNÍ OSOBA V MÍSTĚ DODÁNÍ (jméno, tel, email):", o.get("kontaktni_osoba"), gap_after=20) field("SPLATNOST:", o.get("splatnost")) field("ZÁRUKA:", o.get("zaruka")) field("POZNÁMKA:", o.get("poznamka")) field("PŘÍLOHY:", o.get("prilohy")) _draw_footer(c, o) def _draw_footer(c, o): fy = 135 # pt from the bottom of the page; block is drawn downward # Datum / Vystavil c.setFont(_FONT, 8) c.drawString(L, fy, "Datum:") c.drawString(360, fy, "Vystavil:") c.setFont(_BOLD, 8) c.drawString(L, fy - 12, str(o.get("datum_vystaveni") or "")) c.drawString(360, fy - 12, str(o.get("vystavil") or "")) c.setFont(_BOLD, 8) c.drawString(L, fy - 30, "Faktury zasílejte na email adresu: faktury@colsys.cz") legal = ( "Dodavatel souhlasí a potvrzuje, že se seznámil jako: A) prodávající s " "Všeobecnými nákupními podmínkami (VNP), B) nebo jako zhotovitel s " "Všeobecnými smluvními podmínkami (VSP) společnosti Colsys s.r.o. platnými " "ke dni vystavení objednávky (viz http://www.colsys.cz/dokumenty-ke-stazeni). " "Ve fakturách uvádějte vždy číslo objednávky přesně (včetně velkých a malých " "písmen) a zasílejte je v elektronické formě emailem ve formátu isdoc nebo " "isdocx. Přílohou faktury je vždy odsouhlasený protokol o předání/převzetí " "(Dodací list, Předávací protokol, Zjišťovací protokol apod.)." ) c.setFont(_FONT, 6.5) yy = fy - 44 for ln in _wrap(c, legal, _FONT, 6.5, R - L): c.drawString(L, yy, ln) yy -= 8 c.drawString(L, yy - 2, "Zapsáno: odd.C, vl. 902 obchodního rejstříku vedeného u Městského soudu v Praze.") # ─────────────────────────── items table ─────────────────────────── _COLS = [ ("Název zboží", "name", 200, "left"), ("ID dodavatele", "supplier_id", 80, "left"), ("Množství", "quantity", 55, "right"), ("MJ", "unit", 35, "left"), ("J. cena", "unit_price", 70, "right"), ("Celkem", "total_price", 75, "right"), ] def _draw_items(c, o, items): c.showPage() num = o.get("objednavka_number") or "" y = 50 c.setFont(_BOLD, 12) c.drawString(L, _ty(y), "Položky objednávky") c.setFont(_FONT, 9) c.drawRightString(R, _ty(y), f"Objednávka: {num}") y += 22 def header(): nonlocal y x = L c.setFont(_BOLD, 8) c.setFillColorRGB(1, 1, 1) c.setStrokeColorRGB(0.15, 0.39, 0.92) c.setFillColorRGB(0.15, 0.39, 0.92) c.rect(L, _ty(y + 14), R - L, 16, stroke=0, fill=1) c.setFillColorRGB(1, 1, 1) for title, _k, w, align in _COLS: if align == "right": c.drawRightString(x + w - 4, _ty(y + 10), title) else: c.drawString(x + 4, _ty(y + 10), title) x += w c.setFillColorRGB(0, 0, 0) y += 18 header() c.setFont(_FONT, 8) total = 0.0 for it in items or []: # page break if y > PAGE_H - 80: c.showPage() y = 50 header() c.setFont(_FONT, 8) name_lines = _wrap(c, it.get("name"), _FONT, 8, _COLS[0][2] - 6) row_h = max(12, len(name_lines) * 10 + 2) x = L for title, k, w, align in _COLS: if k == "name": for i, ln in enumerate(name_lines): c.drawString(x + 4, _ty(y + 9 + i * 10), ln) else: v = it.get(k) if k in ("unit_price", "total_price"): s = _money(v) elif k == "quantity": s = _qty(v) else: s = str(v or "") if align == "right": c.drawRightString(x + w - 4, _ty(y + 9), s) else: c.drawString(x + 4, _ty(y + 9), s) x += w c.setStrokeColorRGB(0.85, 0.85, 0.85) c.setLineWidth(0.4) c.line(L, _ty(y + row_h), R, _ty(y + row_h)) y += row_h try: total += float(it.get("total_price") or 0) except (TypeError, ValueError): pass # total row y += 6 c.setFont(_BOLD, 9) c.drawString(L + 4, _ty(y + 9), "Celkem bez DPH") c.drawRightString(R - 4, _ty(y + 9), _money(total) + " Kč") def build_order_pdf(order: dict, items: list, out_path: str) -> str: c = canvas.Canvas(out_path, pagesize=A4) _draw_form_page(c, order) _draw_items(c, order, items) c.save() return out_path