Files
AI_portal/offer-to-order/order_form.py
2026-07-07 13:16:26 +02:00

331 lines
12 KiB
Python

"""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")) + ("" 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) + "")
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