331 lines
12 KiB
Python
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")) + (" 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
|