1995 lines
69 KiB
Python
1995 lines
69 KiB
Python
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()
|
||
|
||
|