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