from kivy.app import App from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.button import Button from kivy.uix.label import Label from kivy.uix.popup import Popup from kivy.uix.scrollview import ScrollView from kivy.metrics import dp from kivy.logger import Logger from kivy.uix.modalview import ModalView from kivy.graphics import Color, RoundedRectangle from urllib.parse import quote from kivy.uix.image import Image from kivy.core.image import Image as CoreImage import qrcode from decimal import Decimal, ROUND_HALF_UP from uuid import uuid4 from io import BytesIO from numberpad import NumberPad import api_call import data import bankterm_service # JQ import threading from kivy.clock import Clock import requests import uuid from datetime import datetime, date, time as dt_time from requests.auth import HTTPBasicAuth import json # JQ class PaymentItem: """ Jedna platba v PaymentDialogu. Uchovává částku v originální měně a přepočet do CZK. """ def __init__( self, ptype, suma: float, suma_czk: float, note: str | None = None, hotel_charge=None, p_kopii: int | None = None, tip: float = 0.0, adjustment_ids=None, rounding_delta: float = 0.0, terminal_result: dict | None = None, ): self.id = str(uuid4()) # typ platby (PaymentType) self.ptype = ptype # částka v měně platby self.suma = round(float(suma), 2) # částka přepočtená do CZK self.suma_czk = round(float(suma_czk), 2) self.note = note self.hotel_charge = hotel_charge copy_count = p_kopii if p_kopii is not None else getattr(ptype, "p_kopii", 1) try: self.p_kopii = max(int(copy_count), 0) except Exception: self.p_kopii = 1 self.tip = round(float(tip or 0), 2) self.adjustment_ids = list(adjustment_ids or []) self.rounding_delta = round(float(rounding_delta or 0), 2) self.terminal_result = dict(terminal_result or {}) def spd_qr(*, iban: str, amount_czk: float, message: str = "", vs: str | None = None) -> str: iban = iban.replace(" ", "") parts = [ "SPD*1.0", f"ACC:{iban}", f"AM:{amount_czk:.2f}", "CC:CZK", ] if vs: parts.append(f"X-VS:{vs}") if message: parts.append(f"MSG:{quote(message[:60])}") return "*".join(parts) def make_qr_texture( *, iban: str, amount_czk: float, message: str, vs: str | None = None, ): qr_text = spd_qr( iban=iban, amount_czk=amount_czk, message=message, vs=vs, ) qr = qrcode.QRCode( error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=6, border=1, ) qr.add_data(qr_text) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buf = BytesIO() img.save(buf, format="PNG") buf.seek(0) core = CoreImage(buf, ext="png") return core.texture class QRPreviewDialog(ModalView): def __init__(self, texture, on_confirm, **kw): super().__init__( size_hint=(None, None), size=(dp(360), dp(440)), auto_dismiss=False, **kw ) self.on_confirm = on_confirm root = BoxLayout(orientation="vertical", padding=dp(8), spacing=dp(8)) img = Image(texture=texture, allow_stretch=True) root.add_widget(img) btn = Button( text="ZAPLACENO", size_hint_y=None, height=dp(48), ) btn.bind(on_press=self._confirm) root.add_widget(btn) self.add_widget(root) def _confirm(self, *_): self.dismiss() self.on_confirm() # ===================================================== # MODELY # ===================================================== class PaymentDialog(ModalView): def __init__(self, kasutxt, controller, ucet, payment_types, on_done, setup, on_cancel=None, **kw): super().__init__( size_hint=(None, None), size=(dp(760), dp(520)), auto_dismiss=False, **kw ) self.setup = setup self.ucet = ucet self.on_done = on_done self.on_cancel = on_cancel self.kasutxt = kasutxt self.payments: list[PaymentItem] = [] self.payment_types = payment_types self.controller = controller self.discount_abs = 0.0 self.discount_pct: float | None = None # JQ Platba kartou self.current_service_id = None self.card_payment_running = False self.terminal_type = "BESTERON" self.sale_id = "Alto/foodw32" self.terminal_url = "http://10.109.39.73:7500/" self.terminal_id = "Foodw32" self.terminal_user = "user" self.terminal_password = "pass" # JQ Konec Platba kartou root = BoxLayout(orientation="vertical", padding=dp(6), spacing=dp(6)) # ================= INFO ================= self.lbl_total = Label() self.lbl_paid = Label() self.lbl_remain = Label() info = GridLayout(cols=2, size_hint_y=None, height=dp(90)) info.add_widget(Label(text="CELKEM")) info.add_widget(self.lbl_total) info.add_widget(Label(text="ZAPLACENO")) info.add_widget(self.lbl_paid) info.add_widget(Label(text="ZBÝVÁ")) info.add_widget(self.lbl_remain) root.add_widget(info) # ================= STŘED ================= mid = BoxLayout(spacing=dp(6)) self.list_box = GridLayout(cols=1, spacing=dp(4)) mid.add_widget(self.list_box) right = GridLayout(cols=1, size_hint_x=None, width=dp(160), spacing=dp(4)) for pt in self.payment_types: b = Button(text=pt.name) b.bind(on_press=lambda _, p=pt: self._add_payment(p)) right.add_widget(b) mid.add_widget(right) root.add_widget(mid) # ================= DOLNÍ ================= bottom = BoxLayout(size_hint_y=None, height=dp(48), spacing=dp(6)) bottom.add_widget(Button(text="SLEVA", on_press=self._on_discount)) bottom.add_widget(Button(text="SLEVA %", on_press=self._on_discount_pct)) # bottom.add_widget(Button(text="ZRUŠIT", on_press=lambda *_: self._cancel())) # JQ bottom.add_widget(Button(text="ZRUŠIT", on_press=lambda *_: self._cancel_all())) # JQ Konec root.add_widget(bottom) self.add_widget(root) self._refresh() # ===================================================== # STAV # ===================================================== def _total_items(self) -> float: return float(self.ucet.total_czk()) def _discount(self) -> float: return float(self.discount_abs or 0) def _to_pay(self) -> float: return max(self._total_items() - self._discount(), 0.0) def _paid(self) -> float: return sum((p.suma_czk for p in self.payments), 0.0) def _remaining(self) -> float: return max(self._to_pay() - self._paid(), 0.0) # ===================================================== # UI # ===================================================== def _refresh(self): self.lbl_total.text = f"{self._total_items():.2f} + {self._currency()} " self.lbl_paid.text = f"{self._paid():.2f}+{self._currency()}" self.lbl_remain.text = f"{self._remaining():.2f}+{self._currency()}" self.list_box.clear_widgets() # -------- SLEVA -------- if self.discount_abs > 0: row = BoxLayout(size_hint_y=None, height=dp(40)) txt = "SLEVA" if self.discount_pct is not None: txt += f" ({self.discount_pct:.2f}%)" row.add_widget(Label(text=txt)) row.add_widget(Label(text=f"-{self.discount_abs:.2f}+{self._currency()}")) self.list_box.add_widget(row) # -------- PLATBY -------- for p in self.payments: row = BoxLayout(size_hint_y=None, height=dp(40)) rate = float(p.ptype.rate) if rate != 1.0: left = f"{p.ptype.name}: {p.suma:.2f} {p.ptype.unit}" right = f"= {p.suma_czk:.2f}+{self._currency()}" else: left = p.ptype.name right = f"{p.suma_czk:.2f}+{self._currency()}" row.add_widget(Label(text=left)) row.add_widget(Label(text=right)) btn = Button(text="≤", size_hint_x=None, width=dp(40)) btn.bind(on_press=lambda _, pid=p.id: self._remove_payment(pid)) row.add_widget(btn) self.list_box.add_widget(row) if self._remaining() <= 0.009: self._finish() # ===================================================== # PLATBY # ===================================================== def _add_payment(self, ptype: data.PaymentType): remaining_czk = float(self._remaining()) rate = float(ptype.rate) initial = remaining_czk / rate if rate != 1.0 else remaining_czk def accept(val: str): if not val: return s = str(val).strip().replace(" ", "").replace(",", ".") try: if "/" in s: num, den = s.split("/", 1) suma = float(num) / float(den) else: suma = float(s) except Exception: return if suma <= 0: return suma_czk = round(suma * rate, 2) is_cash = (ptype.is_cash == True) if not is_cash: suma_czk = min(suma_czk, remaining_czk) suma = round(suma_czk / rate, 2) if suma_czk <= 0: return # QR platba if ptype.code == "QR": texture = make_qr_texture( iban=self.setup.iban, amount_czk=suma_czk, message=f"Účet {self.ucet.ucislo or self.ucet.stul}", vs=self.ucet.ucislo, ) def confirmed(suma_=suma, suma_czk_=suma_czk): self.payments.append( PaymentItem( ptype=ptype, suma=round(suma_, 2), suma_czk=round(suma_czk_, 2), ) ) self._refresh() QRPreviewDialog(texture=texture, on_confirm=confirmed).open() return # PLATBA kartou if ptype.code == "CARD": #if terminal_type == "BESTERON": self._card_payment_async(ptype, suma, suma_czk) # self._refresh() return # ostatní platby self.payments.append( PaymentItem( ptype=ptype, suma=round(suma, 2), suma_czk=round(suma_czk, 2), ) ) self._refresh() NumberPad( mode="number", allow_fraction=False, decimal_places=2, max_len=7, initial_value=f"{initial:.2f}", on_accept=accept, ).open() def _remove_payment(self, pid: str): self.payments = [p for p in self.payments if p.id != pid] self._refresh() # ===================================================== # SLEVA ABS # ===================================================== def _on_discount(self, *_): def accept(val: str): try: amt = float(val) except Exception: return if amt <= 0: return amt = min(amt, self._remaining()) self.discount_abs = round(amt, 2) self.discount_pct = None self._refresh() NumberPad( mode="number", allow_fraction=False, decimal_places=2, max_len=6, initial_value=f"{self._remaining():.2f}", on_accept=accept, ).open() # ===================================================== # SLEVA % # ===================================================== def _on_discount_pct(self, *_): total = self._total_items() def accept(val: str): try: pct = float(val) except Exception: return if pct <= 0: return discount = round(total * pct / 100.0, 2) paid = self._paid() if paid > total - discount: needed_discount = total - paid if needed_discount <= 0: return pct = round(needed_discount / total * 100.0, 2) discount = needed_discount self._popup_info( "Sleva upravena", f"Sleva upravena na {pct}% aby pokryla již zaplacenou částku." ) self.discount_abs = round(discount, 2) self.discount_pct = pct self._refresh() NumberPad( mode="number", allow_fraction=False, decimal_places=2, max_len=5, initial_value="10", on_accept=accept, ).open() # ===================================================== # POPUP # ===================================================== def _popup_info(self, title, text): box = BoxLayout(orientation="vertical", padding=dp(10), spacing=dp(10)) box.add_widget(Label(text=text)) btn = Button(text="OK", size_hint_y=None, height=dp(40)) popup = ModalView(size_hint=(None, None), size=(dp(320), dp(200))) def _dismiss_after_touch(*_): Clock.schedule_once(lambda *_: popup.dismiss(), 0) btn.bind(on_release=_dismiss_after_touch) box.add_widget(btn) popup.add_widget(box) popup.open() # ===================================================== # FINISH # ===================================================== def _finish(self): self.ucet.discount_abs = float(self.discount_abs) self.ucet.platby = [] for p in self.payments: self.ucet.platby.append( data.Platba( code=p.ptype.code, nazev=p.ptype.name, suma=float(p.suma), suma_czk=float(p.suma_czk), unit=p.ptype.unit, rate=float(p.ptype.rate), p_kopii=max(int(getattr(p, "p_kopii", getattr(p.ptype, "p_kopii", 1)) or 0), 0), tip=float(getattr(p, "tip", 0) or 0), ) ) self.on_done(self.ucet) self.dismiss() # ===================================================== # CANCEL # ===================================================== def _cancel(self): if self.on_cancel: self.on_cancel() self.dismiss() # JQ def _cancel_all(self): if self.card_payment_running: self._abort_transaction() if self.on_cancel: self.on_cancel() self.dismiss() def _card_payment_async(self, ptype, suma, suma_czk, note=None, hotel_charge=None, adjustment_ids=None, rounding_delta: float = 0.0): if self.card_payment_running: self._popup_info("Platba", "Už probíhá jiná karetní transakce.") return self.card_payment_running = True popup = self._popup_wait( "Platba kartou", "Přiložte kartu k terminálu..." ) def worker(): result = self._call_terminal_api(suma_czk) def done(dt): self.card_payment_running = False popup.dismiss() if result["success"]: self._extract_receipt(result["raw"]) self.payments.append( PaymentItem( ptype=ptype, suma=round(suma, 2), suma_czk=round(suma_czk, 2), note=note, hotel_charge=hotel_charge, adjustment_ids=adjustment_ids, rounding_delta=rounding_delta, terminal_result=result, ) ) if hasattr(self, "_next_payment_hotel_charge"): self._next_payment_hotel_charge = None if hasattr(self, "_next_payment_adjustment_ids"): self._next_payment_adjustment_ids = [] if getattr(self, "quick_complete", False): self._finish() return self._refresh() else: if hasattr(self, "_next_payment_adjustment_ids"): remove_ids = set(self._next_payment_adjustment_ids or []) self.applied_discounts = [ app for app in getattr(self, "applied_discounts", []) if app.id not in remove_ids ] self._next_payment_adjustment_ids = [] if hasattr(self, "recalculate"): self.recalculate() self._popup_info("Platba zamítnuta", result.get("error", "Chyba terminálu")) from kivy.clock import Clock Clock.schedule_once(done) import threading threading.Thread(target=worker, daemon=True).start() def _card_payment_result(self, success, ptype, suma, suma_czk, note=None, hotel_charge=None, adjustment_ids=None, rounding_delta: float = 0.0): if success: self.payments.append( PaymentItem( ptype=ptype, suma=round(suma, 2), suma_czk=round(suma_czk, 2), note=note, hotel_charge=hotel_charge, adjustment_ids=adjustment_ids, rounding_delta=rounding_delta, ) ) if getattr(self, "quick_complete", False): self._finish() return if hasattr(self, "_next_payment_hotel_charge"): self._next_payment_hotel_charge = None self._refresh() else: self._popup_info( "Platba zamítnuta", "Terminál odmítl transakci." ) def _bank_terminal_client(self): term = self._selected_bankterm() term_data = getattr(term, "term_data", None) if term else None config = bankterm_service.BankTerminalConfig( terminal_type=str(getattr(term_data, "typ", "") or self.terminal_type), url=self._bankterm_url(term_data) if term_data else self.terminal_url, terminal_id=str(getattr(term_data, "terminal_id", "") or self.terminal_id), sale_id=str(getattr(term_data, "sale_id", "") or self.sale_id), user=str(getattr(term_data, "terminal_user", "") or self.terminal_user), password=str(getattr(term_data, "terminal_password", "") or self.terminal_password), currency="CZK", timeout=60, ) return bankterm_service.create_bank_terminal_client(config) def _selected_bankterm(self): printer = self.printer_by_no.get(str(self.selected_printer or "")) id_term = str(getattr(printer, "id_term", "") or "").strip() if printer else "" if not id_term: return None return self.bankterm_map.get(id_term) def _selected_bankterm_type(self) -> str: term = self._selected_bankterm() term_data = getattr(term, "term_data", None) if term else None return str(getattr(term_data, "typ", "") or "").strip().upper() def _bankterm_url(self, term_data) -> str: reqadr = str(getattr(term_data, "eft_reqadr", "") or "").strip() if reqadr.startswith("http://") or reqadr.startswith("https://"): return reqadr host = str(getattr(term_data, "eft_ipadr", "") or "").strip() if not host: return self.terminal_url protocol = str(getattr(term_data, "protokol", "") or "http").strip().lower() if not protocol.startswith(("http://", "https://")): protocol = f"{protocol}://" port = int(getattr(term_data, "eft_rempor", 0) or getattr(term_data, "eft_lclpor", 0) or 0) url = f"{protocol}{host}" if port: url = f"{url}:{port}" if reqadr: url = f"{url}/{reqadr.lstrip('/')}" return url def _call_terminal_api(self, amount): result = self._bank_terminal_client().payment(float(amount)) if result.service_id: self.current_service_id = result.service_id return result.legacy_dict() def _abort_transaction(self): if not self.current_service_id: return self._bank_terminal_client().abort(self.current_service_id) def _log_transaction(self, data): with open("terminal_log.jsonl", "a", encoding="utf-8") as f: f.write(json.dumps({ "time": datetime.utcnow().isoformat(), "data": data }) + "\n") def _extract_receipt(self, resp): text = self._bank_terminal_client().extract_receipt_text(resp) if text: print("=== SLIP ===") print(text) def _refund(self, original_service_id, amount): result = self._bank_terminal_client().refund(original_service_id, float(amount)) return result.legacy_dict() def _popup_wait(self, title, text): box = BoxLayout(orientation="vertical", padding=dp(10), spacing=dp(10)) box.add_widget(Label(text=text)) btn = Button(text="ZRUŠIT", size_hint_y=None, height=dp(40)) popup = ModalView(size_hint=(None, None), size=(dp(320), dp(200))) btn.bind(on_press=lambda *_: self._abort_transaction()) box.add_widget(btn) popup.add_widget(box) popup.open() return popup # JQ KONEC class GuidedPaymentDialog(PaymentDialog): STEP_DISCOUNT = "discount" STEP_PAYMENT = "payment" def __init__( self, ucet, controller, payment_types, on_done, setup, on_cancel=None, discounts=None, discount_permissions=None, discounts_all_allowed=False, printers=None, bankterms=None, default_printer=None, on_printer_change=None, preferred_payment=None, handler_runner=None, discount_runner=None, cenik_map=None, kasutxt:data.KasUtxtRiadky=None, quick_complete: bool = False, **kw ): ModalView.__init__( self, size_hint=(1, 1), auto_dismiss=False, **kw ) self.setup = setup self.ucet = ucet self.on_done = on_done self.on_cancel = on_cancel self.handler_runner = handler_runner self.discount_runner = discount_runner self.controller = controller self.payment_types = sorted( list(payment_types or []), key=lambda p: (getattr(p, "poradie", 0) or 0, str(getattr(p, "name", ""))), ) self.discounts = list(discounts or []) self.discount_permissions = { str(x).strip() for x in (discount_permissions or []) if str(x).strip() } self.discounts_all_allowed = bool(discounts_all_allowed) self.cenik_map = dict(cenik_map or {}) self.printers = list(printers or []) self.printer_by_no = { str(getattr(p, "prn_no", "") or ""): p for p in self.printers } self.printer_map = { getattr(p, "prn_no", ""): getattr(p, "prn_name", "") for p in self.printers } self.bankterms = list(bankterms or []) self.bankterm_map = { str(getattr(term, "id_term", "") or ""): term for term in self.bankterms } default_printer = str(default_printer or "").strip() if default_printer and default_printer not in self.printer_by_no: default_printer = "" self.selected_printer = default_printer or ( getattr(self.printers[0], "prn_no", "") if self.printers else "" ) self.on_printer_change = on_printer_change self.kasutxt=kasutxt self.preferred_payment = preferred_payment self._preferred_opened = False self.quick_complete = bool(quick_complete) self.payments: list[PaymentItem] = [] self.applied_discounts: list[data.UcetDiscount] = [ self._coerce_discount_application(item) for item in (getattr(self.ucet, "discounts_applied", []) or []) ] self.selected_discount = None self.discount_abs = 0.0 self.discount_pct: float | None = None self._base_poloz = [ pol.model_copy(deep=True) for pol in (getattr(self.ucet, "poloz", []) or []) ] self._base_total_before_money = round(float(self.ucet.total_czk()), 2) self._current_total = self._base_total_before_money self._recalculating = False self.loyalty_card = "" self.loyalty_name = "" self.loyalty_info = "" self.send_receipt_email = bool(getattr(self.ucet, "send_receipt_email", False)) self.handler_info = "" self._next_payment_note = None self._next_payment_hotel_charge = None self._next_payment_adjustment_ids: list[str] = [] self._handler_ptype = None self.current_service_id = None self.card_payment_running = False self.terminal_type = "BESTERON" self.sale_id = "Alto/foodw32" self.terminal_url = "http://10.109.39.73:7500/" self.terminal_id = "Foodw32" self.terminal_user = "user" self.terminal_password = "pass" self.step = ( self.STEP_DISCOUNT if self._has_discount_step() else self.STEP_PAYMENT ) self._build_ui() self.recalculate() self._refresh() def on_open(self): if self.step == self.STEP_PAYMENT and not self.quick_complete: self._open_preferred_payment_once() def run_quick_payment(self, ptype=None): self.quick_complete = True payment_type = ptype or self.preferred_payment if not payment_type: self._popup_info("Platba", "Nie je nastavený druh rýchlej platby.") return False if payment_type not in self.payment_types: match = str(getattr(payment_type, "code", "") or "").strip() payment_type = next( ( p for p in self.payment_types if str(getattr(p, "code", "") or "").strip() == match ), None, ) if not payment_type: self._popup_info("Platba", "Nastavený druh platby nie je dostupný.") return False self.step = self.STEP_PAYMENT self._begin_add_payment(payment_type) return True def on_touch_down(self, touch): super().on_touch_down(touch) return True def on_touch_move(self, touch): super().on_touch_move(touch) return True def on_touch_up(self, touch): super().on_touch_up(touch) return True def _build_ui(self): root = BoxLayout(orientation="vertical", padding=dp(8), spacing=dp(8)) self.add_widget(root) header = BoxLayout(size_hint_y=None, height=dp(72), spacing=dp(8)) title_box = BoxLayout(orientation="vertical") self.lbl_title = self._label("Platba", font_size=24, bold=True) self.lbl_step = self._label("") title_box.add_widget(self.lbl_title) title_box.add_widget(self.lbl_step) header.add_widget(title_box) self.lbl_to_pay_top = self._label("", font_size=24, bold=True) header.add_widget(self.lbl_to_pay_top) self.btn_printer = Button( text="Tlačiareň", size_hint=(None, 1), width=dp(190), ) self._fit_button_text(self.btn_printer) self.btn_printer.bind(on_release=self._open_printer_popup) header.add_widget(self.btn_printer) root.add_widget(header) body = BoxLayout(spacing=dp(8)) nav = GridLayout( cols=1, size_hint=(None, 1), width=dp(88), spacing=dp(6), ) self.btn_discount_step = Button(text="1\nZľava\n/karta") self.btn_payment_step = Button(text="2\nPlatby") self._fit_button_text(self.btn_discount_step, font_size=13) self._fit_button_text(self.btn_payment_step, font_size=13) self.btn_discount_step.bind(on_press=lambda *_: self._go_discount()) self.btn_payment_step.bind(on_press=lambda *_: self._go_payment()) nav.add_widget(self.btn_discount_step) nav.add_widget(self.btn_payment_step) body.add_widget(nav) self.content = BoxLayout(orientation="vertical", spacing=dp(8)) body.add_widget(self.content) summary = BoxLayout( orientation="vertical", size_hint=(None, 1), width=dp(330), spacing=dp(8), padding=(dp(10), dp(10), dp(10), dp(10)), ) self._paint_box(summary, (0.17, 0.17, 0.17, 1), radius=4) self.lbl_customer = self._label("", font_size=21, bold=True, halign="center", size_hint_y=None, height=dp(32)) self.lbl_table = self._label("", font_size=14, halign="center", size_hint_y=None, height=dp(24)) summary.add_widget(self.lbl_customer) summary.add_widget(self.lbl_table) self.totals_box = BoxLayout(orientation="vertical", spacing=dp(2), size_hint_y=None, height=dp(154)) self.lbl_total = self._summary_value_row("Celková suma") self.lbl_discount = self._summary_value_row("Zľava") self.lbl_after_discount = self._summary_value_row("Suma po zľave") self.lbl_tip = self._summary_value_row("TIP") self.lbl_paid = self._summary_value_row("Zaplatené") self.lbl_remain = self._summary_value_row("Zostáva") summary.add_widget(self.totals_box) self.lbl_loyalty = self._label("", font_size=14, size_hint_y=None, height=dp(28)) summary.add_widget(self.lbl_loyalty) self.summary_box = GridLayout(cols=1, spacing=dp(5), size_hint_y=None) self.summary_box.bind( minimum_height=self.summary_box.setter("height") ) scroll = ScrollView(do_scroll_x=False, do_scroll_y=True, bar_width=dp(10)) scroll.add_widget(self.summary_box) summary.add_widget(scroll) footer = BoxLayout(size_hint_y=None, height=dp(46), spacing=dp(8)) self.btn_summary_action = Button(text="") self.btn_summary_copies = Button(text="") self._fit_button_text(self.btn_summary_action, font_size=13) self._fit_button_text(self.btn_summary_copies, font_size=13) self.btn_summary_action.bind(on_release=self._summary_action) self.btn_summary_copies.bind(on_release=self._edit_receipt_copies) footer.add_widget(self.btn_summary_action) footer.add_widget(self.btn_summary_copies) summary.add_widget(footer) body.add_widget(summary) root.add_widget(body) bottom = BoxLayout(size_hint_y=None, height=dp(56), spacing=dp(8)) self.btn_back = Button(text="Späť") self.btn_cancel = Button(text="Zrušiť") self.btn_cancel.background_color = (0.6, 0.2, 0.2, 1) self.btn_primary = Button(text="Pokračovať") self._fit_button_text(self.btn_back) self._fit_button_text(self.btn_cancel) self._fit_button_text(self.btn_primary) self.btn_back.bind(on_press=lambda *_: self._back()) self.btn_cancel.bind(on_press=lambda *_: self._cancel_all()) self.btn_primary.bind(on_press=lambda *_: self._primary()) bottom.add_widget(self.btn_back) bottom.add_widget(self.btn_cancel) bottom.add_widget(self.btn_primary) root.add_widget(bottom) def _label(self, text="", **kwargs): halign = kwargs.pop("halign", "left") valign = kwargs.pop("valign", "middle") lbl = Label(text=text, halign=halign, valign=valign, **kwargs) lbl.bind(size=lambda inst, *_: setattr(inst, "text_size", inst.size)) return lbl def _paint_box(self, widget, color, radius=0): with widget.canvas.before: bg_color = Color(*color) corner = (dp(radius), dp(radius)) bg = RoundedRectangle(pos=widget.pos, size=widget.size, radius=[corner, corner, corner, corner]) def _sync_bg(inst, *_): bg.pos = inst.pos bg.size = inst.size widget.bind(pos=_sync_bg, size=_sync_bg) widget._bg_color_instr = bg_color return widget def _set_box_color(self, widget, color): instr = getattr(widget, "_bg_color_instr", None) if instr: instr.rgba = color def _fit_button_text(self, btn, font_size=16): btn.font_size = font_size btn.halign = "center" btn.valign = "middle" btn.bind( size=lambda inst, *_: setattr(inst, "text_size", (max(inst.width - dp(12), 1), max(inst.height - dp(8), 1))) ) return btn def _panel_title(self, text): return self._label(text, font_size=21, bold=True, size_hint_y=None, height=dp(38)) def _row(self, left, right="", height=40): row = BoxLayout(size_hint_y=None, height=dp(height), spacing=dp(6)) row.add_widget(self._label(left)) if right: row.add_widget(self._label(right)) return row def _summary_value_row(self, label): row = BoxLayout(size_hint_y=None, height=dp(24), spacing=dp(8)) left = self._label(label, font_size=14) right = self._label("", font_size=14, halign="right") row.add_widget(left) row.add_widget(right) self.totals_box.add_widget(row) return right def _has_discount_step(self): return self._discounts_enabled() or self._loyalty_enabled() def _discounts_enabled(self): return bool(getattr(self.setup, "is_sleva", False)) def _loyalty_enabled(self): return bool(getattr(self.setup, "loyalty_system", "")) def _total_items(self) -> float: return round(float(getattr(self, "_base_total_before_money", 0.0) or 0.0), 2) def _discount(self) -> float: return round(float(self.discount_abs or 0), 2) def _to_pay(self) -> float: return max(round(float(getattr(self, "_current_total", self.ucet.total_czk()) or 0.0), 2), 0.0) def _paid(self) -> float: return round(sum((p.suma_czk for p in self.payments), 0.0), 2) def _settlement_rounding_delta(self) -> float: return round(sum((getattr(p, "rounding_delta", 0.0) or 0.0 for p in self.payments), 0.0), 2) def _account_round50(self) -> float: return round(self._paid() - self._to_pay(), 2) def _paid_effective(self) -> float: return round(self._paid() - self._settlement_rounding_delta(), 2) def _remaining(self) -> float: return max(round(self._to_pay() - self._paid_effective(), 2), 0.0) def _change(self) -> float: return max(round(self._paid_effective() - self._to_pay(), 2), 0.0) def _tip_total(self) -> float: return round(sum((getattr(p, "tip", 0.0) or 0.0 for p in self.payments), 0.0), 2) def _currency(self) -> str: return getattr(self.setup, "zkr_mena", "Kč") if self.setup else "Kč" def _money(self, value) -> str: return f"{float(value or 0):.2f} {self._currency()}" def _payment_round_step(self, ptype) -> float: mode = int(getattr(ptype, "round50", 0) or 0) return { 5: 0.05, 1: 0.50, 4: 1.00, 2: 10.00, }.get(mode, 0.0) def _round_amount_for_payment(self, ptype, amount_czk: float) -> tuple[float, float]: original = round(float(amount_czk or 0), 2) step = self._payment_round_step(ptype) if step <= 0: return original, 0.0 rounded_decimal = ( Decimal(str(original)) / Decimal(str(step)) ).quantize(Decimal("1"), rounding=ROUND_HALF_UP) * Decimal(str(step)) rounded = round(float(rounded_decimal), 2) return rounded, round(rounded - original, 2) def _coerce_discount_application(self, item) -> data.UcetDiscount: if isinstance(item, data.UcetDiscount): return item.model_copy(deep=True) if isinstance(item, dict): return data.UcetDiscount(**item) return data.UcetDiscount() def _new_adjustment_id(self) -> str: return str(uuid4()) def _line_key(self, pol) -> str: return str(getattr(pol, "line_id", "") or "") def _current_line_items(self, line_ids=None): allowed = {str(x) for x in (line_ids or []) if str(x)} result = [] for pol in (getattr(self.ucet, "poloz", []) or []): if allowed and self._line_key(pol) not in allowed: continue qty = float(getattr(pol, "pocet", 0) or 0) / max(int(getattr(pol, "delitel", 1) or 1), 1) if qty <= 0: continue if float(getattr(pol, "cena", 0) or 0) < 0: continue result.append(pol) return result def _price_for_level(self, pol, price_level): cenpol = self.cenik_map.get(int(getattr(pol, "id_card", 0) or 0)) if not cenpol: return None price_level = str(price_level or "").strip() cena = next((c for c in (getattr(cenpol, "ceny", []) or []) if str(getattr(c, "name", "")).strip() == price_level), None) if not cena: return None delitel = int(getattr(pol, "delitel", 1) or 1) if delitel == 2: unit_price = getattr(cena, "cena2", getattr(cena, "cena", 0)) elif delitel == 3: unit_price = getattr(cena, "cena3", None) if unit_price is None: unit_price = getattr(cena, "cena", 0) elif delitel == 4: unit_price = getattr(cena, "cena4", None) if unit_price is None: unit_price = getattr(cena, "cena", 0) else: unit_price = getattr(cena, "cena", 0) return cena, float(unit_price or 0) def _apply_price_level_to_lines(self, price_level, line_ids=None): allowed = {str(x) for x in (line_ids or []) if str(x)} for pol in (getattr(self.ucet, "poloz", []) or []): if allowed and self._line_key(pol) not in allowed: continue price_data = self._price_for_level(pol, price_level) if not price_data: continue cena, unit_price = price_data typ_menu = int(getattr(pol, "typ_menu", 0) or 0) if typ_menu not in (10, 11): pol.cena = round(unit_price, 4) pol.cenhlad = getattr(cena, "name", str(price_level)) pol.dph = getattr(cena, "dan", getattr(pol, "dph", "")) pol.mena = getattr(cena, "mena", getattr(pol, "mena", "")) def _line_total_current(self, pol) -> float: qty = float(getattr(pol, "pocet", 0) or 0) / max(int(getattr(pol, "delitel", 1) or 1), 1) return round(qty * float(getattr(pol, "cena", 0) or 0), 4) def _distribute_adjustment(self, items, amount: float) -> float: items = list(items or []) if not items: return 0.0 subtotal = round(sum(self._line_total_current(pol) for pol in items), 2) if subtotal <= 0: return 0.0 amount = round(float(amount or 0), 2) if amount > 0: amount = min(amount, subtotal) if abs(amount) < 0.005: return 0.0 remaining = amount applied = 0.0 for idx, pol in enumerate(items): line_total = round(self._line_total_current(pol), 2) if line_total <= 0: continue if idx == len(items) - 1: delta = remaining else: delta = round(amount * (line_total / subtotal), 2) remaining = round(remaining - delta, 2) new_total = round(line_total - delta, 2) if new_total < 0: delta = line_total new_total = 0.0 qty = float(getattr(pol, "pocet", 0) or 0) / max(int(getattr(pol, "delitel", 1) or 1), 1) if qty <= 0: continue pol.cena = round(new_total / qty, 4) applied = round(applied + delta, 2) return applied def _update_discount_metadata(self): total = round(float(self.ucet.total_czk()), 2) self._current_total = total self.discount_abs = round(self._base_total_before_money - total, 2) self.discount_pct = None money_apps = [app for app in self.applied_discounts if int(app.typ_zlavy or 0) != 2] price_apps = [app for app in self.applied_discounts if int(app.typ_zlavy or 0) == 2] self.selected_discount = self.applied_discounts[-1] if self.applied_discounts else None self.ucet.discounts_applied = [app.model_copy(deep=True) for app in self.applied_discounts] self.ucet.discounts_prorated = True self.ucet.discount_abs = float(self.discount_abs) names = [app.name for app in money_apps if app.name] self.ucet.discount_name = ", ".join(names) self.ucet.discount_id = money_apps[-1].id_zlavy if money_apps else (price_apps[-1].id_zlavy if price_apps else None) try: self.ucet.sumdph() except Exception: Logger.exception("PAYMENT recalculate: DPH recompute failed") def _has_overpayment(self) -> bool: return self._paid_effective() > self._to_pay() + 0.009 def _warn_overpayment_if_needed(self): if self._has_overpayment(): self._popup_info( "Platba", "Zadané platby sú vyššie ako hodnota účtu po zľavách. Odstráňte alebo upravte niektorú platbu.", ) def _discard_pending_payment_adjustments(self): remove_ids = set(getattr(self, "_next_payment_adjustment_ids", []) or []) if not remove_ids: return self.applied_discounts = [ app for app in self.applied_discounts if app.id not in remove_ids ] self._next_payment_adjustment_ids = [] self.recalculate() def recalculate(self, show_overpaid_warning: bool = False): if self._recalculating: return self._recalculating = True try: self.ucet.poloz = [ pol.model_copy(deep=True) for pol in self._base_poloz ] for pol in (getattr(self.ucet, "poloz", []) or []): pol.cena_puv = float(getattr(pol, "cena", 0) or 0) price_apps = [app for app in self.applied_discounts if int(app.typ_zlavy or 0) == 2] for app in price_apps: if app.price_level: self._apply_price_level_to_lines(app.price_level, app.line_ids) for pol in (getattr(self.ucet, "poloz", []) or []): pol.cena_puv = float(getattr(pol, "cena", 0) or 0) self._base_total_before_money = round(float(self.ucet.total_czk()), 2) absolute_apps = [ app for app in self.applied_discounts if int(app.typ_zlavy or 0) in (3, 5) ] percent_apps = [ app for app in self.applied_discounts if int(app.typ_zlavy or 0) in (1, 4) ] for app in absolute_apps + percent_apps: items = self._current_line_items(app.line_ids) base = round(sum(self._line_total_current(pol) for pol in items), 2) if base <= 0: app.amount = 0.0 continue typ = int(app.typ_zlavy or 1) if typ in (1, 4): pct = float(app.pct if app.pct is not None else app.value or 0) amount = round(base * pct / 100.0, 2) if typ == 4: amount = -amount app.pct = pct else: amount = abs(float(app.value or app.amount or 0)) if typ == 5: amount = -amount applied = self._distribute_adjustment(items, amount) app.amount = round(applied, 2) self._update_discount_metadata() finally: self._recalculating = False if show_overpaid_warning: self._warn_overpayment_if_needed() def _receipt_copies(self) -> int: if self.payments: return max(max(int(getattr(p, "p_kopii", 1) or 0), 0) for p in self.payments) return 1 def _can_finish(self): if not self.payments: return False return abs(self._paid_effective() - self._to_pay()) <= 0.009 def _refresh(self): self.lbl_step.text = ( "1/2 Zľavy a vernostná karta" if self.step == self.STEP_DISCOUNT else "2/2 Platby a potvrdenie" ) sumazlavy=self._discount() self.lbl_to_pay_top.text = f"K úhrade: {self._money(self._to_pay())}" self.lbl_customer.text = self._customer_title() self.lbl_table.text = self._account_subtitle() self.lbl_total.text = self._money(self._total_items()) if sumazlavy > 0: self.lbl_discount.text = f"-{float(sumazlavy or 0):.2f} {self._currency()}" elif sumazlavy == 0: self.lbl_discount.text = f"{float(sumazlavy or 0):.2f} {self._currency()}" else: self.lbl_discount.text = f"+{float(-sumazlavy or 0):.2f} {self._currency()}" self.lbl_after_discount.text = self._money(self._to_pay()) self.lbl_tip.text = self._money(self._tip_total()) self.lbl_paid.text = self._money(self._paid()) remain_text = self._money(self._remaining()) if self._change() > 0: remain_text = self._money(self._change()) self.lbl_remain.text = remain_text self.lbl_loyalty.text = self._loyalty_summary() self.btn_summary_action.text = self._summary_action_text() self.btn_summary_copies.text = f"Kópií: {self._receipt_copies()}" if self.payments else "Kópie" povolene = getattr(self.setup, "nkop_edit", True) if self.setup else True if povolene : self.btn_summary_copies.disabled = not bool(self.payments) else: self.btn_summary_copies.disabled = True self._refresh_printer_button() self._refresh_summary_list() self.btn_discount_step.disabled = not self._has_discount_step() self.btn_payment_step.disabled = False self.btn_back.disabled = self.step == self.STEP_DISCOUNT self.btn_primary.text = ( "Pokračovať na platbu" if self.step == self.STEP_DISCOUNT else "Vykonať platbu" ) self.btn_primary.disabled = ( self.step == self.STEP_PAYMENT and not self._can_finish() ) self._refresh_primary_button_style() if self.step == self.STEP_DISCOUNT: self._render_discount_step() else: self._render_payment_step() def _refresh_summary_list(self): self.summary_box.clear_widgets() for app in self.applied_discounts: typ = int(app.typ_zlavy or 1) if typ == 2: value = f"hladina {app.price_level}" else: value = app.amount value = f"-{float(value or 0):.2f} {self._currency()}" if value > 0 else f"+{float(-value or 0):.2f} {self._currency()}" self.summary_box.add_widget(self._row( app.name or "Úprava účtu", value, )) if self.loyalty_card or self.loyalty_name: self.summary_box.add_widget(self._row("Vernosť", self._loyalty_summary())) for pay in self.payments: self.summary_box.add_widget(self._payment_summary_row(pay)) if self.payments: rounding_total = self._account_round50() if abs(rounding_total) >= 0.005: self.summary_box.add_widget(self._row("Zaokrúhlenie", self._money(rounding_total))) def _customer_title(self): if self.loyalty_name: return self.loyalty_name target = getattr(self.ucet, "hotel_charge", None) if not target: target = next((getattr(pay, "hotel_charge", None) for pay in self.payments if getattr(pay, "hotel_charge", None)), None) if target: guest_name = str(getattr(target, "guest_name", "") or "").strip() if guest_name: return guest_name return f"Účet {getattr(self.ucet, 'stul', '') or ''}".strip() or "Účet" def _account_subtitle(self): parts = [] stul = getattr(self.ucet, "stul", None) if stul: parts.append(f"Stôl {stul}") guests = getattr(self.ucet, "guest_count", 0) or 0 if guests: parts.append(f"{guests} hostí") return ", ".join(parts) def _refresh_primary_button_style(self): if self.step == self.STEP_PAYMENT and self._can_finish(): self.btn_primary.background_color = (0.2, 0.6, 0.2, 1) else: #self.btn_primary.background_normal = "" self.btn_primary.background_color = (0.32, 0.32, 0.32, 1) def _summary_action_text(self): if not self.payments: return "Predúčet" return "Účtenka emailom\nÁno" if self.send_receipt_email else "Účtenka emailom\nNie" def _payment_summary_row(self, pay): row = BoxLayout(size_hint_y=None, height=dp(64), spacing=dp(5), padding=(dp(4), 0, dp(4), 0)) self._paint_box(row, (0.23, 0.23, 0.23, 1), radius=3) left = BoxLayout(orientation="vertical", spacing=dp(1)) label = pay.ptype.name amount_text = self._money(pay.suma_czk) if float(pay.ptype.rate or 1.0) != 1.0: label = f"{pay.ptype.name}: {pay.suma:.2f} {pay.ptype.unit}" amount_text = self._money(pay.suma_czk) left.add_widget(self._label(label, font_size=14, size_hint_y=None, height=dp(24))) detail = self._payment_detail_text(pay) left.add_widget(self._label(detail, font_size=12, size_hint_y=None, height=dp(20))) row.add_widget(left) row.add_widget(self._label(amount_text, font_size=14, halign="right", size_hint=(None, 1), width=dp(82))) btn_tip = Button(text=self._tip_button_text(pay), size_hint=(None, 1), width=dp(58)) self._fit_button_text(btn_tip, font_size=12) btn_tip.bind(on_release=lambda _, pid=pay.id: self._ask_tip(pid)) row.add_widget(btn_tip) btn = Button(text="X", size_hint=(None, 1), width=dp(36)) btn.bind(on_press=lambda _, pid=pay.id: self._remove_payment(pid)) row.add_widget(btn) return row def _payment_detail_text(self, pay): parts = [] if getattr(pay, "note", None): parts.append(str(pay.note)) if getattr(pay, "hotel_charge", None): target = pay.hotel_charge room = str(getattr(target, "room_number", "") or "").strip() guest = str(getattr(target, "guest_name", "") or "").strip() hotel = str(getattr(target, "reception_name", "") or "").strip() target_text = " / ".join(x for x in (hotel, room, guest) if x) if target_text: parts.append(target_text) if self._terminal_result_success(pay): parts.append("Terminál OK") elif self._requires_direct_terminal_payment(pay): parts.append("Terminál pri ukončení") if getattr(pay, "tip", 0): parts.append(f"TIP {self._money(pay.tip)}") return " | ".join(parts) or "Zadať TIP" def _tip_button_text(self, pay): tip = getattr(pay, "tip", 0.0) or 0.0 return f"TIP\n{tip:.2f}" if tip else "TIP" def _loyalty_summary(self): if self.loyalty_name: return self.loyalty_name if self.loyalty_card: return f"Karta {self.loyalty_card}" return "Vernosť: -" def _render_discount_step(self): self.content.clear_widgets() self.content.add_widget(self._panel_title("Zľavy a vernostný systém")) if self._discounts_enabled(): discounts = self._available_discounts() if discounts: self.content.add_widget(self._label("Dostupné zľavy na účet", size_hint_y=None, height=dp(28))) grid = GridLayout(cols=2, spacing=dp(6), size_hint_y=None) grid.bind(minimum_height=grid.setter("height")) for zlava in discounts: btn = Button( text=self._discount_button_text(zlava), size_hint_y=None, height=dp(58), ) self._fit_button_text(btn, font_size=15) btn.bind(on_press=lambda _, z=zlava: self._select_discount(z)) grid.add_widget(btn) scroll = ScrollView(do_scroll_x=False, do_scroll_y=True) scroll.add_widget(grid) self.content.add_widget(scroll) else: self.content.add_widget(self._label("Nie je dostupná žiadna zľava na účet.")) else: self.content.add_widget(self._label("Zľavy nie sú povolené v nastavení pokladne.")) loyalty_box = BoxLayout(size_hint_y=None, height=dp(58), spacing=dp(6)) if self._loyalty_enabled(): btn_card = Button(text="Načítať kartu") btn_system = Button(text="Vernostný systém") self._fit_button_text(btn_card) self._fit_button_text(btn_system) btn_card.bind(on_press=lambda *_: self._scan_loyalty_card()) btn_system.bind(on_press=lambda *_: self._open_loyalty_system()) loyalty_box.add_widget(btn_card) loyalty_box.add_widget(btn_system) self.content.add_widget(loyalty_box) actions = BoxLayout(size_hint_y=None, height=dp(54), spacing=dp(6)) btn_clear = Button(text="Bez zľavy") btn_continue = Button(text="Pokračovať") self._fit_button_text(btn_clear) self._fit_button_text(btn_continue) btn_clear.bind(on_press=lambda *_: self._clear_discount()) btn_continue.bind(on_press=lambda *_: self._go_payment()) actions.add_widget(btn_clear) actions.add_widget(btn_continue) self.content.add_widget(actions) def _render_payment_step(self): self.content.clear_widgets() self.content.add_widget(self._panel_title("Druhy platby")) allowed = self._allowed_payment_types() if not allowed: self.content.add_widget(self._label("Nie je dostupný žiadny povolený druh platby.")) return grid = GridLayout(cols=2, spacing=dp(6), size_hint_y=None) grid.bind(minimum_height=grid.setter("height")) for ptype in allowed: text = self._payment_button_text(ptype) btn = Button( text=text, size_hint_y=None, height=self._payment_button_height(text), ) self._fit_button_text(btn, font_size=14) btn.bind(on_press=lambda _, p=ptype: self._begin_add_payment(p)) grid.add_widget(btn) scroll = ScrollView(do_scroll_x=False, do_scroll_y=True) scroll.add_widget(grid) self.content.add_widget(scroll) if self.payments: fiscal = "fiškálne" if self.payments[0].ptype.fiscal else "nefiškálne" self.content.add_widget(self._label( f"Ďalšie platby sú filtrované na {fiscal} platby.", size_hint_y=None, height=dp(34), )) def _payment_button_text(self, ptype): text = str(getattr(ptype, "name", "") or "").strip() if getattr(ptype, "unit", ""): text += f"\n{ptype.unit}" return text def _payment_button_height(self, text): line_count = sum( max(1, (len(line) + 23) // 24) for line in (str(text or "").splitlines() or [""]) ) return dp(max(76, 30 + line_count * 24)) def _go_discount(self): if not self._has_discount_step(): return self.step = self.STEP_DISCOUNT self._refresh() def _go_payment(self): self.step = self.STEP_PAYMENT self._refresh() self._open_preferred_payment_once() def _back(self): if self.step == self.STEP_PAYMENT and self._has_discount_step(): self.step = self.STEP_DISCOUNT self._refresh() def _primary(self): if self.step == self.STEP_DISCOUNT: self._go_payment() return self._finish() def _open_preferred_payment_once(self): if self._preferred_opened or not self.preferred_payment: return if self.preferred_payment not in self.payment_types: return self._preferred_opened = True Clock.schedule_once(lambda *_: self._begin_add_payment(self.preferred_payment), 0.05) def _available_discounts(self): now = datetime.now() return [ zlava for zlava in self.discounts if self._discount_allowed(zlava, now) ] def _discount_allowed(self, zlava, now): if getattr(zlava, "naucet", 0) != 1: return False if self._has_single_only_discount(): return False if int(getattr(zlava, "ibajedna", 0) or 0) == 1 and self.applied_discounts: return False if not self._discount_permission_ok(zlava): return False if not self._date_ok(zlava, now.date()): return False if not self._time_ok(zlava, now.time()): return False if not self._day_ok(zlava, now.isoweekday()): return False if not self._month_ok(zlava, now.month): return False if int(getattr(zlava, "ajpocenovejhladine", 0) or 0) != 1 and self._has_nondefault_price_level(): return False if int(getattr(zlava, "ajpozlavenapolozku", 0) or 0) != 1 and self._has_item_discount(): return False if not self._discountable_items(zlava): return False return True def _has_single_only_discount(self) -> bool: return any(int(getattr(app, "ibajedna", 0) or 0) == 1 for app in self.applied_discounts) def _discount_permission_ok(self, zlava): if self.discounts_all_allowed: return True if not self.discount_permissions: return False candidates = { str(getattr(zlava, "idriadok", "")).strip(), str(getattr(zlava, "meno", "")).strip(), str(getattr(zlava, "prg_dotaz", "")).strip(), str(getattr(zlava, "premenna", "")).strip(), } return bool(candidates & self.discount_permissions) def _date_ok(self, zlava, today): datumod = self._as_date(getattr(zlava, "datumod", None)) datumdo = self._as_date(getattr(zlava, "datumdo", None)) if datumod and today < datumod: return False if datumdo and today > datumdo: return False return True def _as_date(self, value): if not value: return None if isinstance(value, datetime): return value.date() if isinstance(value, date): return value try: return date.fromisoformat(str(value)[:10]) except Exception: return None def _time_ok(self, zlava, now_time): casod = self._as_time(getattr(zlava, "casod", "")) casdo = self._as_time(getattr(zlava, "casdo", "")) if not casod and not casdo: return True if casod and not casdo: return now_time >= casod if casdo and not casod: return now_time <= casdo if casod <= casdo: return casod <= now_time <= casdo return now_time >= casod or now_time <= casdo def _as_time(self, value): value = str(value or "").strip() if not value: return None try: hh, mm = value[:5].split(":", 1) return dt_time(int(hh), int(mm)) except Exception: return None def _day_ok(self, zlava, day_no): flags = [int(getattr(zlava, f"den{i}", 0) or 0) for i in range(1, 8)] if not any(flags): return True return flags[day_no - 1] != 0 def _month_ok(self, zlava, month_no): flags = [int(getattr(zlava, f"mes{i:02}", 0) or 0) for i in range(1, 13)] if not any(flags): return True return flags[month_no - 1] != 0 def _discount_button_text(self, zlava): return f"{self._discount_name(zlava)}\n{self._discount_value_text(zlava)}" def _discount_name(self, zlava): name = str(getattr(zlava, "meno", "") or "").strip() return name or f"Zľava {getattr(zlava, 'idriadok', '')}" def _discount_value_text(self, zlava): amount, pct = self._discount_amount_for(zlava) typ = int(getattr(zlava, "typ_zlavy", 1) or 1) zadanie = int(getattr(zlava, "zl_zadanie", 1) or 1) if zadanie == 1: if typ == 2: return "Zadať cenovú hladinu" return "Zadať %" if typ == 1 or typ == 4 else f"Zadať {self._currency()}" if typ == 2: return f"Cenová hladina {int(float(getattr(zlava, 'zl_koef', 0) or 0))}" sign = "-" if amount >= 0 else "+" shown = abs(amount) if pct is not None: return f"{pct:.2f}% ({sign}{shown:.2f} {self._currency()})" return f"{sign}{shown:.2f} {self._currency()}" def _discount_amount_for(self, zlava): total = self._discount_base(zlava) koef = float(getattr(zlava, "zl_koef", 0) or 0) typ = int(getattr(zlava, "typ_zlavy", 1) or 1) zadanie = int(getattr(zlava, "zl_zadanie", 1) or 1) if typ == 2 or zadanie == 1: return 0.0, None if typ == 3: amount = round(koef, 2) if amount > 0: amount = min(amount, total) return amount, None if typ == 5: amount = round(-koef, 2) return amount, None pct = round(koef, 2) amount = round(total * pct / 100.0, 2) if typ == 4: amount = -amount if amount > 0: amount = min(amount, total) return amount, pct def _select_discount(self, zlava): if int(getattr(zlava, "zl_zadanie", 1) or 1) == 1: self._ask_discount_value(zlava) return if int(getattr(zlava, "typ_zlavy", 1) or 1) == 2: self._apply_price_level_discount(zlava) return amount, pct = self._discount_amount_for(zlava) self._apply_selected_discount(zlava, amount, pct) def _apply_price_level_discount(self, zlava): try: price_level = str(int(float(getattr(zlava, "zl_koef", 0) or 0))) except Exception: self._popup_info("Zľava", "Neplatná cenová hladina.") return if not price_level or price_level == "0": self._popup_info("Zľava", "Neplatná cenová hladina.") return self._apply_selected_discount(zlava, 0.0, None, price_level=price_level) def _make_discount_application(self, zlava, amount=0.0, pct=None, price_level="", source="account", payment_type=None): typ = int(getattr(zlava, "typ_zlavy", 1) or 1) if typ == 2 and not price_level: price_level = str(int(float(getattr(zlava, "zl_koef", 0) or 0))) line_ids = [ self._line_key(pol) for pol in self._discountable_items(zlava) if self._line_key(pol) ] if not line_ids: line_ids = [self._line_key(pol) for pol in self._current_line_items() if self._line_key(pol)] if typ in (1, 4): value = float(pct if pct is not None else getattr(zlava, "zl_koef", 0) or 0) elif typ == 2: value = float(price_level or 0) else: value = abs(float(amount or getattr(zlava, "zl_koef", 0) or 0)) return data.UcetDiscount( id=self._new_adjustment_id(), id_zlavy=getattr(zlava, "idriadok", None), name=self._discount_name(zlava), typ_zlavy=typ, source=source, value=value, amount=round(float(amount or 0), 2), pct=pct, price_level=str(price_level or ""), ibajedna=int(getattr(zlava, "ibajedna", 0) or 0), line_ids=line_ids, payment_code=str(getattr(payment_type, "code", "") or ""), payment_name=str(getattr(payment_type, "name", "") or ""), ) def _apply_selected_discount(self, zlava, amount, pct, price_level=""): app = self._make_discount_application( zlava, amount=amount, pct=pct, price_level=price_level, source="account", ) self.applied_discounts.append(app) self.recalculate(show_overpaid_warning=True) self._refresh() def _ask_discount_value(self, zlava): typ = int(getattr(zlava, "typ_zlavy", 1) or 1) if typ == 2: def accept_level(val: str): try: price_level = int(float(str(val or "0").replace(",", "."))) except Exception: return if price_level <= 0: return if hasattr(zlava, "model_copy"): selected = zlava.model_copy(update={"zl_koef": float(price_level)}) else: selected = zlava selected.zl_koef = float(price_level) self._apply_price_level_discount(selected) NumberPad( mode="number", allow_fraction=False, decimal_places=0, max_len=3, initial_value=str(int(float(getattr(zlava, "zl_koef", 0) or 0)) or ""), on_accept=accept_level, ).open() return base = self._discount_base(zlava) def accept(val: str): value = self._parse_amount(val) if value <= 0: return if typ == 1: pct = min(value, 100.0) amount = round(base * pct / 100.0, 2) self._apply_selected_discount(zlava, amount, pct) return if typ == 4: pct = value amount = -round(base * pct / 100.0, 2) self._apply_selected_discount(zlava, amount, pct) return if typ == 5: amount = -value self._apply_selected_discount(zlava, amount, None) return amount = min(round(value, 2), base) self._apply_selected_discount(zlava, amount, None) NumberPad( mode="number", allow_fraction=False, decimal_places=2, max_len=7, initial_value="0.00", on_accept=accept, ).open() def _line_total(self, pol) -> float: qty = float(getattr(pol, "pocet", 0) or 0) / max(int(getattr(pol, "delitel", 1) or 1), 1) return round(qty * float(getattr(pol, "cena", 0) or 0), 2) def _discount_base(self, zlava) -> float: return round(sum(self._line_total(pol) for pol in self._discountable_items(zlava)), 2) def _discountable_items(self, zlava): return [ pol for pol in (getattr(self.ucet, "poloz", []) or []) if self._discount_applies_to_item(zlava, pol) ] def _discount_applies_to_item(self, zlava, pol) -> bool: if self._item_no_discount(pol): return False if int(getattr(zlava, "vs_druhy", 1) or 0) == 1: return True item_id = int(getattr(pol, "id_card", 0) or 0) item_kind = int(getattr(pol, "c_druh", 0) or 0) allowed_cards = {int(getattr(item, "c_karty", 0) or 0) for item in (getattr(zlava, "kalky", []) or [])} allowed_kinds = {int(getattr(item, "c_druh", 0) or 0) for item in (getattr(zlava, "druhy", []) or [])} return bool((item_id and item_id in allowed_cards) or (item_kind and item_kind in allowed_kinds)) def _item_no_discount(self, pol) -> bool: cenpol = self.cenik_map.get(int(getattr(pol, "id_card", 0) or 0)) attrs = [str(attr).strip().lower() for attr in (getattr(cenpol, "atributes", []) or [])] return any(attr in {"nodiscount", "no_discount", "bez_zlavy"} for attr in attrs) def _has_nondefault_price_level(self) -> bool: for pol in (getattr(self.ucet, "poloz", []) or []): current = str(getattr(pol, "cenhlad", "") or "").strip() default = str( getattr(pol, "def_hlad", "") or "" ).strip() if default and current and current != default: return True return False def _has_item_discount(self) -> bool: for pol in (getattr(self, "_base_poloz", []) or []): old_price = getattr(pol, "cena_puv", None) if old_price is not None and abs(float(old_price or 0) - float(getattr(pol, "cena", 0) or 0)) > 0.004: return True return False def _clear_discount(self): self.applied_discounts = [] self.selected_discount = None self.discount_abs = 0.0 self.discount_pct = None self.recalculate(show_overpaid_warning=True) self._refresh() def _restore_price_level_discount(self): self.recalculate() def _clear_payments_after_total_change(self): self.recalculate(show_overpaid_warning=True) def _scan_loyalty_card(self): NumberPad( mode="code", max_len=40, allow_text=True, auto_accept_scanner=True, on_accept=self._set_loyalty_card, ).open() def _open_loyalty_system(self): system = getattr(self.setup, "loyalty_system", "") self._popup_info( "Vernostný systém", f"Vyhľadávanie pre systém {system} zatiaľ nie je pripojené.\n" "Kartu je možné načítať čítačkou alebo klávesnicou.", ) self._scan_loyalty_card() def _set_loyalty_card(self, value): value = str(value or "").strip() if not value: return self.loyalty_card = value self.loyalty_name = f"Karta {value}" self.loyalty_info = "" self._refresh() def _allowed_payment_types(self): if not self.payments: return self.payment_types fiscal = bool(self.payments[0].ptype.fiscal) return [p for p in self.payment_types if bool(p.fiscal) == fiscal] def _begin_add_payment(self, ptype: data.PaymentType): if ptype not in self._allowed_payment_types(): self._popup_info("Platba", "Po prvej platbe je možné doplniť iba platby rovnakého fiškálneho typu.") return if self._remaining() <= 0.009 and not getattr(ptype, "is_cash", False): return if not self._run_payment_handler(ptype): return self._ask_payment_amount(ptype) def _run_payment_handler(self, ptype): handler = getattr(ptype, "handler", None) self._next_payment_note = None self._next_payment_adjustment_ids = [] self._handler_ptype = ptype if not handler: return True if not callable(self.handler_runner): Logger.info(f"Payment handler not connected: {handler}") self.handler_info = f"{handler}: nepripojené" return True try: try: result = self.handler_runner(ptype=ptype, ucet=self.ucet, dialog=self) except TypeError: result = self.handler_runner(ptype, self.ucet, self) except Exception as e: self._popup_info("Platba", f"Obslužný program platby zlyhal:\n{e}") return False if result is False: return False self._apply_handler_result(result) return True def _apply_handler_result(self, result): if not isinstance(result, dict): return if "note" in result: self._next_payment_note = str(result.get("note") or "") if "hotel_charge" in result: self._next_payment_hotel_charge = result.get("hotel_charge") Logger.info( "PAYMENT handler hotel target: payment=%s target=%s", getattr(result.get("payment_type", None), "code", ""), self._next_payment_hotel_charge.model_dump(mode="json") if hasattr(self._next_payment_hotel_charge, "model_dump") else self._next_payment_hotel_charge, ) payment_type = self._handler_ptype adjustment_added = False if "price_level" in result: level = str(result.get("price_level") or "").strip() if level: app = data.UcetDiscount( id=self._new_adjustment_id(), name=f"{getattr(payment_type, 'name', '')}: cenová hladina {level}", typ_zlavy=2, source="payment", value=float(level) if str(level).replace(".", "", 1).isdigit() else 0.0, price_level=level, payment_code=str(getattr(payment_type, "code", "") or ""), payment_name=str(getattr(payment_type, "name", "") or ""), ) self.applied_discounts.append(app) self._next_payment_adjustment_ids.append(app.id) adjustment_added = True if "discount_abs" in result: amount = float(result.get("discount_abs") or 0) if amount: line_ids = [ self._line_key(pol) for pol in self._current_line_items() if self._line_key(pol) and not self._item_no_discount(pol) ] app = data.UcetDiscount( id=self._new_adjustment_id(), name=f"{getattr(payment_type, 'name', '')}: zľava", typ_zlavy=3 if amount > 0 else 5, source="payment", value=abs(amount), amount=amount, line_ids=line_ids, payment_code=str(getattr(payment_type, "code", "") or ""), payment_name=str(getattr(payment_type, "name", "") or ""), ) self.applied_discounts.append(app) self._next_payment_adjustment_ids.append(app.id) adjustment_added = True if "discount_pct" in result: pct = float(result.get("discount_pct") or 0) if pct: line_ids = [ self._line_key(pol) for pol in self._current_line_items() if self._line_key(pol) and not self._item_no_discount(pol) ] app = data.UcetDiscount( id=self._new_adjustment_id(), name=f"{getattr(payment_type, 'name', '')}: zľava {pct:g}%", typ_zlavy=1 if pct > 0 else 4, source="payment", value=abs(pct), pct=abs(pct), line_ids=line_ids, payment_code=str(getattr(payment_type, "code", "") or ""), payment_name=str(getattr(payment_type, "name", "") or ""), ) self.applied_discounts.append(app) self._next_payment_adjustment_ids.append(app.id) adjustment_added = True if adjustment_added: self.recalculate(show_overpaid_warning=True) if "handler_info" in result: self.handler_info = str(result.get("handler_info") or "") elif "price_level" in result: self.handler_info = f"Cenová hladina: {result.get('price_level')}" elif self._next_payment_note: self.handler_info = self._next_payment_note self._refresh() def _ask_payment_amount(self, ptype: data.PaymentType): remaining_czk = float(self._remaining()) rate = float(ptype.rate or 1.0) initial = remaining_czk / rate if rate != 1.0 else remaining_czk if self.quick_complete: if remaining_czk <= 0.009: self._discard_pending_payment_adjustments() return suma_czk, rounding_delta = self._round_amount_for_payment(ptype, remaining_czk) suma = round(suma_czk / rate, 2) if rate != 1.0 else suma_czk self._add_payment_amount( ptype, suma, suma_czk, self._next_payment_note, self._next_payment_hotel_charge, list(self._next_payment_adjustment_ids), rounding_delta=rounding_delta, ) return def accept(val: str): suma = self._parse_amount(val) if suma <= 0: self._discard_pending_payment_adjustments() return suma_czk_raw = round(suma * rate, 2) if not ptype.is_cash and suma_czk_raw > remaining_czk: suma_czk_raw = round(remaining_czk, 2) suma = round(suma_czk_raw / rate, 2) elif suma_czk_raw > remaining_czk + 0.009: self._popup_info("Platba", "Zadaná suma je vyššia ako zostávajúca hodnota účtu.") self._discard_pending_payment_adjustments() return suma_czk, rounding_delta = self._round_amount_for_payment(ptype, suma_czk_raw) suma = round(suma_czk / rate, 2) if rate != 1.0 else suma_czk if not ptype.allow_partial and abs((suma_czk - rounding_delta) - remaining_czk) > 0.009: self._popup_info("Platba", "Táto platba musí uhradiť celý zostávajúci zostatok.") self._discard_pending_payment_adjustments() return if suma_czk <= 0: self._discard_pending_payment_adjustments() return self._add_payment_amount( ptype, suma, suma_czk, self._next_payment_note, self._next_payment_hotel_charge, list(self._next_payment_adjustment_ids), rounding_delta=rounding_delta, ) NumberPad( mode="number", allow_fraction=False, decimal_places=2, max_len=7, initial_value=f"{initial:.2f}", on_accept=accept, on_cancel=lambda *_: self._discard_pending_payment_adjustments(), ).open() def _parse_amount(self, value): if not value: return 0.0 text = str(value).strip().replace(" ", "").replace(",", ".") try: if "/" in text: num, den = text.split("/", 1) return float(num) / float(den) return float(text) except Exception: return 0.0 def _add_payment_amount(self, ptype, suma, suma_czk, note=None, hotel_charge=None, adjustment_ids=None, rounding_delta: float = 0.0): code = str(ptype.code or "").upper() if code == "QR": texture = make_qr_texture( iban=self.setup.iban, amount_czk=suma_czk, message=f"Účet {self.ucet.ucislo or self.ucet.stul}", vs=self.ucet.ucislo, ) def confirmed(suma_=suma, suma_czk_=suma_czk, note_=note, hotel_charge_=hotel_charge, adjustment_ids_=adjustment_ids, rounding_delta_=rounding_delta): self.payments.append(PaymentItem( ptype=ptype, suma=round(suma_, 2), suma_czk=round(suma_czk_, 2), note=note_, hotel_charge=hotel_charge_, adjustment_ids=adjustment_ids_, rounding_delta=rounding_delta_, )) self._next_payment_hotel_charge = None self._next_payment_adjustment_ids = [] if self.quick_complete: self._finish() return self._refresh() QRPreviewDialog(texture=texture, on_confirm=confirmed).open() return self.payments.append(PaymentItem( ptype=ptype, suma=round(suma, 2), suma_czk=round(suma_czk, 2), note=note, hotel_charge=hotel_charge, adjustment_ids=adjustment_ids, rounding_delta=rounding_delta, )) Logger.info( "PAYMENT add: code=%s amount=%s hotel=%s note=%s", getattr(ptype, "code", ""), round(suma_czk, 2), bool(hotel_charge), note or "", ) self._next_payment_hotel_charge = None self._next_payment_adjustment_ids = [] if self.quick_complete: self._finish() return self._refresh() def _selected_printer_terminal_id(self) -> str: printer = self.printer_by_no.get(str(self.selected_printer or "")) return str(getattr(printer, "id_term", "") or "").strip() if printer else "" def _direct_bankterm_type(self) -> str: term_type = self._selected_bankterm_type() if term_type in {"", "AFS", "FISKAL", "FISCAL", "FISKALPRO", "EKASA"}: return "" return term_type def _terminal_result_success(self, pay) -> bool: result = getattr(pay, "terminal_result", {}) or {} return isinstance(result, dict) and bool(result.get("success")) def _requires_direct_terminal_payment(self, pay) -> bool: if not bool(getattr(getattr(pay, "ptype", None), "is_bankterm", False)): return False if self._terminal_result_success(pay): return False if not self._selected_printer_terminal_id(): return False return bool(self._direct_bankterm_type()) def _pending_direct_terminal_payments(self): return [ pay for pay in self.payments if self._requires_direct_terminal_payment(pay) ] def _has_completed_terminal_payment(self) -> bool: return any(self._terminal_result_success(pay) for pay in self.payments) def _remove_payment(self, pid: str): pay = self._find_payment_item(pid) if pay and self._terminal_result_success(pay): self._popup_info( "Platba", "Táto platba už prebehla cez bankový terminál a nie je možné ju len vymazať.", ) return removed = [p for p in self.payments if p.id == pid] remove_ids = { adj_id for p in removed for adj_id in (getattr(p, "adjustment_ids", []) or []) } if remove_ids: self.applied_discounts = [ app for app in self.applied_discounts if app.id not in remove_ids ] self.recalculate() self.payments = [p for p in self.payments if p.id != pid] self._refresh() def _find_payment_item(self, pid: str): return next((pay for pay in self.payments if pay.id == pid), None) def _ask_tip(self, pid: str): pay = self._find_payment_item(pid) if not pay: return def accept(val: str): tip = self._parse_amount(val) if tip < 0: return pay.tip = round(tip, 2) self._refresh() NumberPad( mode="number", allow_fraction=False, decimal_places=2, max_len=7, initial_value=f"{float(getattr(pay, 'tip', 0) or 0):.2f}", on_accept=accept, ).open() def _edit_receipt_copies(self, *_): if not self.payments: self._popup_info("Kópie", "Kópie sa nastavujú podľa zvolenej platby.") return def accept(val: str): try: copies = int(float(str(val or "0").replace(",", "."))) except Exception: return copies = max(copies, 0) for pay in self.payments: pay.p_kopii = copies self._refresh() NumberPad( mode="number", allow_fraction=False, decimal_places=0, max_len=2, initial_value=str(self._receipt_copies()), on_accept=accept, ).open() def _summary_action(self, *_): if not self.payments: self._print_preview_receipt() return self.send_receipt_email = not self.send_receipt_email self._refresh() def _print_preview_receipt(self): try: import kivy_printer preview_ucet = self.ucet.model_copy(deep=True) preview_ucet.bill_printer = self.selected_printer receipt_text = self.controller._render_receipt_preview_text( preview_ucet, kind="prebill", title="Preducet", printer_no=self.selected_printer, ) def print_prebill(): preview_ucet.bill_printer = self.selected_printer self.controller._enqueue_receipt_print_jobs( preview_ucet, kind="prebill", title="Preducet", copies=1, required=False, ) kivy_printer.show_receipt_preview( preview_ucet, width=40, txt="Preducet", currencytxt=self._currency(), kasutxt=self.kasutxt, receipt_text=receipt_text, on_print=print_prebill, print_label="TLAC", ) except Exception as e: self._popup_info("Predúčet", f"Predúčet sa nepodarilo zobraziť:\n{e}") def _refresh_printer_button(self): selected = str(self.selected_printer or "").strip() current = ( self.controller._client_setting_value("prn_no") if hasattr(self.controller, "_client_setting_value") else str((self.controller.client_settings or {}).get("prn_no", "") or "").strip() ) if selected and current != selected: room_name = ( self.controller._client_setting_value("room_name") if hasattr(self.controller, "_client_setting_value") else str((self.controller.client_settings or {}).get("room_name", "") or "").strip() ) self.controller.client_settings = api_call.save_clientsettings_API( self.controller.ctx, prn_no=selected, room_name=room_name, ) if hasattr(self.controller, "_set_client_setting_value"): self.controller._set_client_setting_value("prn_no", selected) self.controller.default_printer = selected name = self.printer_map.get(selected, selected) self.btn_printer.text = f"Tlač: {name or '-'}" def _open_printer_popup(self, *_): if not self.printers: self._popup_info("Tlačiareň", "Nie je dostupná žiadna tlačiareň.") return layout = BoxLayout(orientation="vertical", spacing=dp(5), padding=dp(8)) popup = Popup( title="Tlačiareň", content=layout, size_hint=(0.4, 0.6), ) for printer in self.printers: btn = Button(text=getattr(printer, "prn_name", getattr(printer, "prn_no", ""))) def select(_btn, prn=printer): self.selected_printer = getattr(prn, "prn_no", "") if self.on_printer_change: self.on_printer_change(self.selected_printer) self._refresh_printer_button() popup.dismiss() btn.bind(on_release=select) layout.add_widget(btn) popup.open() def _popup_info(self, title, text): box = BoxLayout(orientation="vertical", padding=dp(10), spacing=dp(10)) box.add_widget(self._label(text)) btn = Button(text="OK", size_hint_y=None, height=dp(42)) popup = ModalView(size_hint=(None, None), size=(dp(360), dp(220))) def _dismiss_after_touch(*_): Clock.schedule_once(lambda *_: popup.dismiss(), 0) btn.bind(on_release=_dismiss_after_touch) box.add_widget(btn) popup.add_widget(box) popup.open() def _run_direct_terminal_payments_before_finish(self): pending = self._pending_direct_terminal_payments() if not pending: return False if self.card_payment_running: self._popup_info("Platba", "Už prebieha komunikácia s bankovým terminálom.") return True self.card_payment_running = True terminal_name = self._direct_bankterm_type() or "terminál" popup = self._popup_wait( "Platba kartou", f"Prebieha komunikácia s bankovým terminálom ({terminal_name}).", ) def worker(): failed_message = "" failed_result = None completed = [] for pay in pending: result = self._call_terminal_api(pay.suma_czk) if result.get("success"): pay.terminal_result = result completed.append(pay) try: self._extract_receipt(result.get("raw") or {}) except Exception: Logger.exception("PAYMENT terminal receipt extraction failed") continue failed_result = result failed_message = result.get("error") or "Terminál odmietol transakciu." break def done(_dt): self.card_payment_running = False popup.dismiss() if failed_message: if completed: self._popup_info( "Platba zamietnutá", "Niektoré terminálové platby už prebehli, ale ďalšia transakcia zlyhala.\n" "Účet nie je uzavretý. Opravte platby alebo skúste vykonať platbu znova.\n" f"{failed_message}", ) else: self._popup_info("Platba zamietnutá", failed_message) self._refresh() return self._finish_after_terminal() Clock.schedule_once(done) threading.Thread(target=worker, daemon=True).start() return True def _finish(self): if self.card_payment_running: self._popup_info("Platba", "Už prebieha komunikácia s bankovým terminálom.") return if not self._can_finish(): if self._has_overpayment(): self._warn_overpayment_if_needed() return self.recalculate() if self._run_direct_terminal_payments_before_finish(): return self._finish_after_terminal() def _finish_after_terminal(self): self.recalculate() self.ucet.discount_abs = float(self._discount()) self.ucet.discounts_applied = [] self.ucet.discounts_prorated = True self.ucet.discount_name = ", ".join(app.name for app in self.applied_discounts if app.name) self.ucet.discount_id = self.applied_discounts[-1].id_zlavy if self.applied_discounts else None self.ucet.loyalty_card = self.loyalty_card self.ucet.loyalty_name = self.loyalty_name self.ucet.loyalty_info = self.loyalty_info self.ucet.bill_printer = self.selected_printer self.ucet.send_receipt_email = bool(self.send_receipt_email) self.ucet.round50 = float(self._account_round50()) self.ucet.platby = [] fallback_hotel_charge = getattr(self.ucet, "hotel_charge", None) for pay in self.payments: hotel_charge = pay.hotel_charge if not hotel_charge and fallback_hotel_charge and len(self.payments) == 1: hotel_charge = fallback_hotel_charge self.ucet.platby.append(data.Platba( code=pay.ptype.code, nazev=pay.ptype.name, suma=float(pay.suma), suma_czk=float(pay.suma_czk), unit=pay.ptype.unit, rate=float(pay.ptype.rate or 1.0), fiscal=bool(pay.ptype.fiscal), is_bankterm=bool(getattr(pay.ptype, "is_bankterm", False)), p_kopii=max(int(getattr(pay, "p_kopii", getattr(pay.ptype, "p_kopii", 1)) or 0), 0), tip=float(getattr(pay, "tip", 0) or 0), poznamka=pay.note, hotel_charge=hotel_charge, terminal_result=dict(getattr(pay, "terminal_result", {}) or {}), )) Logger.info( "PAYMENT finish: payments=%s hotel_payments=%s", len(self.ucet.platby), sum(1 for pay in self.ucet.platby if getattr(pay, "hotel_charge", None)), ) self.on_done(self.ucet) self.dismiss() def _cancel_all(self): if self._has_completed_terminal_payment(): self._popup_info( "Platba", "Nie je možné zrušiť platobné okno, pretože niektorá platba už prebehla cez bankový terminál.", ) return if self.card_payment_running: self._abort_transaction() if self.on_cancel: self.on_cancel() self.dismiss() PaymentDialog = GuidedPaymentDialog # ===================================================== # TEST # ===================================================== class TestApp(App): def build(self): types = [ data.PaymentType(code="CASH", name="Hotove", unit="Kc", is_cash=True, is_default=True), data.PaymentType(code="CARD", name="Karta", unit="Kc"), data.PaymentType(code="QR", name="QR platba", unit="Kc"), ] setup = data.PosSetup(platby=types) return PaymentDialog( ucet=data.Ucet(stul="1", poloz=[]), payment_types=types, setup=setup, on_done=lambda *_: None, ) if __name__ == "__main__": TestApp().run()