Files
KPK/kivy_printer.py
2026-06-23 15:20:56 +02:00

1995 lines
69 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.metrics import dp
from kivy.core.text import LabelBase
from kivy.clock import Clock
from kivy.uix.scatter import Scatter
from kivy.graphics import Color, Rectangle
from kivy.logger import Logger
from kivy.app import App
import socket
import textwrap
from pathlib import Path
import data
def open_printer_tcp(host: str, port: int = 9100, timeout: float = 3.0):
sock = socket.create_connection((host, port), timeout=timeout)
return Printer(sock)
def do_print_ucet(ucet, txt: str = ""):
app = App.get_running_app()
cfg = app.cfg
try:
printer = open_printer_tcp(cfg.bill_printer.split(":")[0], cfg.bill_printer.split(":")[1])
tisk_uctu(ucet, printer, txt, width=40)
printer.close()
except Exception as e:
...
Logger.error(f"TISK CHYBA: {e}")
# vlastní monospace font:
# LabelBase.register(name="Mono", fn_regular="DejaVuSansMono.ttf")
def print_storno_dummy(self, u_sec):
WIDTH = 40
print()
print("=" * WIDTH)
print("STORNO KUCHYŇ".center(WIDTH))
print("-" * WIDTH)
print(f"STŮL: {u_sec.stul}")
print("-" * WIDTH)
for p in u_sec.poloz:
qty = f"{abs(p.pocet)}/{p.delitel}" if p.delitel != 1 else f"{abs(p.pocet)}"
line = f"{qty:>4} {p.nazev}"
print(line[:WIDTH])
print("-" * WIDTH)
print("ZRUŠIT".center(WIDTH))
print("=" * WIDTH)
print()
def print_kitchen_dummy(self, u_print):
WIDTH = 40
print()
print("=" * WIDTH)
print("KUCHYŇ".center(WIDTH))
print("-" * WIDTH)
print(f"STŮL: {u_print.stul}")
print("-" * WIDTH)
for p in u_print.poloz:
qty = f"{p.pocet}/{p.delitel}" if p.delitel != 1 else f"{p.pocet}"
line = f"{qty:>4} {p.nazev}"
print(line[:WIDTH])
# MESSAGE PRO KUCHYNI
msg = ""
if p.zpravy:
msg = "\n".join(f" -{z}" for z in p.zpravy)
print(msg[:WIDTH])
print("-" * WIDTH)
print("=" * WIDTH)
print()
def print_ucet_dummy(self, u_print, txt: str="", currencytxt:str="Kč", kasutxt:data.KasUtxtRiadky=None):
print()
print("=" * 40)
print("=== ÚČET (DUMMY) ===")
print("=" * 40)
if txt!="":
print(txt)
# ---- HLAVIČKA ----
if kasutxt:
riadok = kasutxt["userhead1"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["userhead2"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["userhead3"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["userhead4"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["userhead5"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["userhead6"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["userhead7"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["userhead8"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["userhead9"]
if riadok:
print(f"{riadok.center(40)}")
print(f"Stůl: {u_print.stul}")
print(f"Číšník: {u_print.autor}")
print(f"Otevřen: {u_print.open_at}")
if u_print.closed_at:
print(f"Uzavřen: {u_print.closed_at}")
print("-" * 40)
# ---- POLOŽKY ----
total_items = 0.0
total_before_discount = 0.0
for p in u_print.poloz:
qty = p.pocet / (p.delitel or 1)
line_sum = qty * p.cena
line_before = qty * (p.cena_puv if getattr(p, "cena_puv", None) is not None else p.cena)
total_items += line_sum
total_before_discount += line_before
qty_txt = (
f"{p.pocet}/{p.delitel}"
if p.delitel and p.delitel != 1
else f"{p.pocet}"
)
print(f"{p.nazev}")
print(f" {qty_txt} × {p.cena:.2f} = {line_sum:.2f}")
print("-" * 40)
subtotal_for_print = total_before_discount if getattr(u_print, "discounts_prorated", False) else total_items
print(f"MEZISOUČET: {subtotal_for_print:.2f} {currencytxt}")
# ---- SLEVA ----
if getattr(u_print, "discount_abs", 0):
if u_print.discount_abs > 0:
print(f"SLEVA: -{u_print.discount_abs:.2f} {currencytxt}")
else:
print(f"PRIRAZKA: {-u_print.discount_abs:.2f} {currencytxt}")
if not getattr(u_print, "discounts_prorated", False):
total_items -= u_print.discount_abs
print("-" * 40)
print(f"K ZAPLACENÍ: {total_items:.2f} {currencytxt}")
# ---- PLATBY ----
if getattr(u_print, "platby", None):
print("-" * 40)
print("PLATBY:")
paid = 0.0
for pay in u_print.platby:
rate = getattr(pay, "rate", 1.0) or 1.0
czk_value = pay.suma * rate
# Výpis
if pay.unit != currencytxt:
print(
f" {pay.nazev}: {pay.suma:.2f} {pay.unit}"
f" ({czk_value:.2f} {currencytxt})"
)
else:
print(f" {pay.nazev}: {pay.suma:.2f} {pay.unit}")
if getattr(pay, "tip", 0):
print(f" TIP: {float(pay.tip):.2f} {currencytxt}")
paid += czk_value
print("-" * 40)
print(f"ZAPLACENO: {paid:.2f} {currencytxt}")
change = paid - total_items
if change > 0:
print(f"VRÁCENO: {change:.2f} {currencytxt}")
print("=" * 40)
if kasutxt:
riadok = kasutxt["usertail1"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["usertail2"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["usertail3"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["usertail4"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["usertail5"]
if riadok:
print(f"{riadok.center(40)}")
riadok = kasutxt["usertail6"]
if riadok:
print(f"{riadok.center(40)}")
print("=== KONEC ÚČTU ===")
print("=" * 40)
print()
def format_uctu_str( ucet, width: int = 40, txt: str = "") -> str:
return "\n".join(format_uctu_text(ucet, width, txt))
def parse_dotaz_st(poznamka: str) -> list[tuple[str, str]]:
if not poznamka or "dotaz_st:" not in poznamka:
return []
text = poznamka.split("dotaz_st:", 1)[1].strip()
fields = [
"akcia",
"hjmeno",
"adresa1",
"adresa2",
"adresa3",
"ico",
"dic",
"icdph",
"schvalil",
]
result = []
for i, field in enumerate(fields):
start = text.find(field)
if start == -1:
continue
value_start = start + len(field)
next_positions = [
text.find(next_field, value_start)
for next_field in fields[i + 1:]
if text.find(next_field, value_start) != -1
]
value_end = min(next_positions) if next_positions else len(text)
value = text[value_start:value_end].strip(" ,")
if value:
result.append((field, value))
return result
def parse_dotaz_ho(poznamka: str) -> list[tuple[str, str]]:
if not poznamka or "dotaz_ho:" not in poznamka:
return []
text = poznamka.split("dotaz_ho:", 1)[1].strip()
fields = [
"izba",
"host",
"skupina",
"recepcia",
]
result = []
for i, field in enumerate(fields):
start = text.find(field)
if start == -1:
continue
value_start = start + len(field)
next_positions = [
text.find(next_field, value_start)
for next_field in fields[i + 1:]
if text.find(next_field, value_start) != -1
]
value_end = min(next_positions) if next_positions else len(text)
value = text[value_start:value_end].strip(" ,")
if value:
result.append((field, value))
return result
def format_uctu_text(ucet, width: int = 40, txt: str="", currencytxt:str="Kč", kasutxt:data.KasUtxtRiadky=None) -> list[str]:
lines: list[str] = []
if kasutxt:
riadok = kasutxt["userhead1"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["userhead2"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["userhead3"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["userhead4"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["userhead5"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["userhead6"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["userhead7"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["userhead8"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["userhead9"]
if riadok:
lines.append(riadok.center(width))
def line(left: str = "", right: str = ""):
#Zarovná left vlevo a right doprava na pevnou šířku
if right:
space = width - len(left) - len(right)
if space < 1:
space = 1
lines.append((left + " " * space + right)[:width])
else:
lines.append(left[:width])
def sep(ch: str = "-"):
lines.append(ch * width)
discount = round(getattr(ucet, "discount_abs", 0.0) or 0.0, 2)
# ================= HLAVIČKA =================
sep("=")
if txt != "":
dt = txt
lines.append(dt.center(width))
dt = ucet.closed_at or ucet.datetime or ""
lines.append(dt.center(width))
cislo = ucet.ucislo or "-"
stul = ucet.stul
lines.append(f"UCET {cislo}/ Stul {stul}".center(width))
# STORNO OZNAČENÍ
if getattr(ucet, "is_storno", None):
lines.append(" STORNO ".center(width))
if ucet.autor:
lines.append(f"Obsluha: {ucet.autor}".center(width))
sep("=")
# ================= POLOŽKY =================
total = 0.0
total_before_discount = 0.0
for p in ucet.poloz:
# je to dělená porce?
delene = bool(p.delitel and p.delitel != 1)
if delene:
kusu = p.pocet / p.delitel
qty_txt = f"{p.pocet}×1/{p.delitel}" # např. 2×1/2
else:
kusu = p.pocet
qty_txt = str(p.pocet)
cena = round(p.cena * kusu, 2)
cena_before = round((p.cena_puv if getattr(p, "cena_puv", None) is not None else p.cena) * kusu, 2)
total += cena
total_before_discount += cena_before
# ====== JEDEN ŘÁDEK jen NEDĚLENÝ 1 KS ======
if not delene and kusu == 1:
left = p.nazev
right = f"{cena:.2f}"
line(left.ljust(width - len(right)) + right)
# ====== DĚLENÉ NEBO VÍCE KS → VŽDY DVA ŘÁDKY ======
else:
line(p.nazev[:width])
left = f"{qty_txt} x {p.cena:.2f}"
right = f"{cena:.2f}"
line(left.ljust(width - len(right)) + right)
# ================= SOUČET =================
sep("-")
prorated = getattr(ucet, "discounts_prorated", False)
payable = total if prorated else total - discount
subtotal_for_print = total_before_discount if prorated else total
line("MEZISOUČET", f"{subtotal_for_print:.2f} {currencytxt}")
if discount != 0:
if discount > 0:
line("SLEVA", f"-{discount:.2f} {currencytxt}")
else:
line("PRIRAZKA", f"{-discount:.2f} {currencytxt}")
sep("-")
line("K ÚHRADĚ", f"{payable:.2f} {currencytxt}")
else:
line("K ÚHRADĚ", f"{payable:.2f} {currencytxt}")
# ================= DPH =================
sep("-")
for d in ucet.dane:
# d.rate např. "0.21"
# Petr 11.5.
parts = d.rate.split(".", 1)
pct = parts[1] if len(parts) > 1 else parts[0]
# Petr 11.5.
pct = pct.ljust(2, "0")
zaklad = round(d.zaklad, 2)
dph_castka = round(d.zaklad * float(d.rate), 2)
left = f"DPH {pct} % Zaklad {zaklad:.2f}"
right = f"{dph_castka:.2f}"
line(left, right)
# ================= PLATBY =================
if getattr(ucet, "platby", None):
sep("-")
for p in ucet.platby:
if p.unit != currencytxt and p.rate != 1:
left = f"{p.nazev} {p.suma:.2f} {p.unit}"
right = f"{p.suma * p.rate:.2f}"
else:
left = p.nazev
right = f"{p.suma:.2f}"
line(left, right)
for key, value in parse_dotaz_st(getattr(p, "poznamka", "")):
line(key.capitalize(), value)
for key, value in parse_dotaz_ho(getattr(p, "poznamka", "")):
line(key.capitalize(), value)
if getattr(p, "tip", 0):
line("TIP", f"{float(p.tip):.2f}")
sep("=")
if kasutxt:
riadok = kasutxt["usertail1"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["usertail2"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["usertail3"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["usertail4"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["usertail5"]
if riadok:
lines.append(riadok.center(width))
riadok = kasutxt["usertail6"]
if riadok:
lines.append(riadok.center(width))
for i in range(5):
line("","")
return lines
def normalize_cp852(text: str) -> str:
replacements = {
"—": "-", "": "-", "": "-",
"“": '"', "”": '"', "„": '"',
"": "'", "": "'",
"…": "...",
"\u00a0": " ", # NBSP
"č": "c", "ě": "e", "š": "s", "ř": "r",
"ž": "z", "ý": "y", "á": "a", "í": "i",
"é": "e", "ú": "u", "ů": "u",
}
for k, v in replacements.items():
text = text.replace(k, v)
return text
def tisk_kitchen(
ucet,
printer,
txt,
*,
width: int = 40,
init_cmd: bytes = b"\x1b@",
cut_cmd: bytes = b"\x1dV\x00",
):
def line(char="-"):
txt(printer, char * width)
printer.write(init_cmd)
line("=")
txt(printer, "KUCHYŇ".center(width))
line("=")
txt(printer, f"STŮL: {ucet.stul}")
line()
for p in ucet.poloz:
qty = f"{p.pocet}/{p.delitel}" if p.delitel != 1 else str(p.pocet)
name = p.nazev
qty_field = f"{qty:>5}"
txt(printer, f"{qty_field} {name}")
# ---- CHOD ----
if getattr(p, "chod", ""):
txt(printer, f" [CHOD {p.chod}]")
# ---- ZPRÁVY ----
if getattr(p, "zpravy", None):
for z in p.zpravy:
txt(printer, f" • {z}")
line()
printer.write(cut_cmd)
def tisk_uctu( ucet, printer, txt, *, width: int = 40,
init_cmd: bytes = b"\x1b@",
cut_cmd: bytes = b"\x1dV\x00",
):
# inicializace tiskárny
if init_cmd:
printer.write(init_cmd)
for l in format_uctu_text(ucet, width, txt):
safe = normalize_cp852(l)
printer.write((safe + "\n").encode("cp852", errors="replace"))
# odřez
if cut_cmd:
printer.write(cut_cmd)
class Printer:
def __init__(self, stream):
self.stream = stream
def write(self, data: bytes):
if hasattr(self.stream, "sendall"):
self.stream.sendall(data) # socket
else:
self.stream.write(data) # file/serial
def close(self):
if hasattr(self.stream, "close"):
self.stream.close()
class ConsolePrinter(Printer):
def write(self, data: bytes):
data = data.replace(b"\x1b", b"<ESC>")
print(data.decode("ascii", errors="replace"), end="")
def show_receipt_preview(ucet, width: int = 40, txt: str = "", printer=None, currencytxt:str ="Kč", kasutxt:data.KasUtxtRiadky=None):
# ROOT
root = BoxLayout(
orientation="vertical",
spacing=dp(10),
padding=dp(10),
)
# tmavé pozadí
with root.canvas.before:
Color(0.08, 0.08, 0.08, 1)
bg = Rectangle(pos=root.pos, size=root.size)
root.bind(pos=lambda i, v: setattr(bg, "pos", v))
root.bind(size=lambda i, v: setattr(bg, "size", v))
# SCROLL OBLAST BEZ ZALAMOVÁNÍ
receipt_text = "\n".join(format_uctu_text(ucet, width, txt, currencytxt, kasutxt=kasutxt))
scroll = ScrollView(
do_scroll_x=True, # horizontální scroll povolen
do_scroll_y=True,
bar_width=dp(8),
size_hint=(1, 1),
)
receipt = Label(
text=receipt_text,
font_name="RobotoMono-Regular",
font_size=dp(14), # menší font
halign="left",
valign="top",
size_hint=(None, None),
color=(0.9, 0.9, 0.9, 1),
)
receipt.bind(
texture_size=lambda inst, size: setattr(inst, "size", size)
)
scroll.add_widget(receipt)
root.add_widget(scroll)
# TLAČÍTKA
bottom = BoxLayout(
size_hint_y=None,
height=dp(70), # stejná výška jako jinde
spacing=dp(10),
)
btn_print = Button(
text="TISK",
size_hint=(1, 1),
)
btn_close = Button(
text="ZPĚT",
size_hint=(1, 1),
)
bottom.add_widget(btn_print)
bottom.add_widget(btn_close)
root.add_widget(bottom)
popup = Popup(
title="Náhled účtu",
title_color=(1, 1, 1, 1),
separator_color=(0.3, 0.3, 0.3, 1),
content=root,
size_hint=(0.95, 0.95), # automaticky reaguje na velikost okna
auto_dismiss=False,
)
btn_print.bind(on_press=lambda *_: do_print_ucet(ucet))
btn_close.bind(on_press=popup.dismiss)
popup.open()
def show_receipt_preview(
ucet,
width: int = 40,
txt: str = "",
printer=None,
currencytxt: str = "Kc",
kasutxt: data.KasUtxtRiadky = None,
receipt_text: str | None = None,
on_print=None,
print_label: str = "TLAC",
):
root = BoxLayout(
orientation="vertical",
spacing=dp(10),
padding=dp(10),
)
with root.canvas.before:
Color(0.08, 0.08, 0.08, 1)
bg = Rectangle(pos=root.pos, size=root.size)
root.bind(pos=lambda i, v: setattr(bg, "pos", v))
root.bind(size=lambda i, v: setattr(bg, "size", v))
if receipt_text is None:
receipt_text = "\n".join(format_uctu_text(ucet, width, txt, currencytxt, kasutxt=kasutxt))
scroll = ScrollView(
do_scroll_x=True,
do_scroll_y=True,
bar_width=dp(8),
size_hint=(1, 1),
)
receipt = Label(
text=receipt_text,
font_name="RobotoMono-Regular",
font_size=dp(14),
halign="left",
valign="top",
size_hint=(None, None),
color=(0.9, 0.9, 0.9, 1),
)
receipt.bind(texture_size=lambda inst, size: setattr(inst, "size", size))
scroll.add_widget(receipt)
root.add_widget(scroll)
bottom = BoxLayout(
size_hint_y=None,
height=dp(70),
spacing=dp(10),
)
btn_print = Button(
text=print_label,
size_hint=(1, 1),
disabled=not callable(on_print),
)
btn_close = Button(
text="SPAT",
size_hint=(1, 1),
)
bottom.add_widget(btn_print)
bottom.add_widget(btn_close)
root.add_widget(bottom)
popup = Popup(
title="Nahlad uctu",
title_color=(1, 1, 1, 1),
separator_color=(0.3, 0.3, 0.3, 1),
content=root,
size_hint=(0.95, 0.95),
auto_dismiss=False,
)
def do_preview_print(*_):
if not callable(on_print):
return
try:
on_print()
popup.dismiss()
except Exception as e:
Logger.exception(f"Nahled uctu: tlac zlyhala: {e}")
btn_print.bind(on_press=do_preview_print)
btn_close.bind(on_press=popup.dismiss)
popup.open()
def _format_clsrep_text_legacy(clsrep: dict, width: int = 40) -> list[str]:
lines: list[str] = []
def line(left: str = "", right: str = ""):
if right:
space = width - len(left) - len(right)
if space < 1:
space = 1
lines.append((left + " " * space + right)[:width])
else:
lines.append(left[:width])
def sep(ch: str = "-"):
lines.append(ch * width)
interval = clsrep.get("interval", {})
summary = clsrep.get("summary", {})
platby = clsrep.get("platby", {})
uzivatele = clsrep.get("uzivatele", {})
dph = clsrep.get("dph", {})
# ===== HLAVIČKA =====
sep("=")
clsrep_no = clsrep.get("clsrep_no") or "-"
created_at = clsrep.get("created_at") or ""
lines.append(f"UZAVERKA {clsrep_no}".center(width))
if created_at:
lines.append(created_at.center(width))
sep("=")
interval = clsrep.get("interval", {})
line("Od ucet", interval.get("ucislo_od", ""))
line("Do ucet", interval.get("ucislo_do", ""))
line("Od cas", interval.get("closed_at_od", ""))
line("Do cas", interval.get("closed_at_do", ""))
sep("-")
line("Pocet uctu", str(summary.get("pocet_uctu", 0)))
line("TOTAL BASE", f"{summary.get('total_base_currency', 0.0):.2f}")
line("TOTAL PLATBY", f"{summary.get('total_payments', 0.0):.2f}")
diff = summary.get("difference", 0.0)
if abs(diff) > 0.01:
sep("-")
line("ROZDIL", f"{diff:.2f}")
# ===== PLATBY =====
sep("=")
lines.append(" PLATBY ".center(width))
sep("=")
for code, suma in platby.items():
line(code, f"{suma:.2f}")
# ===== UZIVATELE =====
sep("=")
lines.append(" OBSLUHA ".center(width))
sep("=")
for user, data in uzivatele.items():
line(user)
line(" Celkem", f"{data.get('total_base_currency',0):.2f}")
line(" Hotovost", f"{data.get('hotovost',0):.2f}")
sep("-")
# ===== DPH =====
sep("=")
lines.append(" DPH ".center(width))
sep("=")
for rate, data in dph.items():
left = f"DPH {rate}"
right = f"{data.get('celkem',0):.2f}"
line(left, right)
line(" Zaklad", f"{data.get('zaklad',0):.2f}")
line(" Dan", f"{data.get('dan',0):.2f}")
sep("-")
sep("=")
for _ in range(5):
line()
# OTEVŘENÉ ÚČTY
open_ucty = clsrep.get("open_ucty") or []
if open_ucty:
sep("=")
lines.append(" OTEVRENE UCTY ".center(width))
sep("=")
total_open = 0.0
for u in open_ucty:
ucislo = u.get("ucislo") or "-"
stul = u.get("stul") or ""
autor = u.get("autor") or ""
open_at = u.get("open_at") or ""
blocked_by = u.get("blocked_by") or ""
total = float(u.get("total_base_currency") or 0.0)
total_open += total
# 1. řádek: stůl + ucislo
line(f"Stul {stul}", f"Ucet {ucislo}")
# 2. řádek: autor + open time
left = (autor or "UNKNOWN")
right = (open_at or "")
line(left, right)
# 3. řádek: blokace (když je)
if blocked_by:
line("Blok:", blocked_by)
# 4. řádek: total
line("Celkem", f"{total:.2f}")
sep("-")
line("CELKEM OTEVRENO", f"{total_open:.2f}")
sep("-")
return lines
def _format_clsrep_text_v1(clsrep: dict, width: int = 40) -> list[str]:
lines: list[str] = []
def as_float(value, default: float = 0.0) -> float:
try:
return float(value or default)
except Exception:
return default
def line(left: str = "", right: str = ""):
left = str(left or "")
right = str(right or "")
if right:
space = width - len(left) - len(right)
if space < 1:
space = 1
lines.append((left + " " * space + right)[:width])
else:
lines.append(left[:width])
def sep(ch: str = "-"):
lines.append(ch * width)
interval = clsrep.get("interval", {}) or {}
summary = clsrep.get("summary", {}) or {}
platby = clsrep.get("platby", {}) or {}
uzivatele = clsrep.get("uzivatele", {}) or {}
dph = clsrep.get("dph", {}) or {}
settings = clsrep.get("closure_settings", {}) or {}
cash_state = clsrep.get("cash_state", []) or []
warnings = clsrep.get("warnings", []) or []
sep("=")
clsrep_no = clsrep.get("clsrep_no") or "-"
created_at = clsrep.get("created_at") or ""
lines.append(f"UZAVERKA {clsrep_no}".center(width))
if created_at:
lines.append(str(created_at).center(width))
sep("=")
line("Od ucet", interval.get("ucislo_od", ""))
line("Do ucet", interval.get("ucislo_do", ""))
line("Od cas", interval.get("closed_at_od", ""))
line("Do cas", interval.get("closed_at_do", ""))
if settings:
line("Odvod", settings.get("uzav_odvod", ""))
if settings.get("men_sp_man"):
line("Men.skladu", settings.get("men_sp_man", ""))
sep("-")
line("Pocet uctu", str(summary.get("pocet_uctu", 0)))
line("Suma uctov", f"{as_float(summary.get('total_base_currency')):.2f}")
line("Suma platieb", f"{as_float(summary.get('total_payments')):.2f}")
diff = as_float(summary.get("difference"))
if abs(diff) > 0.01:
sep("-")
line("Rozdiel", f"{diff:.2f}")
sep("=")
lines.append(" PLATBY ".center(width))
sep("=")
for code, suma in platby.items():
line(code, f"{as_float(suma):.2f}")
if cash_state:
sep("=")
lines.append(" STAV PLATIDIEL ".center(width))
sep("=")
for row in cash_state:
title = row.get("payment_name") or row.get("payment_code") or "-"
prn_no = row.get("prn_no") or "-"
line(str(title)[:28], f"PRN {prn_no}")
line(" Zaciatok", f"{as_float(row.get('opening_amount')):.2f}")
line(" Trzba", f"{as_float(row.get('sales_amount')):.2f}")
receivable = as_float(row.get("receivable_amount"))
if abs(receivable) >= 0.005:
line(" Pohladavky", f"{receivable:.2f}")
deposit = as_float(row.get("manual_deposit_amount"))
withdrawal = as_float(row.get("manual_withdrawal_amount"))
if abs(deposit) >= 0.005:
line(" Vklady", f"{deposit:.2f}")
if abs(withdrawal) >= 0.005:
line(" Vybery", f"{withdrawal:.2f}")
auto_withdrawal = as_float(row.get("auto_withdrawal_amount"))
if abs(auto_withdrawal) >= 0.005:
line(" Auto vyber", f"{auto_withdrawal:.2f}")
line(" Prenos", f"{as_float(row.get('carry_amount')):.2f}")
status = row.get("status") or ""
if status:
line(" Stav", status)
error = row.get("error") or ""
if error:
line(" Chyba", str(error)[:width - 8])
sep("-")
sep("=")
lines.append(" OBSLUHA ".center(width))
sep("=")
for user, user_data in uzivatele.items():
line(user)
line(" Celkem", f"{as_float(user_data.get('total_base_currency')):.2f}")
line(" Hotovost", f"{as_float(user_data.get('hotovost')):.2f}")
sep("-")
sep("=")
lines.append(" DPH ".center(width))
sep("=")
for rate, vat_row in dph.items():
line(f"DPH {rate}", f"{as_float(vat_row.get('celkem')):.2f}")
line(" Zaklad", f"{as_float(vat_row.get('zaklad')):.2f}")
line(" Dan", f"{as_float(vat_row.get('dan')):.2f}")
sep("-")
open_ucty = clsrep.get("open_ucty") or []
if open_ucty:
sep("=")
lines.append(" OTVORENE UCTY ".center(width))
sep("=")
total_open = 0.0
for u in open_ucty:
ucislo = u.get("ucislo") or "-"
stul = u.get("stul") or ""
autor = u.get("autor") or ""
open_at = u.get("open_at") or ""
blocked_by = u.get("blocked_by") or ""
total = as_float(u.get("total_base_currency"))
total_open += total
line(f"Stol {stul}", f"Ucet {ucislo}")
line(autor or "UNKNOWN", open_at)
if blocked_by:
line("Blok", blocked_by)
line("Celkem", f"{total:.2f}")
sep("-")
line("CELKEM OTVORENE", f"{total_open:.2f}")
sep("-")
if warnings:
sep("=")
lines.append(" UPOZORNENIA ".center(width))
sep("=")
for warning in warnings:
line(str(warning))
return lines
def _clsrep_as_dict(value):
if hasattr(value, "model_dump"):
return value.model_dump()
return value if isinstance(value, dict) else {}
def _clsrep_float(value, default: float = 0.0) -> float:
try:
return float(value or default)
except Exception:
return default
def _clsrep_indexed(rows: list[dict]) -> dict[str, dict]:
return {str(idx): row for idx, row in enumerate(rows or [], start=1)}
def _closure_template_candidates() -> list[Path]:
base = Path(__file__).resolve().parent
templates = base / "templates"
return [
templates / "TP-closure_default.jinja2",
templates / "TP-closure_sk.jinja2",
base / "TP-closure_sk.jinja2",
]
def _resolve_closure_template() -> tuple[str, str] | None:
for path in _closure_template_candidates():
try:
if path.exists():
return path.read_text(encoding="utf-8"), path.name
except Exception as exc:
Logger.warning(f"CLOSURE TEMPLATE LOAD FAILED {path}: {exc}")
return None
def _closure_flag(settings: dict, name: str, default: bool = False) -> bool:
flags = settings.get("flags") if isinstance(settings, dict) else {}
if isinstance(flags, dict) and name in flags:
return bool(flags.get(name))
return default
def _closure_section_rows(sections: dict, name: str) -> list[dict]:
rows = sections.get(name) if isinstance(sections, dict) else []
return [row for row in (rows or []) if isinstance(row, dict)]
def _legacy_closure_section_data(rows: list[dict]) -> dict[str, dict]:
return _clsrep_indexed(rows)
def _build_legacy_closure_context(clsrep: dict, width: int = 40) -> dict:
clsrep = _clsrep_as_dict(clsrep)
interval = _clsrep_as_dict(clsrep.get("interval"))
summary = _clsrep_as_dict(clsrep.get("summary"))
settings = _clsrep_as_dict(clsrep.get("closure_settings"))
sections = _clsrep_as_dict(clsrep.get("sections"))
cash_state = [row for row in (clsrep.get("cash_state") or []) if isinstance(row, dict)]
dph = _clsrep_as_dict(clsrep.get("dph"))
open_ucty = [row for row in (clsrep.get("open_ucty") or []) if isinstance(row, dict)]
def money_value(value) -> float:
return round(_clsrep_float(value), 2)
def add_section(target: dict, name: str, rows: list[dict]):
if rows:
target[str(len(target) + 1)] = {
"meno": name,
"data": _legacy_closure_section_data(rows),
}
sekcie: dict[str, dict] = {}
payment_names = {
str(row.get("payment_code") or ""): str(row.get("payment_name") or row.get("payment_code") or "")
for row in cash_state
if row.get("payment_code")
}
if _closure_flag(settings, "t_uz_cenhl", False):
add_section(sekcie, "Tržby po cenových hladinách", [
{
"cen_hlad": row.get("code") or row.get("name") or "-",
"prachy_puv": money_value(row.get("amount_original", row.get("amount"))),
"prachy": money_value(row.get("amount")),
}
for row in _closure_section_rows(sections, "price_levels")
])
if _closure_flag(settings, "t_uz_drpl", True):
rows = []
payment_rows = _closure_section_rows(sections, "payments_by_code")
if not payment_rows:
platby = _clsrep_as_dict(clsrep.get("platby"))
payment_rows = [
{
"code": code,
"name": payment_names.get(str(code), str(code)),
"amount": platby.get(code),
"amount_original": platby.get(code),
"tip": 0.0,
}
for code in sorted(platby)
]
for row in payment_rows:
code = str(row.get("code") or "")
rows.append({
"druh_pl": code,
"popis": row.get("name") or payment_names.get(code, code),
"cena_pl": money_value(row.get("amount")),
"cena_pl_puv": money_value(row.get("amount_original", row.get("amount"))),
"prachy": money_value(row.get("amount")),
"prachy_puv": money_value(row.get("amount_original", row.get("amount"))),
"tip": money_value(row.get("tip")),
})
add_section(sekcie, "Tržby po druhoch platby", rows)
if _closure_flag(settings, "t_uz_mena", False):
add_section(sekcie, "Tržby po menách", [
{
"mena": row.get("code") or row.get("name") or "-",
"cena_pl": money_value(row.get("base_amount")),
"cena_mena": money_value(row.get("amount")),
}
for row in _closure_section_rows(sections, "currency_payments")
])
if _closure_flag(settings, "t_uz_fisk_platby", False):
add_section(sekcie, "Tržby za fiškálne platby", [
{
"popis": row.get("name") or row.get("code") or "-",
"cena_pl": money_value(row.get("amount")),
"cena_pl_puv": money_value(row.get("amount_original", row.get("amount"))),
"prachy": money_value(row.get("amount")),
"prachy_puv": money_value(row.get("amount_original", row.get("amount"))),
"tip": money_value(row.get("tip")),
}
for row in _closure_section_rows(sections, "fiscal_payments")
])
if _closure_flag(settings, "t_uz_poh_drpl", False):
add_section(sekcie, "Úhrady pohľadávok", [
{
"druh_pl": row.get("name") or row.get("code") or "-",
"username": "",
"cena_pl": money_value(row.get("amount")),
"cena_pl_puv": money_value(row.get("amount_original", row.get("amount"))),
}
for row in _closure_section_rows(sections, "receivables_by_payment")
])
if _closure_flag(settings, "t_uz_man", False):
add_section(sekcie, "Tržby po manageroch", [
{
"id_zkratka": row.get("name") or row.get("code") or "-",
"prachy_puv": money_value(row.get("amount_original", row.get("amount"))),
"prachy": money_value(row.get("amount")),
}
for row in _closure_section_rows(sections, "managers")
])
if _closure_flag(settings, "t_uz_man_dph", False):
add_section(sekcie, "Tržby po manageroch a daniach", [
{
"id_zkratka": row.get("name") or row.get("code") or "-",
"dan_sazba": _clsrep_float(row.get("rate")),
"zaklad": _clsrep_float(row.get("zaklad")),
"dan": money_value(row.get("dan")),
"prachy": money_value(row.get("celkem")),
}
for row in _closure_section_rows(sections, "managers_by_vat")
])
if _closure_flag(settings, "t_uz_spdph", False):
add_section(sekcie, "Tržby po platbách, manageroch a daniach", [
{
"id_zkratka": row.get("name") or row.get("code") or "-",
"druh_pl": row.get("payment_code") or "",
"dan_sazba": _clsrep_float(row.get("rate")),
"zaklad": _clsrep_float(row.get("zaklad")),
"dan": money_value(row.get("dan")),
"prachy": money_value(row.get("celkem")),
}
for row in _closure_section_rows(sections, "managers_payments_by_vat")
])
if _closure_flag(settings, "t_uz_odovzdanie", True):
rows = []
for row in cash_state:
prn_no = str(row.get("prn_no") or "").strip() or None
name = str(row.get("payment_name") or row.get("payment_code") or "-")
code = str(row.get("payment_code") or "")
odovzdat = str(row.get("payment_odovzdat") or "").strip()
if not odovzdat:
continue
for field, label, typ, operation, sign in [
("opening_amount", "Zo včera", 2, 2, 1),
("sales_amount", "Tržba", 0, 0, 1),
("receivable_amount", "Úhrada", 1, 0, 1),
("manual_deposit_amount", "Vklady", 2, 0, 1),
("manual_withdrawal_amount", "Výbery", 2, 1, -1),
("auto_withdrawal_amount", "Uzávierka", 2, 3, 1),
("carry_amount", "Prenos", 2, 4, -1),
]:
amount = money_value(row.get(field))
if abs(amount) < 0.005:
continue
rows.append({
"odovzdat": odovzdat,
"druh_pl": odovzdat,
"payment_code": code,
"operacia": operation,
"typ": typ,
"prn_no": prn_no,
"j0": name,
"suma": money_value(amount * sign),
})
add_section(sekcie, "Na odovzdanie", rows)
if _closure_flag(settings, "t_uz_dph", True):
add_section(sekcie, "Tržby po DPH", [
{
"dan_sazba": _clsrep_float(rate),
"zaklad": _clsrep_float(row.get("zaklad")),
"dan": money_value(row.get("dan")),
"prachy": money_value(row.get("celkem")),
"round50": 0.0,
}
for rate, row in dph.items()
if isinstance(row, dict)
])
if _closure_flag(settings, "t_uz_dph_fis", False):
add_section(sekcie, "Tržby po DPH - fiškálne platby", [
{
"dan_sazba": _clsrep_float(row.get("rate")),
"zaklad": _clsrep_float(row.get("zaklad")),
"dan": money_value(row.get("dan")),
"prachy": money_value(row.get("celkem")),
"round50": 0.0,
}
for row in _closure_section_rows(sections, "fiscal_payments_by_vat")
])
if _closure_flag(settings, "t_uz_drpldan", False):
add_section(sekcie, "Tržby po platbách a daniach", [
{
"druh_pl": row.get("payment_code") or "",
"dan_sazba": _clsrep_float(row.get("rate")),
"zaklad": _clsrep_float(row.get("zaklad")),
"dan": money_value(row.get("dan")),
"prachy": money_value(row.get("celkem")),
"round50": 0.0,
}
for row in _closure_section_rows(sections, "payments_by_vat")
])
if _closure_flag(settings, "t_uz_drplfisdan", False):
add_section(sekcie, "Tržby po fiškálnych platbách a daniach", [
{
"druh_pl": row.get("payment_code") or "",
"dan_sazba": _clsrep_float(row.get("rate")),
"zaklad": _clsrep_float(row.get("zaklad")),
"dan": money_value(row.get("dan")),
"prachy": money_value(row.get("celkem")),
"round50": 0.0,
}
for row in _closure_section_rows(sections, "fiscal_payments_by_vat")
])
if _closure_flag(settings, "t_uz_terminal", False):
add_section(sekcie, "Platby terminálom", [
{
"prn_name": row.get("name") or row.get("code") or "-",
"suma": money_value(row.get("amount")),
}
for row in _closure_section_rows(sections, "terminal_payments")
])
if _closure_flag(settings, "t_uz_trzdr", False):
add_section(sekcie, "Tržby po druhoch", [
{
"druh": row.get("name") or row.get("code") or "-",
"mnozstvi": _clsrep_float(row.get("qty")),
"prachy_puv": money_value(row.get("amount_original", row.get("amount"))),
"prachy": money_value(row.get("amount")),
}
for row in _closure_section_rows(sections, "items_by_kind")
])
if _closure_flag(settings, "t_uz_harek", False):
add_section(sekcie, "Spotreba", [
{
"id_zkratka": row.get("id_zkratka") or row.get("sklad") or "",
"nazev": row.get("name") or row.get("code") or "-",
"druh": row.get("druh") or "",
"spart": row.get("spart") or "",
"cen_hlad": row.get("cen_hlad") or "",
"mnozstvi": _clsrep_float(row.get("qty")),
"jc": money_value(row.get("jc")),
"ciastka": money_value(row.get("amount")),
"dph": row.get("dph") or "",
}
for row in _closure_section_rows(sections, "items_sold")
])
if _closure_flag(settings, "t_uz_casni", True):
add_section(sekcie, "Tržby po čašníkoch", [
{
"username": row.get("name") or row.get("code") or "-",
"cena_pl": money_value(row.get("amount")),
"prachy_puv": money_value(row.get("amount_original", row.get("amount"))),
"prachy": money_value(row.get("amount")),
}
for row in _closure_section_rows(sections, "receipt_counts_by_user")
])
if _closure_flag(settings, "t_uz_cshot", False):
add_section(sekcie, "Tržby po čašníkoch v hotovosti", [
{
"username": row.get("username") or row.get("name") or row.get("code") or "-",
"cena_pl": money_value(row.get("amount")),
"prachy": money_value(row.get("amount")),
}
for row in _closure_section_rows(sections, "cashiers_cash")
])
if _closure_flag(settings, "t_uz_puctu_cas", False):
add_section(sekcie, "Tržby po čašníkoch a druhoch platieb", [
{
"username": row.get("username") or row.get("autor") or "",
"autor": row.get("autor") or "",
"cena_pl": money_value(row.get("amount")),
"cena_pl_puv": money_value(row.get("amount_original", row.get("amount"))),
"druh_pl": row.get("code") or "",
}
for row in _closure_section_rows(sections, "payments_by_user")
])
if _closure_flag(settings, "t_uz_vklad_vyber", False):
add_section(sekcie, "Sumár vkladov a výberov", [
{
"datum": row.get("closed_at") or "",
"username": row.get("autor") or "",
"popis": row.get("operation_label") or "",
"ciastka": money_value(row.get("amount")),
}
for row in _closure_section_rows(sections, "cash_operations")
])
if _closure_flag(settings, "t_uz_stoly", True):
add_section(sekcie, "Otvorené stoly", [
{
"miestnost": row.get("room_name") or "",
"stol": row.get("stul") or row.get("ucislo") or "",
"suma": money_value(row.get("total_base_currency")),
}
for row in open_ucty
])
hlavicka = {
"titulka": "UZAVIERKA",
"uz_cislo": clsrep.get("clsrep_no") or "-",
"c_uzaverka": clsrep.get("clsrep_no") or "-",
"uzaverka": clsrep.get("created_at") or "",
"id_zkratka": str(clsrep.get("clsrep_no") or "").split("-", 1)[0],
"od": f"{interval.get('ucislo_od', '')} {interval.get('closed_at_od', '')}".strip(),
"do": f"{interval.get('ucislo_do', '')} {interval.get('closed_at_do', '')}".strip(),
"h1": f"Pocet uctov: {summary.get('pocet_uctu', 0)}",
"h2": f"Suma uctov: {money_value(summary.get('total_base_currency')):.2f}",
"h3": f"Suma platieb: {money_value(summary.get('total_payments')):.2f}",
}
if abs(_clsrep_float(summary.get("difference"))) >= 0.005:
hlavicka["h4"] = f"Rozdiel: {money_value(summary.get('difference')):.2f}"
return {
"printer": {
"reset": "",
"max_characters": width,
"crlf": "\n",
},
"hlavicka": hlavicka,
"sekcie": sekcie,
"report": clsrep,
"width": width,
}
def _render_clsrep_jinja(clsrep: dict, width: int = 40) -> tuple[str, str] | None:
resolved = _resolve_closure_template()
if resolved is None:
return None
template_text, template_source = resolved
try:
from jinja2 import Environment
env = Environment(autoescape=False, trim_blocks=True, lstrip_blocks=True)
template = env.from_string(template_text)
rendered = template.render(**_build_legacy_closure_context(clsrep, width=width))
rendered = "\n".join(line.rstrip() for line in rendered.splitlines())
rendered = rendered.strip("\n") + "\n"
return rendered, template_source
except Exception as exc:
Logger.exception(f"CLOSURE TEMPLATE RENDER FAILED {template_source}: {exc}")
return None
def format_clsrep_text(clsrep: dict, width: int = 40) -> list[str]:
lines: list[str] = []
money_unit = "EUR"
def as_dict(value):
if hasattr(value, "model_dump"):
return value.model_dump()
return value if isinstance(value, dict) else {}
clsrep = as_dict(clsrep)
rendered = _render_clsrep_jinja(clsrep, width=width)
if rendered is not None:
rendered_text, _template_source = rendered
return rendered_text.splitlines()
def as_float(value, default: float = 0.0) -> float:
try:
return float(value or default)
except Exception:
return default
def money(value) -> str:
return f"{as_float(value):.2f} {money_unit}"
def line(left: str = "", right: str = ""):
left = str(left or "")
right = str(right or "")
if not right:
while left:
lines.append(left[:width])
left = left[width:]
if not left:
return
return
available = width - len(right) - 1
if available < 10:
lines.append(left[:width])
lines.append(right[-width:].rjust(width))
return
if len(left) <= available:
lines.append(left + " " * (width - len(left) - len(right)) + right)
return
lines.append(left[:width])
lines.append(right[-width:].rjust(width))
def center(text: str = ""):
lines.append(str(text or "")[:width].center(width))
def blank():
if lines and lines[-1] != "":
lines.append("")
def sep(ch: str = "-"):
lines.append(ch * width)
def title(text: str):
blank()
sep("=")
center(text)
sep("=")
def wrap(label: str, value: str = ""):
label = str(label or "")
value = str(value or "")
if value:
prefix = f"{label}: "
body_width = max(10, width - len(prefix))
parts = textwrap.wrap(value, width=body_width) or [""]
lines.append((prefix + parts[0])[:width])
indent = " " * min(len(prefix), width - 1)
for part in parts[1:]:
lines.append((indent + part)[:width])
return
if label.startswith("- "):
body_width = max(10, width - 2)
parts = textwrap.wrap(label[2:], width=body_width) or [""]
lines.append(("- " + parts[0])[:width])
for part in parts[1:]:
lines.append((" " + part)[:width])
return
for part in textwrap.wrap(label, width=width) or [""]:
lines.append(part[:width])
def nonzero(value) -> bool:
return abs(as_float(value)) >= 0.005
def status_text(value: str) -> str:
value = str(value or "").strip().lower()
return {
"preview": "nahlad",
"settled": "vyrovnane",
"completed": "vykonane",
"failed": "chyba",
"carry": "prenos",
}.get(value, value)
def vat_label(rate: str) -> str:
raw = str(rate or "").strip()
if raw in ("-1", "-1.0"):
return "Bez DPH"
try:
value = float(raw.replace(",", "."))
except Exception:
return f"DPH {raw}"
if value == -1:
return "Bez DPH"
if 0 < value <= 2:
value = (value - 1) * 100
if abs(value - round(value)) < 0.0001:
return f"DPH {int(round(value))}%"
return f"DPH {value:.2f}%"
def flag(name: str, default: bool = False) -> bool:
flags = settings.get("flags") if isinstance(settings, dict) else {}
if isinstance(flags, dict) and name in flags:
return bool(flags.get(name))
return default
def section_rows(name: str) -> list[dict]:
rows = sections.get(name) if isinstance(sections, dict) else []
return [row for row in (rows or []) if isinstance(row, dict)]
def amount_section(title_text: str, rows: list[dict], *, code_field: str = "code", name_field: str = "name"):
if not rows:
return
title(title_text)
for row in rows:
name = str(row.get(name_field) or row.get(code_field) or "-")
code = str(row.get(code_field) or "").strip()
label = name if not code or name == code else f"{name} ({code})"
line(label, money(row.get("amount")))
def qty_amount_section(title_text: str, rows: list[dict]):
if not rows:
return
title(title_text)
for row in rows:
name = str(row.get("name") or row.get("code") or "-")
code = str(row.get("code") or "").strip()
label = name if not code or name == code else f"{name} ({code})"
qty = as_float(row.get("qty"))
line(label, money(row.get("amount")))
if abs(qty) >= 0.0001:
line(" Mnozstvo", f"{qty:.4g}")
def count_amount_section(title_text: str, rows: list[dict]):
if not rows:
return
title(title_text)
for row in rows:
name = str(row.get("name") or row.get("code") or "-")
line(name, money(row.get("amount")))
line(" Pocet", str(row.get("count") or 0))
def tax_section(title_text: str, rows: list[dict]):
if not rows:
return
title(title_text)
last_payment = None
for row in rows:
payment = str(row.get("payment_name") or row.get("payment_code") or "-")
code = str(row.get("payment_code") or "").strip()
payment_label = payment if not code or payment == code else f"{payment} ({code})"
if payment_label != last_payment:
if last_payment is not None:
sep("-")
line(payment_label)
last_payment = payment_label
line(f" {vat_label(str(row.get('rate') or ''))}", money(row.get("celkem")))
line(" Zaklad", money(row.get("zaklad")))
line(" Dan", money(row.get("dan")))
def receipt_section(title_text: str, rows: list[dict]):
if not rows:
return
title(title_text)
for row in rows:
ucislo = str(row.get("ucislo") or "-")
total = money(row.get("total_base_currency"))
line(f"Ucet {ucislo}", total)
line(str(row.get("closed_at") or ""), str(row.get("autor") or ""))
table = str(row.get("table_name") or row.get("stul") or "")
room = str(row.get("room_name") or "")
if table or room:
line(table, room)
if row.get("origin"):
line(" Typ", row.get("origin"))
payment_text = str(row.get("payment_text") or "").strip()
if payment_text:
wrap(" Platby", payment_text)
sep("-")
interval = as_dict(clsrep.get("interval"))
summary = as_dict(clsrep.get("summary"))
platby = as_dict(clsrep.get("platby"))
uzivatele = as_dict(clsrep.get("uzivatele"))
dph = as_dict(clsrep.get("dph"))
settings = as_dict(clsrep.get("closure_settings"))
sections = as_dict(clsrep.get("sections"))
cash_state = clsrep.get("cash_state") or []
warnings = clsrep.get("warnings") or []
open_ucty = clsrep.get("open_ucty") or []
for row in cash_state:
if not isinstance(row, dict):
continue
unit = str(row.get("payment_unit") or "").strip()
if unit:
money_unit = unit
break
sep("=")
center("UZAVIERKA")
center(clsrep.get("clsrep_no") or "-")
created_at = clsrep.get("created_at") or ""
if created_at:
center(created_at)
sep("=")
line("Ucty", f"{interval.get('ucislo_od', '')} - {interval.get('ucislo_do', '')}")
line("Cas od", interval.get("closed_at_od", ""))
line("Cas do", interval.get("closed_at_do", ""))
if settings:
odvod = str(settings.get("uzav_odvod") or "").strip()
if odvod:
line("Rezim odvodu", odvod)
men_sp_man = str(settings.get("men_sp_man") or "").strip()
if men_sp_man:
line("Zamena skladov", men_sp_man)
title("SUHRN")
line("Pocet uctov", summary.get("pocet_uctu", 0))
line("Suma uctov", money(summary.get("total_base_currency")))
line("Suma platieb", money(summary.get("total_payments")))
diff = as_float(summary.get("difference"))
if abs(diff) >= 0.005:
line("Rozdiel", money(diff))
payment_names: dict[str, str] = {}
for row in cash_state:
if not isinstance(row, dict):
continue
code = str(row.get("payment_code") or "").strip()
name = str(row.get("payment_name") or "").strip()
if code and name:
payment_names[code] = name
payment_report_rows = section_rows("payments_by_code")
if flag("t_uz_drpl", True) and (payment_report_rows or platby):
title("TRZBY PODLA PLATIEB")
if payment_report_rows:
for row in payment_report_rows:
code = str(row.get("code") or "").strip()
name = str(row.get("name") or code or "-")
label = name if not code or name == code else f"{name} ({code})"
original = as_float(row.get("amount_original", row.get("amount")))
amount = as_float(row.get("amount"))
line(label, money(amount))
if abs(original - amount) >= 0.005:
line(" Pred zlavou", money(original))
if nonzero(row.get("tip")):
line(" TIP", money(row.get("tip")))
else:
for code in sorted(platby):
name = payment_names.get(str(code), str(code))
label = name if name == str(code) else f"{name} ({code})"
line(label, money(platby.get(code)))
if cash_state and flag("t_uz_odovzdanie", True):
title("ODOVZDANIE A PRENOS")
grouped: dict[str, list[dict]] = {}
for row in cash_state:
if isinstance(row, dict):
odovzdat = str(row.get("payment_odovzdat") or "").strip()
if not odovzdat:
continue
grouped.setdefault(str(row.get("prn_no") or "-"), []).append(row)
for prn_no in sorted(grouped):
blank()
center(f"TLACIAREN {prn_no}")
sep("-")
for row in grouped[prn_no]:
name = str(row.get("payment_name") or row.get("payment_code") or "-")
code = str(row.get("payment_code") or "").strip()
label = name if not code or name == code else f"{name} ({code})"
line(label)
if nonzero(row.get("opening_amount")):
line(" Z predch. uzavierky", money(row.get("opening_amount")))
if nonzero(row.get("sales_amount")):
line(" Trzba", money(row.get("sales_amount")))
if nonzero(row.get("receivable_amount")):
line(" Uhrady pohladavok", money(row.get("receivable_amount")))
if nonzero(row.get("manual_deposit_amount")):
line(" Vklady", money(row.get("manual_deposit_amount")))
if nonzero(row.get("manual_withdrawal_amount")):
line(" Vybery", money(row.get("manual_withdrawal_amount")))
if nonzero(row.get("auto_deposit_amount")):
line(" Auto vklad", money(row.get("auto_deposit_amount")))
if nonzero(row.get("auto_withdrawal_amount")):
line(" Auto vyber", money(row.get("auto_withdrawal_amount")))
line(" Stav pred odvodom", money(row.get("balance_amount")))
line(" Prenos dalej", money(row.get("carry_amount")))
status = status_text(row.get("status") or "")
if status:
line(" Stav", status)
error = str(row.get("error") or "").strip()
if error:
wrap(" Chyba", error)
sep("-")
if flag("t_uz_fisk_platby", False):
amount_section("FISKALNE PLATBY", section_rows("fiscal_payments"))
if flag("t_uz_terminal", False):
amount_section("PLATBY TERMINALOM", section_rows("terminal_payments"))
if flag("t_uz_poh_drpl", False):
amount_section("UHRADY POHLADAVOK", section_rows("receivables_by_payment"))
if flag("t_uz_man", False):
qty_amount_section("TRZBY PODLA MANAGEROV", section_rows("managers"))
if flag("t_uz_man_dph", False):
tax_section("MANAGERI A DPH", section_rows("managers_by_vat"))
if flag("t_uz_mena", False):
rows = section_rows("currency_payments")
if rows:
title("PLATBY PODLA MIEN")
for row in rows:
code = str(row.get("code") or "-")
line(code, money(row.get("base_amount")))
line(" V mene", f"{as_float(row.get('amount')):.2f} {code}")
if flag("t_uz_drpldan", False):
tax_section("PLATBY A DPH", section_rows("payments_by_vat"))
if flag("t_uz_drplfisdan", False):
tax_section("FISKALNE PLATBY A DPH", section_rows("fiscal_payments_by_vat"))
if flag("t_uz_spdph", False):
tax_section("PLATBY, MANAGERI A DPH", section_rows("managers_payments_by_vat"))
if flag("t_uz_trzdr", False):
qty_amount_section("TRZBY PODLA DRUHOV", section_rows("items_by_kind"))
if flag("t_uz_trzdr", False):
qty_amount_section("TRZBY PODLA SPART", section_rows("sparts"))
if flag("t_uz_harek", False):
qty_amount_section("PREDANE POLOZKY", section_rows("items_sold"))
if flag("t_uz_cenhl", False):
qty_amount_section("CENOVE HLADINY", section_rows("price_levels"))
if flag("t_uz_spcis", False):
qty_amount_section("SKLADY", section_rows("storages"))
if flag("t_uz_vklad_vyber", False):
rows = section_rows("cash_operations")
if rows:
title("VKLADY A VYBERY")
for row in rows:
line(f"{row.get('operation_label') or '-'} {row.get('ucislo') or ''}", money(row.get("amount")))
line(str(row.get("closed_at") or ""), str(row.get("autor") or ""))
line(str(row.get("payment_name") or row.get("payment_code") or ""), f"PRN {row.get('prn_no') or '-'}")
sep("-")
if flag("t_uz_vkl_drpl", False) or flag("t_uz_trz_vkl_drpl", False):
amount_section("VKLADY/VYBERY PODLA PLATIEB", section_rows("cash_operations_summary"))
if flag("t_uz_puctu", False):
count_amount_section("POCTY UCTOV PODLA OBSLUHY", section_rows("receipt_counts_by_user"))
if flag("t_uz_ucet", False):
receipt_section("ZOZNAM UCTOV", section_rows("receipt_list"))
if flag("t_uz_stzur", False):
receipt_section("ZURNAL STORIEN", section_rows("storno_journal"))
if uzivatele and (flag("t_uz_casni", True) or flag("t_uz_man", False)):
title("OBSLUHA")
for user in sorted(uzivatele):
user_data = as_dict(uzivatele.get(user))
line(str(user))
line(" Celkom", money(user_data.get("total_base_currency")))
if nonzero(user_data.get("hotovost")):
line(" Hotovost", money(user_data.get("hotovost")))
if flag("t_uz_puctu_cas", False):
count_amount_section("POCTY UCTOV PO OBSLUHE", section_rows("receipt_counts_by_user"))
if dph and flag("t_uz_dph", True):
title("DPH")
for rate in sorted(dph):
vat_row = as_dict(dph.get(rate))
line(vat_label(str(rate)), money(vat_row.get("celkem")))
line(" Zaklad", money(vat_row.get("zaklad")))
line(" Dan", money(vat_row.get("dan")))
if open_ucty and flag("t_uz_stoly", True):
title("OTVORENE UCTY")
total_open = 0.0
for u in open_ucty:
if not isinstance(u, dict):
continue
total = as_float(u.get("total_base_currency"))
total_open += total
line(f"Stol {u.get('stul') or '-'}", f"Ucet {u.get('ucislo') or '-'}")
if u.get("autor") or u.get("open_at"):
line(str(u.get("autor") or "UNKNOWN"), str(u.get("open_at") or ""))
if u.get("blocked_by"):
line("Blokuje", u.get("blocked_by"))
line("Suma", money(total))
sep("-")
line("Celkom otvorene", money(total_open))
if warnings:
title("UPOZORNENIA")
for warning in warnings:
wrap(f"- {warning}")
if lines and lines[-1] != "":
blank()
return lines
def format_clsrep_str(clsrep: dict, width: int = 40) -> str:
return "\n".join(format_clsrep_text(clsrep, width))
def tisk_clsrep(clsrep, printer, *, width: int = 40,
init_cmd: bytes = b"\x1b@",
cut_cmd: bytes = b"\x1dV\x00"):
if init_cmd:
printer.write(init_cmd)
for l in format_clsrep_text(clsrep, width):
safe = normalize_cp852(l)
printer.write((safe + "\n").encode("cp852", errors="replace"))
if cut_cmd:
printer.write(cut_cmd)
def _obsolete_show_clsrep_preview_direct_tcp(clsrep, width: int = 40):
root = BoxLayout(
orientation="vertical",
spacing=dp(10),
padding=dp(10),
)
with root.canvas.before:
Color(0.08, 0.08, 0.08, 1)
bg = Rectangle(pos=root.pos, size=root.size)
root.bind(pos=lambda i, v: setattr(bg, "pos", v))
root.bind(size=lambda i, v: setattr(bg, "size", v))
receipt_text = "\n".join(format_clsrep_text(clsrep, width))
scroll = ScrollView(
do_scroll_x=True,
do_scroll_y=True,
bar_width=dp(8),
size_hint=(1, 1),
)
receipt = Label(
text=receipt_text,
font_name="RobotoMono-Regular",
font_size=dp(14),
halign="left",
valign="top",
size_hint=(None, None),
color=(0.9, 0.9, 0.9, 1),
)
receipt.bind(
texture_size=lambda inst, size: setattr(inst, "size", size)
)
scroll.add_widget(receipt)
root.add_widget(scroll)
bottom = BoxLayout(
size_hint_y=None,
height=dp(70),
spacing=dp(10),
)
btn_print = Button(text="TISK")
btn_close = Button(text="ZPĚT")
bottom.add_widget(btn_print)
bottom.add_widget(btn_close)
root.add_widget(bottom)
popup = Popup(
title="Náhled uzávěrky",
content=root,
size_hint=(0.95, 0.95),
auto_dismiss=False,
)
def do_print():
app = App.get_running_app()
cfg = app.cfg
try:
printer = open_printer_tcp(
cfg.bill_printer.split(":")[0],
int(cfg.bill_printer.split(":")[1])
)
tisk_clsrep(clsrep, printer)
printer.close()
except Exception as e:
Logger.error(f"TISK UZAVERKY CHYBA: {e}")
btn_print.bind(on_press=lambda *_: do_print())
btn_close.bind(on_press=popup.dismiss)
popup.open()
def _printer_label(printer) -> str:
if not printer:
return "Tlaciaren"
prn_no = str(getattr(printer, "prn_no", "") or "").strip()
prn_name = str(getattr(printer, "prn_name", "") or "").strip()
return f"{prn_no} - {prn_name}" if prn_name else (prn_no or "Tlaciaren")
def show_clsrep_preview(
clsrep,
width: int = 40,
printers=None,
default_printer: str = "",
on_print=None,
title: str = "Nahlad uzavierky",
extra_actions=None,
):
root = BoxLayout(
orientation="vertical",
spacing=dp(10),
padding=dp(10),
)
with root.canvas.before:
Color(0.08, 0.08, 0.08, 1)
bg = Rectangle(pos=root.pos, size=root.size)
root.bind(pos=lambda i, v: setattr(bg, "pos", v))
root.bind(size=lambda i, v: setattr(bg, "size", v))
receipt_text = "\n".join(format_clsrep_text(clsrep, width))
printer_list = list(printers or [])
selected_no = str(default_printer or "").strip()
if not any(str(getattr(prn, "prn_no", "") or "").strip() == selected_no for prn in printer_list):
selected_no = str(getattr(printer_list[0], "prn_no", "") or "").strip() if printer_list else ""
selected = {"prn_no": selected_no}
def selected_printer():
for prn in printer_list:
if str(getattr(prn, "prn_no", "") or "").strip() == selected["prn_no"]:
return prn
return None
scroll = ScrollView(
do_scroll_x=True,
do_scroll_y=True,
bar_width=dp(10),
size_hint=(1, 1),
)
receipt = Label(
text=receipt_text,
font_name="RobotoMono-Regular",
font_size=dp(14),
halign="left",
valign="top",
size_hint=(None, None),
color=(0.92, 0.92, 0.92, 1),
)
receipt.bind(texture_size=lambda inst, size: setattr(inst, "size", size))
scroll.add_widget(receipt)
root.add_widget(scroll)
bottom = BoxLayout(
size_hint_y=None,
height=dp(70),
spacing=dp(10),
)
btn_printer = Button(text=_printer_label(selected_printer()), size_hint=(0.35, 1))
btn_print = Button(text="TLAC")
btn_close = Button(text="SPAT")
bottom.add_widget(btn_printer)
for action in extra_actions or []:
if isinstance(action, dict):
action_text = str(action.get("text") or "")
action_callback = action.get("callback")
else:
try:
action_text, action_callback = action
except Exception:
continue
if not action_text or not callable(action_callback):
continue
btn_action = Button(text=action_text, size_hint=(0.26, 1))
btn_action.bind(on_press=lambda _btn, cb=action_callback: cb())
bottom.add_widget(btn_action)
bottom.add_widget(btn_print)
bottom.add_widget(btn_close)
root.add_widget(bottom)
popup = Popup(
title=title,
content=root,
size_hint=(0.95, 0.95),
auto_dismiss=False,
)
def choose_printer_popup(*_):
if not printer_list:
return
body = BoxLayout(orientation="vertical", spacing=dp(8), padding=dp(8), size_hint_y=None)
body.bind(minimum_height=body.setter("height"))
selector_scroll = ScrollView(size_hint=(1, 1), bar_width=dp(12))
selector_scroll.add_widget(body)
selector = Popup(
title="Vyber tlaciaren",
content=selector_scroll,
size_hint=(0.55, 0.75),
auto_dismiss=True,
)
def choose(prn_no: str):
selected["prn_no"] = str(prn_no or "").strip()
btn_printer.text = _printer_label(selected_printer())
selector.dismiss()
for prn in printer_list:
prn_no = str(getattr(prn, "prn_no", "") or "").strip()
btn = Button(
text=_printer_label(prn),
size_hint_y=None,
height=dp(56),
)
btn.bind(on_press=lambda _btn, value=prn_no: choose(value))
body.add_widget(btn)
selector.open()
def do_print():
if not selected["prn_no"] or not callable(on_print):
return
try:
on_print(selected["prn_no"], receipt_text)
btn_print.text = "ZARADENE"
except Exception as e:
Logger.exception(f"TISK UZAVERKY CHYBA: {e}")
btn_print.bind(on_press=lambda *_: do_print())
btn_printer.bind(on_press=choose_printer_popup)
btn_close.bind(on_press=popup.dismiss)
btn_print.disabled = not selected["prn_no"] or not callable(on_print)
btn_printer.disabled = not bool(printer_list)
popup.open()