# test_posdialog.py import tst_data # posdialog.py from kivy.uix.popup import Popup from kivy.app import App from kivy.uix.screenmanager import ScreenManager from kivy.clock import Clock from kivy.uix.floatlayout import FloatLayout from kivy.uix.screenmanager import Screen from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.scrollview import ScrollView from kivy.uix.button import Button from kivy.uix.label import Label from kivy.metrics import dp from kivy.effects.scroll import ScrollEffect from kivy.uix.textinput import TextInput from kivy.logger import Logger from kivy.core.text import Label as CoreLabel from kivy.uix.widget import Widget from kivy.uix.modalview import ModalView from kivy.core.window import Window from collections import defaultdict from copy import deepcopy from numberpad import NumberPad from data import UcetEdit, UcPolEdit from functools import partial from time import time import data import payment import api_call import uuid import math import unicodedata import re from konstanty import * def messagebox(text, title="Info"): layout = BoxLayout( orientation="vertical", padding=10, spacing=10, ) layout.add_widget(Label( text=text, halign="center", valign="middle", )) btn = Button( text="OK", size_hint=(1, None), height=40, ) layout.add_widget(btn) popup = Popup( title=title, content=layout, size_hint=(None, None), size=(420, 200), auto_dismiss=False, ) def _dismiss_after_touch(*_): Clock.schedule_once(lambda *_: popup.dismiss(), 0) btn.bind(on_release=_dismiss_after_touch) popup.open() from kivy.uix.gridlayout import GridLayout from kivy.clock import Clock from kivy.uix.gridlayout import GridLayout from kivy.uix.button import Button from kivy.metrics import dp from kivy.clock import Clock DIACRITICS = { "a": ["á", "ä"], "c": ["č"], "d": ["ď"], "e": ["é", "ě"], "i": ["í"], "l": ["ľ", "ĺ"], "n": ["ň"], "o": ["ó", "ô"], "r": ["ŕ", "ř"], "s": ["š"], "t": ["ť"], "u": ["ú", "ů"], "y": ["ý"], "z": ["ž"], } class ModalManager: def __init__(self): self.stack = [] def open(self, modal): self.stack.append(modal) modal.open() def close(self, modal): if modal in self.stack: self.stack.remove(modal) modal.dismiss() def get_active(self): return self.stack[-1] if self.stack else None def dispatch_key(self, key): # 🔥 ESC rieš NAJPRV if key == "ESC": if self.close_top(): return True # 🔥 STOP → nepadne app modal = self.get_active() if modal and hasattr(modal, "handle_key"): return modal.handle_key(key) return False def close_top(self): if not self.stack: return False modal = self.stack.pop() modal.dismiss() return True @property def active_modal(self): return self.stack[-1] if self.stack else None class DiacriticPopup(Popup): def __init__(self, parent_keyboard, base_char, variants, **kwargs): super().__init__(**kwargs) self.size_hint = (None, None) self.size = (dp(300), dp(80)) self.auto_dismiss = True self.kb = parent_keyboard self.base = base_char layout = BoxLayout(spacing=dp(5), padding=dp(5)) for ch in variants: b = Button(text=ch, font_size=dp(22)) b.bind(on_release=lambda inst, c=ch: self._select(c)) layout.add_widget(b) self.add_widget(layout) def _select(self, ch): self.kb._insert(ch) self._ignore_mark_until = time() + 0.3 self.dismiss() from kivy.uix.boxlayout import BoxLayout class ActionPanel(BoxLayout): def on_touch_down(self, touch): # panel skrytý if self.opacity == 0: return False # klik mimo panel if not self.collide_point(*touch.pos): return False # klik v paneli return super().on_touch_down(touch) def on_touch_up(self, touch): if self.opacity == 0: return False if not self.collide_point(*touch.pos): return False return super().on_touch_up(touch) class PosKeyboard(GridLayout): def __init__(self, on_key=None, bezokesc=False, **kwargs): super().__init__(**kwargs) self.on_key = on_key self.bezokesc = bezokesc self.cols = 10 self.spacing = dp(4) self.size_hint_y = None self.height = dp(260) self.shift = False self.caps = False self.mode = "abc" self.LONGPRESS_TIME = 0.25 self._longpress_event = None self._longpress_triggered = False self._build() # ========================================================= # 🔨 BUILD # ========================================================= def _build(self): self.clear_widgets() switch_key = "123" if self.mode == "abc" else "ABC" if self.mode == "abc": if self.bezokesc: keys = [ "1","2","3","4","5","6","7","8","9","0", "q","w","e","r","t","y","u","i","o","p", "a","s","d","f","g","h","j","k","l","←", "shift","z","x","c","v","b","n","m",".", "caps","space", switch_key ] else: keys = [ "1","2","3","4","5","6","7","8","9","0", "q","w","e","r","t","y","u","i","o","p", "a","s","d","f","g","h","j","k","l","←", "shift","z","x","c","v","b","n","m",".", "caps","space", switch_key, "OK","ESC" ] else: if self.bezokesc: keys = [ "1","2","3","4","5","6","7","8","9","0", "+","-","/","*","=","%","(",")",".","←", "shift"," "," "," "," "," "," "," "," ", "caps","space", switch_key ] else: keys = [ "1","2","3","4","5","6","7","8","9","0", "+","-","/","*","=","%","(",")",".","←", "shift"," "," "," "," "," "," "," "," ", "caps","space", switch_key, "OK","ESC" ] for k in keys: self.add_widget(self._btn(k)) # ========================================================= # 🔘 BUTTON # ========================================================= def _btn(self, key): b = Button(text=key, font_size=dp(16)) if key == "←": b.bind(on_press=lambda *_: self._send("BACKSPACE")) elif key == "shift": b.bind(on_press=self._shift) elif key == "caps": b.bind(on_press=self._caps) elif key in ("123", "ABC"): b.bind(on_press=lambda *_: self._switch()) elif key == "space": b.bind(on_press=lambda *_: self._send(" ")) elif key == "OK": b.bind(on_press=lambda *_: self._send("ENTER")) elif key == "ESC": b.bind(on_press=lambda *_: self._send("ESC")) elif key.strip() == "": b.disabled = True elif key.isalpha(): b.bind(on_press=lambda inst, k=key: self._start_longpress(inst, k)) b.bind(on_release=lambda inst, k=key: self._end_longpress(inst, k)) else: b.bind(on_press=lambda *_: self._send(key)) # 🔵 highlight if key == "shift" and self.shift: b.background_normal="", b.background_color = (0.2, 0.6, 1, 1) if key == "caps" and self.caps: b.background_normal="", b.background_color = (0.2, 0.6, 1, 1) return b # ========================================================= # 📤 SEND KEY # ========================================================= def _send(self, key): if self.on_key: self.on_key(key) # ========================================================= # 🔤 INSERT LOGIC # ========================================================= def _format_char(self, key): if self.mode != "abc": return key if self.shift or self.caps: return key.upper() return key.lower() # ========================================================= # 🔠 SHIFT / CAPS / MODE # ========================================================= def _shift(self, *_): self.shift = not self.shift self._build() def _caps(self, *_): self.caps = not self.caps self._build() def _switch(self): self.mode = "num" if self.mode == "abc" else "abc" self._build() # ========================================================= # ⏱ LONG PRESS # ========================================================= def _start_longpress(self, btn, key): self._longpress_triggered = False if key.lower() in DIACRITICS: self._longpress_event = Clock.schedule_once( lambda dt: self._show_diacritics(key), self.LONGPRESS_TIME ) def _end_longpress(self, btn, key): if self._longpress_event: self._longpress_event.cancel() if not self._longpress_triggered: self._send(self._format_char(key)) if self.shift: self.shift = False self._build() def _show_diacritics(self, key): self._longpress_triggered = True variants = DIACRITICS.get(key.lower(), []) if self.shift or self.caps: variants = [v.upper() for v in variants] # 👉 jednoduché: pošli prvý variant # (ak chceš popup, doplníme) if variants: self._send(variants[0]) class BaseModal(ModalView): def __init__(self, modal_manager=None, **kwargs): super().__init__(**kwargs) self.modal_manager = modal_manager self.auto_dismiss = False def open(self, *args, **kwargs): super().open(*args, **kwargs) def close(self): if self.modal_manager: self.modal_manager.close(self) else: self.dismiss() def handle_key(self, key): return False # override class TextMessageDialog(BaseModal): def __init__(self, modal_manager, title="Zadaj správu", on_done=None, keyboard=None, **kwargs): super().__init__(modal_manager=modal_manager, size_hint=(None, None), size=(dp(500), dp(400)), **kwargs) self.size_hint = (None, None) self.size = (dp(600), dp(550)) self.auto_dismiss = False self.on_done = on_done root = BoxLayout(orientation="vertical", spacing=dp(8), padding=dp(10)) #root = BoxLayout(orientation="vertical", spacing=dp(10), padding=dp(10)) root.add_widget(Label(text=title, size_hint_y=None, height=dp(40))) self.input = TextInput( multiline=True, font_size=dp(22), size_hint_y=0.35, #size_hint=(1, 1), padding=(dp(12), dp(12)), background_normal="", background_active="", background_color=(0.1, 0.1, 0.1, 1), foreground_color=(1, 1, 1, 1), cursor_color=(1, 1, 1, 1) ) # 🔥 DÔLEŽITÉ – bez toho text nevidno správne self.input.bind( size=lambda inst, *_: setattr(inst, "text_size", (inst.width - dp(20), None)) ) root.add_widget(self.input) # 🔽 keyboard ide SEM if keyboard: root.add_widget(keyboard) # 🔘 buttons btns = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(10)) btn_cancel = Button(text=self.tr("button.cancel","Zrušiť"), background_color=(0.6,0.2,0.2,1),background_normal="") btn_ok = Button(text=self.tr("button.ok","OK"), background_color=(0.2,0.6,0.2,1),background_normal="") btn_cancel.bind(on_press=lambda *_: self.close()) btn_ok.bind(on_press=lambda *_: self._confirm()) btns.add_widget(btn_cancel) btns.add_widget(btn_ok) root.add_widget(btns) self.add_widget(root) def _confirm(self): if self.on_done: self.on_done(self.input.text.strip()) self.close() # 🔥 KLÁVESNICA def handle_key(self, key): if isinstance(key, int): if key == 13: key = "ENTER" elif key == 8: key = "BACKSPACE" if key in ("ENTER", "\r", "\n"): self._confirm() return True if key == "ESC": self.close() return True if key == "BACKSPACE": self.input.do_backspace() return True if key == "CLEAR": self.input.text = "" return True if isinstance(key, str) and len(key) == 1: self.input.insert_text(key) return True return False class SearchDialog(BaseModal): def __init__(self, modal_manager, parent, initial_text='',keyboard=None, tr=None, **kwargs): super().__init__(modal_manager=modal_manager, size_hint=(0.6, 0.8), **kwargs) self.parent_ref = parent root = BoxLayout(orientation="vertical", spacing=dp(6), padding=dp(10)) self.search_bar = TextInput( readonly=True, hint_text="Hľadať...", text=initial_text, size_hint_y=None, height=dp(40) ) root.add_widget(self.search_bar) # 🔽 keyboard ide SEM if keyboard: root.add_widget(keyboard) self.tr=tr btns = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(10)) btn_cancel = Button(text=self.tr("button.cancel","Zrušiť"), background_color=(0.6,0.2,0.2,1),background_normal="") btn_ok = Button(text=self.tr("button.ok","OK"), background_color=(0.2,0.6,0.2,1),background_normal="") btn_ok.bind(on_press=lambda *_: self._confirm()) btn_cancel.bind(on_press=lambda *_: self._cancel()) btns.add_widget(btn_cancel) btns.add_widget(btn_ok) root.add_widget(btns) self.add_widget(root) def _update(self): self.parent_ref._current_search_text = self.search_bar.text items = self.parent_ref._search_items(self.search_bar.text) self.parent_ref._build_menu_from_items(items) def _confirm(self): self.close() def _cancel(self): self.parent_ref._build_menu_from_cenik() self.close() # 🔥 KLÁVESNICA def handle_key(self, key): print(key) if isinstance(key, int): if key in (13, 40, 10, 271): key = "ENTER" elif key == 8: key = "BACKSPACE" if key == "ESC": self._cancel() return True if key == "ENTER": self._confirm() return True if key == "BACKSPACE": self.search_bar.text = self.search_bar.text[:-1] self._update() return True if key in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'): # top row digit = key elif ord(key) in range(256, 266): # numpad digit = str(ord(key) - 256) elif key=='*' or ord(key) == 268: digit = '*' else: digit = None if digit: self.search_bar.text += digit self._update() return True if isinstance(key, str) and len(key) == 1: self.search_bar.text += key self._update() return True return False class CreditCompanySelectDialog(BaseModal): def __init__(self, parent, companies, on_select, on_new, **kwargs): super().__init__( modal_manager=getattr(parent, "modal_manager", None), size_hint=(0.72, 0.82), **kwargs, ) self.parent_ref = parent self.companies = list(companies or []) self.on_select = on_select self.on_new = on_new root = BoxLayout(orientation="vertical", spacing=dp(8), padding=dp(10)) root.add_widget(Label(text=self.tr("pos.uver_hlavicka","Úverový záznam - výber firmy"), size_hint_y=None, height=dp(34))) self.search = TextInput( hint_text=self.tr("pos.uver_hint","Hľadať firmu, adresu, IČO, DIČ..."), multiline=False, size_hint_y=None, height=dp(44), ) self.search.bind(text=lambda *_: self._refresh_list()) root.add_widget(self.search) self.scroll = ScrollView( do_scroll_y=True, size_hint=(1, 1), bar_width=dp(18), scroll_type=["bars", "content"], ) self.list_box = BoxLayout(orientation="vertical", spacing=dp(5), size_hint_y=None) self.list_box.bind(minimum_height=self.list_box.setter("height")) self.scroll.add_widget(self.list_box) root.add_widget(self.scroll) btns = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(8)) btn_cancel = Button(text=self.tr("button.cancel","Zrušiť"), background_color=(0.6, 0.2, 0.2, 1),background_normal="") btn_new = Button(text=self.tr("pos.uver_nova_firma","Nová firma"),background_normal="") btn_cancel.bind(on_press=lambda *_: self.close()) btn_new.bind(on_press=lambda *_: self._new_company()) btns.add_widget(btn_cancel) btns.add_widget(btn_new) root.add_widget(btns) self.add_widget(root) self._refresh_list() Clock.schedule_once(lambda *_: setattr(self.search, "focus", True), 0.1) def _haystack(self, firma): return " ".join([ getattr(firma, "hjmeno", "") or "", getattr(firma, "adresa1", "") or "", getattr(firma, "adresa2", "") or "", getattr(firma, "adresa3", "") or "", getattr(firma, "ico", "") or "", getattr(firma, "icdph", "") or "", getattr(firma, "dic", "") or "", ]) def _matches(self, firma, query): terms = [ self.parent_ref._normalize_text(term) for term in str(query or "").split() if term.strip() ] if not terms: return True haystack = self.parent_ref._normalize_text(self._haystack(firma)) return all(term in haystack for term in terms) def _row_text(self, firma): detail = " ".join( x for x in ( getattr(firma, "adresa1", ""), getattr(firma, "ico", ""), getattr(firma, "dic", ""), ) if x ) name = getattr(firma, "hjmeno", "") or "(bez mena)" return f"{name}\n{detail}" if detail else name def _refresh_list(self): self.list_box.clear_widgets() filtered = [ firma for firma in self.companies if self._matches(firma, self.search.text) ] if not filtered: self.list_box.add_widget(Label( text=self.tr("pos.uver_nenajdena_firma","Žiadna firma nevyhovuje hľadaniu."), size_hint_y=None, height=dp(44), )) return for firma in filtered: btn = Button( text=self._row_text(firma), halign="left", valign="middle", size_hint_y=None, height=dp(62), ) btn.bind(size=lambda inst, *_: setattr(inst, "text_size", (inst.width - dp(16), None))) btn.bind(on_press=lambda _, f=firma: self._select(f)) self.list_box.add_widget(btn) def _select(self, firma): self.close() if self.on_select: self.on_select(firma) def _new_company(self): self.close() if self.on_new: self.on_new() def handle_key(self, key): key = self._key_name(key) if key == "ESC": self.close() return True if key == "ENTER": return True if key == "TAB": return True if key == "BACKSPACE": #if self.search.focus: # return True self.search.do_backspace() return True if key == "DELETE": #if self.search.focus: # return True try: self.search.do_backspace(mode="del") except TypeError: pass return True if key == "HOME": #if self.search.focus: # return True #self.search.cursor = (0, 0) self.scroll.scroll_y = 1 return True if key == "END": #if self.search.focus: # return True #self.search.cursor = (len(self.search.text), 0) self.scroll.scroll_y = 0 return True if key == "PGUP": self.scroll.scroll_y = min(1, self.scroll.scroll_y + 0.8) return True if key == "PGDN": self.scroll.scroll_y = max(0, self.scroll.scroll_y - 0.8) return True if isinstance(key, str) and len(key) == 1: if self.search.focus: return True self.search.focus = True self.search.insert_text(key) return True return False def _key_name(self, key): if isinstance(key, int): return { 8: "BACKSPACE", 9: "TAB", 13: "ENTER", 27: "ESC", 278: "HOME", 279: "END", 280: "PGUP", 281: "PGDN", }.get(key, key) aliases = { "escape": "ESC", "esc": "ESC", "backspace": "BACKSPACE", "\t": "TAB", "tab": "TAB", "delete": "DELETE", "home": "HOME", "end": "END", "pageup": "PGUP", "pagedown": "PGDN", "pgup": "PGUP", "pgdn": "PGDN", } return aliases.get(str(key).lower(), key) class CreditCompanyEditDialog(BaseModal): def __init__(self, parent, firma=None, on_done=None, **kwargs): super().__init__( modal_manager=getattr(parent, "modal_manager", None), size_hint=(0.72, 0.88), **kwargs, ) self.parent_ref = parent self.firma = firma or data.UverFirma() self.on_done = on_done self.inputs = {} self.field_order = [] root = BoxLayout(orientation="vertical", spacing=dp(8), padding=dp(10)) root.add_widget(Label(text=self.tr("pos.uverovy_zaznam","Úverový záznam"), size_hint_y=None, height=dp(34))) self.scroll = ScrollView(do_scroll_y=True, size_hint=(1, 1)) form = BoxLayout(orientation="vertical", spacing=dp(6), size_hint_y=None) form.bind(minimum_height=form.setter("height")) fields = [ ("hjmeno", self.tr("pos.uver_firma","Firma") + " *", getattr(self.firma, "hjmeno", "")), ("adresa1", self.tr("pos.uver_adresa","Adresa") + " 1", getattr(self.firma, "adresa1", "")), ("adresa2", self.tr("pos.uver_adresa","Adresa") + " 2", getattr(self.firma, "adresa2", "")), ("adresa3", self.tr("pos.uver_adresa","Adresa") + " 3", getattr(self.firma, "adresa3", "")), ("ico", self.tr("pos.uver_ico","IČO"), getattr(self.firma, "ico", "")), ("icdph", self.tr("pos.uver_icdph","IČ DPH"), getattr(self.firma, "icdph", "")), ("dic", self.tr("pos.uver_dic","DIČ"), getattr(self.firma, "dic", "")), ("akcia", self.tr("pos.uver_akcia","Akcia"), ""), ("schvalil", self.tr("pos.uver_schvalil","Schválil") + " *", ""), ] self.field_order = [key for key, _, _ in fields] for key, label, value in fields: form.add_widget(Label(text=label, size_hint_y=None, height=dp(22))) inp = TextInput(text=str(value or ""), multiline=False, size_hint_y=None, height=dp(42)) self.inputs[key] = inp form.add_widget(inp) self.scroll.add_widget(form) root.add_widget(self.scroll) btns = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(8)) btn_cancel = Button(text=self.tr("button.cancel","Zrušiť"), background_color=(0.6, 0.2, 0.2, 1),background_normal="") btn_ok = Button(text=self.tr("button.ok","OK"), background_color=(0.2, 0.6, 0.2, 1),background_normal="") btn_cancel.bind(on_press=lambda *_: self.close()) btn_ok.bind(on_press=lambda *_: self._confirm()) btns.add_widget(btn_cancel) btns.add_widget(btn_ok) root.add_widget(btns) self.add_widget(root) Clock.schedule_once(lambda *_: setattr(self.inputs["hjmeno"], "focus", True), 0.1) def _value(self, key): return str(self.inputs[key].text or "").strip() def _confirm(self): if not self._value("hjmeno"): messagebox(self.tr("pos.uver_povinne_meno", "Meno firmy je povinné."), self.tr("pos.uverovy_zaznam", "Úverový záznam")) return if not self._value("schvalil"): messagebox(self.tr("pos.uver_povinne_schvalil", "Meno schvalovateľa je povinné."), self.tr("pos.uverovy_zaznam", "Úverový záznam")) return zaznam = data.UverZaznam( id=getattr(self.firma, "id", None), hjmeno=self._value("hjmeno"), adresa1=self._value("adresa1"), adresa2=self._value("adresa2"), adresa3=self._value("adresa3"), ico=self._value("ico"), icdph=self._value("icdph"), dic=self._value("dic"), akcia=self._value("akcia"), schvalil=self._value("schvalil"), ) self.close() if self.on_done: self.on_done(zaznam) def handle_key(self, key): key = CreditCompanySelectDialog._key_name(self, key) focused = self._focused_input() native_text_input = focused is not None if key == "ESC": self.close() return True if key == "ENTER": self._confirm() return True if key == "PGUP": self.scroll.scroll_y = min(1, self.scroll.scroll_y + 0.8) return True if key == "PGDN": self.scroll.scroll_y = max(0, self.scroll.scroll_y - 0.8) return True if key == "TAB": return self._focus_next_input() if not focused: focused = self.inputs.get("hjmeno") if focused: focused.focus = True else: return False if key == "BACKSPACE": focused.do_backspace() return True if key == "DELETE": try: focused.do_backspace(mode="del") except TypeError: pass return True if key == "HOME": focused.cursor = (0, 0) return True if key == "END": focused.cursor = (len(focused.text), 0) return True if isinstance(key, str) and len(key) == 1: if native_text_input: return True focused.insert_text(key) return True return False def _focus_next_input(self): inputs = [self.inputs[key] for key in self.field_order if key in self.inputs] if not inputs: return False focused = self._focused_input() if focused in inputs: idx = (inputs.index(focused) + 1) % len(inputs) focused.focus = False else: idx = 0 inputs[idx].focus = True return True def _focused_input(self): for inp in self.inputs.values(): if inp.focus: return inp return None class HotelReceptionSelectDialog(BaseModal): def __init__(self, parent, receptions, on_select, **kwargs): super().__init__( modal_manager=getattr(parent, "modal_manager", None), size_hint=(0.62, 0.74), **kwargs, ) self.parent_ref = parent self.receptions = list(receptions or []) self.on_select = on_select root = BoxLayout(orientation="vertical", spacing=dp(8), padding=dp(10)) root.add_widget(Label(text=self.tr("pos.recepcia_vyber", "Výber recepcie"), size_hint_y=None, height=dp(34))) self.search = TextInput( hint_text=self.tr("pos.recepcia_hladanie", "Hľadať recepciu ..."), multiline=False, size_hint_y=None, height=dp(44), ) self.search.bind(text=lambda *_: self._refresh_list()) root.add_widget(self.search) self.scroll = ScrollView( do_scroll_y=True, size_hint=(1, 1), bar_width=dp(18), scroll_type=["bars", "content"], ) self.list_box = BoxLayout(orientation="vertical", spacing=dp(5), size_hint_y=None) self.list_box.bind(minimum_height=self.list_box.setter("height")) self.scroll.add_widget(self.list_box) root.add_widget(self.scroll) btn_cancel = Button(text=self.tr("button.cancel", "Zrušiť"), size_hint_y=None, height=dp(50), background_color=(0.6, 0.2, 0.2, 1),background_normal="") btn_cancel.bind(on_press=lambda *_: self.close()) root.add_widget(btn_cancel) self.add_widget(root) self._refresh_list() Clock.schedule_once(lambda *_: setattr(self.search, "focus", True), 0.1) def _matches(self, reception): query = self.parent_ref._normalize_text(self.search.text) if not query: return True text = " ".join([ getattr(reception, "hotel", "") or "", getattr(reception, "hor_prefix", "") or "", str(getattr(reception, "typ_hotel", "") or ""), ]) return query in self.parent_ref._normalize_text(text) def _refresh_list(self): self.list_box.clear_widgets() filtered = [r for r in self.receptions if self._matches(r)] if not filtered: self.list_box.add_widget(Label(text=self.tr("pos.recepcia_nenastavene", "Nie je nastavená žiadna recepcia"), size_hint_y=None, height=dp(44))) return for reception in filtered: text = f"{reception.hotel}\n{self.tr('pos.recepcia_prefix', 'Prefix')}: {reception.hor_prefix} {self.tr('pos.recepcia_typ', 'Typ')}: {reception.typ_hotel}" btn = Button(text=text, halign="left", valign="middle", size_hint_y=None, height=dp(62)) btn.bind(size=lambda inst, *_: setattr(inst, "text_size", (inst.width - dp(16), None))) btn.bind(on_press=lambda _, r=reception: self._select(r)) self.list_box.add_widget(btn) def _select(self, reception): self.close() if self.on_select: self.on_select(reception) def handle_key(self, key): key = CreditCompanySelectDialog._key_name(self, key) if key == "ESC": self.close() return True if key == "TAB": return True if key == "BACKSPACE": self.search.do_backspace() return True if key == "HOME": self.scroll.scroll_y = 1 return True if key == "END": self.scroll.scroll_y = 0 return True if key == "PGUP": self.scroll.scroll_y = min(1, self.scroll.scroll_y + 0.8) return True if key == "PGDN": self.scroll.scroll_y = max(0, self.scroll.scroll_y - 0.8) return True if isinstance(key, str) and len(key) == 1: if self.search.focus: return True self.search.focus = True self.search.insert_text(key) return True return False class HotelTextInputDialog(BaseModal): def __init__(self, parent, title, hint_text, on_done, **kwargs): super().__init__( modal_manager=getattr(parent, "modal_manager", None), size_hint=(0.52, 0.34), **kwargs, ) self.on_done = on_done root = BoxLayout(orientation="vertical", spacing=dp(8), padding=dp(10)) root.add_widget(Label(text=title, size_hint_y=None, height=dp(34))) self.input = TextInput(hint_text=hint_text, multiline=False, size_hint_y=None, height=dp(48)) root.add_widget(self.input) btns = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(8)) btn_cancel = Button(text=self.tr("button.cancel","Zrušiť"), background_color=(0.6, 0.2, 0.2, 1),background_normal="") btn_ok = Button(text=self.tr("button.ok","OK"), background_color=(0.2, 0.6, 0.2, 1),background_normal="") btn_cancel.bind(on_press=lambda *_: self.close()) btn_ok.bind(on_press=lambda *_: self._confirm()) btns.add_widget(btn_cancel) btns.add_widget(btn_ok) root.add_widget(btns) self.add_widget(root) Clock.schedule_once(lambda *_: setattr(self.input, "focus", True), 0.1) def _confirm(self): value = str(self.input.text or "").strip() if not value: return self.close() if self.on_done: self.on_done(value) def handle_key(self, key): key = CreditCompanySelectDialog._key_name(self, key) if key == "ESC": self.close() return True if key == "ENTER": self._confirm() return True if key == "BACKSPACE": self.input.do_backspace() return True if key == "DELETE": try: self.input.do_backspace(mode="del") except TypeError: pass return True if key == "HOME": self.input.cursor = (0, 0) return True if key == "END": self.input.cursor = (len(self.input.text), 0) return True if isinstance(key, str) and len(key) == 1: if self.input.focus: return True self.input.focus = True self.input.insert_text(key) return True return False class HotelTargetSelectDialog(BaseModal): def __init__(self, parent, reception, rooms_response, on_room, on_manual, on_card, **kwargs): super().__init__( modal_manager=getattr(parent, "modal_manager", None), size_hint=(0.78, 0.86), **kwargs, ) self.parent_ref = parent self.reception = reception self.rooms_response = rooms_response self.rooms = list(getattr(rooms_response, "rooms", []) or []) self.on_room = on_room self.on_manual = on_manual self.on_card = on_card root = BoxLayout(orientation="vertical", spacing=dp(8), padding=dp(10)) title = f"{getattr(reception, 'hotel', '')} - {self.tr('pos.recepcia_izba_skupina', 'izba/skupina')}" root.add_widget(Label(text=title, size_hint_y=None, height=dp(34))) top = BoxLayout(size_hint_y=None, height=dp(48), spacing=dp(8)) self.search = TextInput(hint_text=self.tr('pos.recepcia_hint', 'Hľadať izbu, skupinu alebo hosťa...'), multiline=False) self.search.bind(text=lambda *_: self._refresh_list()) btn_card = Button(text=self.tr('pos.recepcia_karta', 'Karta'), size_hint_x=None, width=dp(120)) btn_card.bind(on_press=lambda *_: self._card()) top.add_widget(self.search) top.add_widget(btn_card) root.add_widget(top) manual = BoxLayout(size_hint_y=None, height=dp(48), spacing=dp(8)) self.room_input = TextInput(hint_text=self.tr('pos.recepcia_cislo_izby', 'Číslo izby'), multiline=False) btn_manual = Button(text=self.tr('pos.recepcia_overit_izbu', 'Overiť izbu'), size_hint_x=None, width=dp(150)) btn_manual.bind(on_press=lambda *_: self._manual()) manual.add_widget(self.room_input) manual.add_widget(btn_manual) root.add_widget(manual) msg = str(getattr(rooms_response, "message", "") or "") if msg: root.add_widget(Label(text=msg, size_hint_y=None, height=dp(28))) self.scroll = ScrollView( do_scroll_y=True, size_hint=(1, 1), bar_width=dp(18), scroll_type=["bars", "content"], ) self.list_box = BoxLayout(orientation="vertical", spacing=dp(5), size_hint_y=None) self.list_box.bind(minimum_height=self.list_box.setter("height")) self.scroll.add_widget(self.list_box) root.add_widget(self.scroll) btn_cancel = Button(text=self.tr("button.cancel","Zrušiť"), size_hint_y=None, height=dp(50), background_color=(0.6, 0.2, 0.2, 1),background_normal="") btn_cancel.bind(on_press=lambda *_: self.close()) root.add_widget(btn_cancel) self.add_widget(root) self._refresh_list() focus_target = self.room_input if getattr(rooms_response, "manual_room", False) else self.search Clock.schedule_once(lambda *_: setattr(focus_target, "focus", True), 0.1) def _matches(self, room): query = self.parent_ref._normalize_text(self.search.text) if not query: return True text = " ".join([ getattr(room, "type", "") or "", getattr(room, "room_code", "") or "", getattr(room, "room_name", "") or "", getattr(room, "guest_name", "") or "", getattr(room, "account_id", "") or "", ]) return query in self.parent_ref._normalize_text(text) def _room_text(self, room): label = "Skupina" if getattr(room, "type", "") == "group" else "Izba" name = getattr(room, "room_name", "") or getattr(room, "room_code", "") or getattr(room, "id", "") guest = getattr(room, "guest_name", "") or "" dates = " - ".join(x for x in (getattr(room, "checkin_date", ""), getattr(room, "checkout_date", "")) if x) parts = [f"{label}: {name}"] if guest: parts.append(guest) if dates: parts.append(dates) if getattr(room, "message", ""): parts.append(getattr(room, "message", "")) return "\n".join(parts) def _refresh_list(self): self.list_box.clear_widgets() filtered = [room for room in self.rooms if self._matches(room)] if not filtered: self.list_box.add_widget(Label(text=self.tr('pos.recepcia_ziadne_izby', 'Žiadne izby na výber.'), size_hint_y=None, height=dp(44))) return for room in filtered: btn = Button( text=self._room_text(room), halign="left", valign="middle", size_hint_y=None, height=dp(72), disabled=not getattr(room, "can_charge", True), ) btn.bind(size=lambda inst, *_: setattr(inst, "text_size", (inst.width - dp(16), None))) btn.bind(on_press=lambda _, r=room: self._select_room(r)) self.list_box.add_widget(btn) def _select_room(self, room): if not getattr(room, "can_charge", True): messagebox(getattr(room, "message", "") or self.tr("pos.recepcia_no_charge","Na túto izbu nie je možné naťažovať."), self.tr("pos.recepcia","Recepcia")) return self.close() if self.on_room: self.on_room(room) def _manual(self): value = str(self.room_input.text or "").strip() if not value: return self.close() if self.on_manual: self.on_manual(value) def _card(self): self.close() if self.on_card: self.on_card() def _focused_input(self): for inp in (self.search, self.room_input): if inp.focus: return inp return None def handle_key(self, key): key = CreditCompanySelectDialog._key_name(self, key) focused = self._focused_input() had_focused_input = focused is not None if key == "ESC": self.close() return True if key == "ENTER": if self.room_input.focus: self._manual() return True if key == "TAB": self.search.focus = not self.search.focus self.room_input.focus = not self.search.focus return True if key == "PGUP": self.scroll.scroll_y = min(1, self.scroll.scroll_y + 0.8) return True if key == "PGDN": self.scroll.scroll_y = max(0, self.scroll.scroll_y - 0.8) return True if not focused: focused = self.search focused.focus = True if key == "BACKSPACE": focused.do_backspace() return True if key == "DELETE": try: focused.do_backspace(mode="del") except TypeError: pass return True if key == "HOME": self.scroll.scroll_y = 1 return True if key == "END": self.scroll.scroll_y = 0 return True if isinstance(key, str) and len(key) == 1: if had_focused_input: return True focused.insert_text(key) return True return False class HotelGuestSelectDialog(BaseModal): def __init__(self, parent, reception, room, guests, on_select, **kwargs): super().__init__( modal_manager=getattr(parent, "modal_manager", None), size_hint=(0.7, 0.78), **kwargs, ) self.parent_ref = parent self.reception = reception self.room = room self.guests = list(guests or []) self.on_select = on_select root = BoxLayout(orientation="vertical", spacing=dp(8), padding=dp(10)) room_name = getattr(room, "room_code", "") or getattr(room, "room_name", "") or getattr(room, "room_id", "") root.add_widget(Label(text=f"{self.tr('pos.recepcia_vyber_hosta','Výber hosťa')} - {room_name}", size_hint_y=None, height=dp(34))) self.search = TextInput(hint_text=self.tr('pos.recepcia_hladanie_hosta','Hľadanie hosťa...'), multiline=False, size_hint_y=None, height=dp(44)) self.search.bind(text=lambda *_: self._refresh_list()) root.add_widget(self.search) self.scroll = ScrollView( do_scroll_y=True, size_hint=(1, 1), bar_width=dp(18), scroll_type=["bars", "content"], ) self.list_box = BoxLayout(orientation="vertical", spacing=dp(5), size_hint_y=None) self.list_box.bind(minimum_height=self.list_box.setter("height")) self.scroll.add_widget(self.list_box) root.add_widget(self.scroll) btn_cancel = Button(text=self.tr("button.cancel","Zrušiť"), size_hint_y=None, height=dp(50), background_color=(0.6, 0.2, 0.2, 1),background_normal="") btn_cancel.bind(on_press=lambda *_: self.close()) root.add_widget(btn_cancel) self.add_widget(root) self._refresh_list() Clock.schedule_once(lambda *_: setattr(self.search, "focus", True), 0.1) def _matches(self, guest): query = self.parent_ref._normalize_text(self.search.text) if not query: return True text = " ".join([ getattr(guest, "guest_name", "") or "", getattr(guest, "id", "") or "", getattr(guest, "room_code", "") or "", ]) return query in self.parent_ref._normalize_text(text) def _refresh_list(self): self.list_box.clear_widgets() filtered = [guest for guest in self.guests if self._matches(guest)] if not filtered: self.list_box.add_widget(Label(text=self.tr("pos.recepcia_ziadny_host", "Žiadny hosť."), size_hint_y=None, height=dp(44))) return for guest in filtered: text = getattr(guest, "guest_name", "") or getattr(guest, "id", "") detail = " - ".join(x for x in (getattr(guest, "checkin_date", ""), getattr(guest, "checkout_date", "")) if x) if detail: text = f"{text}\n{detail}" btn = Button(text=text, halign="left", valign="middle", size_hint_y=None, height=dp(62)) btn.bind(size=lambda inst, *_: setattr(inst, "text_size", (inst.width - dp(16), None))) btn.bind(on_press=lambda _, g=guest: self._select(g)) self.list_box.add_widget(btn) def _select(self, guest): self.close() if self.on_select: self.on_select(guest) def handle_key(self, key): key = CreditCompanySelectDialog._key_name(self, key) if key == "ESC": self.close() return True if key == "TAB": return True if key == "BACKSPACE": self.search.do_backspace() return True if key == "HOME": self.scroll.scroll_y = 1 return True if key == "END": self.scroll.scroll_y = 0 return True if key == "PGUP": self.scroll.scroll_y = min(1, self.scroll.scroll_y + 0.8) return True if key == "PGDN": self.scroll.scroll_y = max(0, self.scroll.scroll_y - 0.8) return True if isinstance(key, str) and len(key) == 1: if self.search.focus: return True self.search.focus = True self.search.insert_text(key) return True return False class POSButton(Button): def __init__(self, **kwargs): super().__init__(**kwargs) self._long_event = None self._long_triggered = False self._tap_handler = None def on_touch_down(self, touch): if self.collide_point(*touch.pos): return super().on_touch_down(touch) return False def on_touch_move(self, touch): if self.collide_point(*touch.pos): return True return False def on_touch_up(self, touch): if getattr(self, "_long_event", None): self._long_event.cancel() self._long_event = None # LONG TOUCH → stop if getattr(self, "_long_triggered", False): self.state = "normal" return True # SHORT TAP if self._tap_handler: self._tap_handler(self) return super().on_touch_up(touch) class POSMenuButton(Button): def __init__(self, **kwargs): super().__init__(**kwargs) self._long_event = None self._long_triggered = False self._tap_handler = None self._col=0 self._line=0 self._span=0 def on_touch_down(self, touch): if touch.is_mouse_scrolling: return False if not self.collide_point(*touch.pos): return False self._long_triggered = False self._long_event = Clock.schedule_once( self._do_long_touch, LONG_TOUCH_TIME ) # zablokuje scroll i další widgety return True def on_touch_up(self, touch): if touch.is_mouse_scrolling: return False if not self.collide_point(*touch.pos): return False if self._long_event: self._long_event.cancel() # LONG TOUCH if self._long_triggered: self.state = "normal" return True # SHORT TAP if self._tap_handler: self.state = "normal" self._tap_handler(self) return True return True def on_touch_move(self, touch): if not self.collide_point(*touch.pos): if self._long_event: self._long_event.cancel() return True def _do_long_touch(self, *_): self._long_triggered = True self.state = "normal" if hasattr(self, "on_long_touch") and callable(self.on_long_touch): self.on_long_touch(self) LONG_TOUCH_TIME = 0.35 class POSDialog(Screen): def __init__(self, kasutxt, controller, default_price_level, cenik, setup, fstmenu, printers, levels, default_printer, payments, alllevels, static_maps=None, bankterms=None, limit_mode: bool = False, **kwargs): super().__init__(**kwargs) self._ignore_mark_until = 0 self.left_matrix_buttons = [] self.current_page = 1 self.controller = controller self.limit_mode = bool(limit_mode) # ================= STAV ================= self.default_price_level = default_price_level self.setup = setup self.cenik = cenik self.static_maps = static_maps or {} self.cenik_map = self.static_maps.get("cenik_map") or {cp.id_card: cp for cp in self.cenik.cenpol} self.default_printer=default_printer self.levels=levels self.printers=printers self.bankterms=list(bankterms or []) self.alllevels=alllevels self.kasutxt=kasutxt self.ucet: UcetEdit | None = None self.fstmenu = fstmenu self.payments=payments self.fstmenu_map = self.static_maps.get("fstmenu_map") or {cp.c_karty: cp for cp in self.fstmenu} self.price_level = getattr(self.setup, "def_cenhla", "1") if self.setup else "1" self.currency = getattr(self.setup, "zkr_mena", "") if self.setup else "" self._orig_checksum = None self.storno_result: tuple[UcetEdit, UcetEdit | None] | None = None self.cell_w = MENU_BTN_W self.total_h = 0 self.menu_cols = 24 self.menu_rows = 10 self.active_guest_id = "ALL" self._refresh_event = None self._code_buffer = "" self._code_event = None self.pos_keyboard = None self._keyboard_bound = False self._search_prev_page = self.current_page self._current_search_text = "" self.ignore_touches = False self.modal_manager = ModalManager() self.price_level_map = self.static_maps.get("price_level_map") or { pl.ch: pl.ch_name for pl in self.alllevels } self.printer_map = self.static_maps.get("printer_map") or { p.prn_no: p.prn_name for p in self.printers } self.default_printer = str(self.default_printer or "").strip() if self.default_printer not in self.printer_map: self.default_printer = ( str(getattr(self.printers[0], "prn_no", "") or "").strip() if self.printers else "" ) self._search_index = self.static_maps.get("search_index") or [] self._code_index = self.static_maps.get("code_index") or {} self.menu_pages = self.static_maps.get("menu_pages") or [] self.mamechody = getattr(self.setup, "is_chod", True) if self.setup else True self.mamehosti = getattr(self.setup, "is_host", True) if self.setup else True self.mametretiny = getattr(self.setup, "is_tretiny", True) if self.setup else True self.mamestvrtiny = getattr(self.setup, "is_stvrtiny", True) if self.setup else True # ================= ROOT ================= root = FloatLayout() # ================= MAIN ================= main = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(6), size_hint=(1, 1) ) # ================= INFO ================= self._build_info_bar(main) self.search_bar = TextInput( readonly=True, # 🔥 input ide cez POSKeyboard hint_text=self.tr("pos.hladat_polozku","Hľadať položku..."), multiline=False ) # ================= TOP (MENU + UCET) ================= top = BoxLayout( orientation="horizontal", spacing=dp(6), size_hint=(1, 1), ) # ---------- LEVÁ: SCROLL + MENU ---------- self.search_toolbar = BoxLayout( orientation="horizontal", size_hint=(1, None), height=dp(50) ) btn_back = Button( #text="← Späť", text="← "+self.tr("button.back","Späť"), size_hint=(None, None), background_color=(0.6,0.2,0.2,1), background_normal="", height=dp(50), width=dp(140) ) btn_back.bind( on_release=lambda *_: self._end_search() ) self.search_toolbar.add_widget(btn_back) self.menu_scroll = ScrollView( do_scroll_x=True, do_scroll_y=True, size_hint=(1, 1), width=MENU_COLS * MENU_BTN_W + dp(18), bar_width=dp(6), effect_cls=ScrollEffect, ) self.menu = FloatLayout( size_hint=(None, None), ) self.menu_scroll.add_widget(self.menu) self.leftpanel = BoxLayout( orientation="vertical", size_hint=(1, 1), spacing=dp(4) ) self.leftpanel.add_widget(self.menu_scroll) self.leftpanel.add_widget(self._build_left_bottom_panel()) top.add_widget(self.leftpanel) # ---------- PRAVÁ: UCET + HOSTIA ---------- right_top = BoxLayout( orientation="horizontal", size_hint=(1, None), height=dp(50), spacing=dp(4) ) self.btn_price_level = Button( text=f"{self.tr('pos.price_level', 'Cenová hladina')}: {self.default_price_level}", size_hint=(0.5, 1), ) self.btn_price_level.bind( on_release=self._open_price_popup ) self.btn_printer = Button( text=self.tr('button.printer','Tlačiareň')+": Default", size_hint=(0.5, 1), ) self.btn_printer.bind(on_release=self._open_printer_popup) # ---------- SELECT ALL ---------- btn_select = Button( text="☑", size_hint=(None, 1), width=dp(60) ) btn_select.bind(on_press=lambda *_: self.action_toggle_select_all()) right_top.add_widget(self.btn_printer) right_top.add_widget(self.btn_price_level) right_top.add_widget(btn_select) self.rightpanel = BoxLayout( orientation="vertical", size_hint=(None, 1), width=MENU_COLS * ACC_BTN_W + dp(12), spacing=dp(4) ) # ================= GUEST + ACTION BAR ================= top_bar = BoxLayout( orientation="horizontal", size_hint=(1, None), height=dp(50), spacing=dp(6) ) # ---------- GUESTS ---------- guests_scroll = ScrollView( size_hint=(1, 1), do_scroll_x=True, do_scroll_y=False ) self.guests_bar = BoxLayout( size_hint_x=None, height=dp(50), spacing=dp(4) ) self.guests_bar.bind(minimum_width=self.guests_bar.setter("width")) guests_scroll.add_widget(self.guests_bar) self.rightpanel.add_widget(right_top) if self.mamehosti or self.mamechody: top_bar.add_widget(guests_scroll) self.rightpanel.add_widget(top_bar) # ===== UCET ===== self.scroll = ScrollView( do_scroll_x=False, do_scroll_y=True, size_hint=(1, 1), bar_width=dp(6), effect_cls=ScrollEffect, ) self.account = GridLayout( cols=1, spacing=(0, dp(4)), size_hint=(1, None), ) self.account.bind(minimum_height=self.account.setter("height")) self.scroll.add_widget(self.account) self.rightpanel.add_widget(self.scroll) self.rightpanel.add_widget(self._build_right_bottom_panel()) top.add_widget(self.rightpanel) # 👉 PRIDAŤ DO TOP # 👤 ALL btn_all = Button( text=self.tr("pos.vsetci","Všetci"), size_hint=(None, 1), width=dp(100), background_normal="", background_color=(0.3,0.3,0.3,1) if self.active_guest_id != "ALL" else (0.2,0.5,0.9,1) ) btn_all.bind(on_press=lambda *_: self.set_active_guest("ALL")) if self.mamehosti: self.guests_bar.add_widget(btn_all) main.add_widget(top) root.add_widget(main) self.action_panel = ActionPanel( orientation="vertical", size_hint=(None, None), size=(dp(180), dp(300)), opacity=0 ) root.add_widget(self.action_panel) self.register_event_type("on_finish") self.add_widget(root) # ================= NAPLNI MENU Z CENIKU ================= Clock.schedule_once(self._build_menu_from_cenik_safe, 0) # po prvním layoutu + při resize okna udrž účet ve správné pozici Clock.schedule_once(self._scroll_ucet_to_top, 0) self.scroll.bind(size=lambda *_: Clock.schedule_once(self._scroll_ucet_to_top, 0)) # Milan 13.03.2026 Window.bind(size=self._on_window_resize) # ========================================================== def tr(self, key, default=None, **kwargs): if self.controller and hasattr(self.controller, "tr"): return self.controller.tr(key, default, **kwargs) return default if default is not None else key def on_touch_down(self, touch): if time() < getattr(self, "_ignore_mark_until", 0): return True panel = getattr(self, "action_panel", None) if (panel and panel.opacity != 0): if panel.collide_point(*touch.pos) or self.rightpanel.collide_point(*touch.pos): return super().on_touch_down(touch) else: self.action_clear_selection() return super().on_touch_down(touch) def _bottom_button( self, text, action=None, perm=None, color=(0.25, 0.25, 0.25, 1), **kwargs, ): btn = Button( text=text, background_normal="", background_color=color, halign="center", valign="middle", **kwargs, ) btn.bind( size=lambda inst, *_: setattr(inst, "text_size", (inst.width - dp(8), inst.height - dp(6))) ) if action: btn.bind(on_press=action) if perm and not self.controller.has_perm(perm): btn.disabled = True btn.opacity = 0.4 return btn def _build_left_bottom_panel(self): panel = GridLayout( cols=4, spacing=dp(6), padding=dp(6), size_hint=(1, None), height=dp(72), ) cancel_color = (0.6, 0.2, 0.2, 1) buttons = [ ("ESC", self.on_esc, None, cancel_color), (self.tr('button.code','Kód'), self.on_mark_by_code, None, (0.25, 0.25, 0.25, 1)), (self.tr('button.search',"Hľadať"), self._start_search, None, (0.25, 0.25, 0.25, 1)), (self.tr('button.loyalty_card',"Vernostná\nkarta"), self.on_loyalty_card, None, (0.25, 0.25, 0.25, 1)), ] for text, action, perm, color in buttons: panel.add_widget( self._bottom_button(text, action, perm, color=color) ) return panel def _build_right_bottom_panel(self): panel = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(6), size_hint=(1, None), height=dp(144), ) quick_payments = self._get_quick_payment_types() quick_row = GridLayout( cols=max(1, len(quick_payments)), spacing=dp(6), size_hint=(1, None), height=dp(60), ) platbatxt=self.tr('button.pay','Platba') if quick_payments: for ptype in quick_payments: quick_row.add_widget( self._bottom_button( f"{platbatxt}\n{ptype.name}", lambda _, p=ptype: self.on_btn_quick_payment(p), "PLATBA", ) ) else: quick_row.add_widget( self._bottom_button( platbatxt, self.on_btn_payment, "PLATBA", ) ) panel.add_widget(quick_row) bottom_row = BoxLayout( orientation="horizontal", spacing=dp(6), size_hint=(1, None), height=dp(60), ) #bottom_row.add_widget(Widget(size_hint=(1, 1))) bottom_row.add_widget( self._bottom_button( self.tr('button.pay_selected',"Platba\nvybraného"), self.on_btn_payment, "PLATBA", size_hint=(1, 1) ) ) bottom_row.add_widget( self._bottom_button( self.tr('button.save',"Uložiť"), self.on_save, color=(0.2, 0.6, 0.2, 1), size_hint=(1, 1) ) ) self.btn_payment_main = next( (w for w in bottom_row.children if isinstance(w, Button) and platbatxt in w.text), None, ) self._refresh_payment_button_text() panel.add_widget(bottom_row) return panel def _has_payment_selection(self): return bool(self.ucet and any( p.selected or p.sel_pocet for p in self.ucet.poloz )) def _refresh_payment_button_text(self): platbatxt=self.tr('button.pay','Platba') btn = getattr(self, "btn_payment_main", None) if not btn: return if getattr(self, "limit_mode", False): btn.text = platbatxt return btn.text = self.tr('button.pay_selected','Platba\nvýbraných') if self._has_payment_selection() else platbatxt def _refresh_price_button(self): name = self.price_level_map.get( self.default_price_level, self.default_price_level ) self.btn_price_level.text = f"{self.tr('pos.price_level', 'Cenová hladina')}: {name}" def _refresh_printer_button(self): selected = str(self.default_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"{self.tr('button.printer','Tlačiareň')}: {name}" def _open_price_popup(self, *_): layout = BoxLayout(orientation="vertical", spacing=5) for pl in self.levels: btn = Button(text=pl.ch_name) def select(_btn, level=pl): self.default_price_level = level.ch self._refresh_price_button() popup.dismiss() btn.bind(on_release=select) layout.add_widget(btn) popup = Popup( title=self.tr('pos.price_level', 'Cenová hladina'), content=layout, size_hint=(0.4, 0.6) ) popup.open() def _open_printer_popup(self, *_): layout = BoxLayout(orientation="vertical", spacing=5) for p in self.printers: btn = Button(text=p.prn_name) def select(_btn, printer=p): self.default_printer = printer.prn_no self._refresh_printer_button() popup.dismiss() btn.bind(on_release=select) layout.add_widget(btn) popup = Popup( title=self.tr('button.printer',"Tlačiareň"), content=layout, size_hint=(0.4, 0.6) ) popup.open() def _normalize_key(self, keycode, text): if text == "\t": return "TAB" if text: return text if isinstance(keycode, int): if 48 <= keycode <= 57: return str(keycode - 48) if 96 <= keycode <= 105: return str(keycode - 96) if 256 <= keycode <= 265: return str(keycode - 256) if keycode in (13, 271): return "ENTER" if keycode == 8: return "BACKSPACE" if keycode == 9: return "TAB" if keycode == 27: return "ESC" if keycode in (127, 266): return "DELETE" if keycode == 27: return "ESC" if keycode == 13: return "ENTER" if keycode == 8: return "BACKSPACE" if keycode == 9: return "TAB" key = keycode[1] if isinstance(keycode, tuple) else keycode return { "enter": "ENTER", "numenter": "ENTER", "numpadenter": "ENTER", "kp_enter": "ENTER", "escape": "ESC", "backspace": "BACKSPACE", "tab": "TAB", "delete": "DELETE", "home": "HOME", "end": "END", "pageup": "PGUP", "pagedown": "PGDN", "pgup": "PGUP", "pgdn": "PGDN", "numpad0": "0", "numpad1": "1", "numpad2": "2", "numpad3": "3", "numpad4": "4", "numpad5": "5", "numpad6": "6", "numpad7": "7", "numpad8": "8", "numpad9": "9", "kp_0": "0", "kp_1": "1", "kp_2": "2", "kp_3": "3", "kp_4": "4", "kp_5": "5", "kp_6": "6", "kp_7": "7", "kp_8": "8", "kp_9": "9", }.get(key, key) def dispatch_key(self, key): # 🔥 ESC rieš NAJPRV if self.modal_manager and self.modal_manager.active_modal: if key == "ESC": if self.modal_manager.close_top(): return True # 🔥 STOP → nepadne app else: if self.modal_manager.dispatch_key(key): return True elif self.search_toolbar.parent: print(key) if key == "ESC": self._end_search() return True else: print(key) if isinstance(key, int): if key in (13, 40, 10, 271): key = "ENTER" elif key == 8: key = "BACKSPACE" elif key == 27 : key = "ESC" if key == 'ENTER': # Enter / Numpad Enter self._process_code_buffer() return True # BACKSPACE if key == 'BACKSPACE': self._code_buffer = self._code_buffer[:-1] return True # ESC if key == 'ESC': # tu treba osetrit esc napr. pri ruseni presunu na iny stol return True # čísla if key in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'): # top row digit = key elif ord(key) in range(256, 266): # numpad digit = str(ord(key) - 256) elif key=='*' or ord(key) == 268: digit = '*' else: digit = None if digit: self._code_buffer += digit self._restart_code_timer() return True return False return False def on_enter(self, *args): self._bind_keyboard() def on_leave(self, *args): self._unbind_keyboard() def on_pre_leave(self, *args): self._unbind_keyboard() def _bind_keyboard(self): if self._keyboard_bound: return Window.unbind(on_key_down=self._on_key_down) Window.bind(on_key_down=self._on_key_down) self._keyboard_bound = True def _unbind_keyboard(self): Window.unbind(on_key_down=self._on_key_down) self._keyboard_bound = False self._clear_code_buffer() def _handle_global_key(self, key): if key == "ESC": self.modal_manager.close() return True return False def _on_key_down(self, window, keycode, scancode, codepoint, modifiers): if self.manager and self.manager.current != self.name: return False key = self._normalize_key(keycode, codepoint) # 🔥 vždy najprv skús modal handled = self.dispatch_key(key) if handled: return True # 🔥 ak chceš blokovať HW input keď je POS keyboard if self.pos_keyboard: return True return False def _restart_code_timer(self): if self._code_event: self._code_event.cancel() self._code_event = Clock.schedule_once( lambda dt: self._clear_code_buffer(), 2.0 # 2 sekundy idle → reset ) def _clear_code_buffer(self): self._code_buffer = "" def _process_code_buffer(self): raw = self._code_buffer.strip() self._clear_code_buffer() if not raw: return code, qty = self._parse_code_qty(raw) # 🔥 lookup (vracia aj koeficient) item, coef = self._find_item_with_ean(code) if not item: self._code_not_found(raw) return final_qty = qty # koeficient z EAN (napr. balenie) if coef: final_qty *= coef self._add_item_from_cenik(item, final_qty) def _code_not_found(self, code): # 🔥 fallback: otvor vyhľadávanie self._start_search(code) def _start_search(self, initial_text=""): if initial_text: if not isinstance(initial_text, str): initial_text = '' keyboard = PosKeyboard( on_key=self.dispatch_key, bezokesc=True ) self._search_prev_page=self.current_page modal = SearchDialog( modal_manager=self.modal_manager, keyboard=keyboard, parent=self, tr=self.tr ) self.modal_manager.open(modal) def _end_search(self): if self.search_toolbar.parent: self.leftpanel.remove_widget(self.search_toolbar) # 🔥 návrat page if self._search_prev_page is not None: self.current_page = self._search_prev_page self._search_prev_page = None self._current_search_text = "" # 🔥 reset menu self._build_menu_from_cenik() # 🔥 clear modal ref #self.search_modal = None # 🔥 reset scroll Clock.schedule_once( lambda *_: self._scroll_menu_to_origin(), 0 ) def _normalize_text(self, text: str) -> str: if not text: return "" text = text.lower() # odstránenie diakritiky text = unicodedata.normalize("NFD", text) text = "".join(c for c in text if unicodedata.category(c) != "Mn") return text def _build_search_index(self): self._search_index = [] for item in self.cenik_map.values(): if item.id_card <= 0: continue # 🔹 NAME norm_name = self._normalize_text(item.d_name or "") # 🔹 KÓD (int / None) kod_val = getattr(item, "kod", None) if kod_val is None: norm_kod = "" else: norm_kod = self._normalize_text(str(kod_val)) # 🔹 EANy (Pydantic modely!) eans = [] for e in getattr(item, "eany", []) or []: ean_val = getattr(e, "ean", None) if not isinstance(e, dict) else e.get("ean") if ean_val: eans.append(self._normalize_text(ean_val)) self._search_index.append({ "item": item, "name": norm_name, "kod": norm_kod, "eans": eans }) def _find_item_with_ean(self, code): # váhový EAN if len(code) in (12, 13) and code.startswith("21"): return self._parse_weight_ean(code) # normálne return self._find_item_by_code(code) def _parse_weight_ean(self, ean: str): product_code = ean[2:7] weight_raw = ean[7:12] item, coef = self._find_item_by_code(product_code) if not item: return None, None weight = int(weight_raw) / 1000.0 # 🔥 kombinácia if coef: weight *= coef return item, weight def _parse_code_qty(self, raw: str): if "*" in raw: code, qty = raw.split("*", 1) try: qty = float(qty.replace(",", ".")) except ValueError: qty = 1 return code, qty return raw, 1 def _build_cenik_index(self): self._code_index = {} for item in self.cenik_map.values(): # hlavný kód if item.kod: self._code_index[str(item.kod)] = (item, 1) # 🔥 EANy s koeficientom for e in getattr(item, "eany", []): if e.ean: coef = e.koeficient or 1 self._code_index[str(e.ean)] = (item, coef) def _code_index_get(self, code): if not hasattr(self, "_code_index") or not self._code_index: self._build_cenik_index() return self._code_index.get(str(code)) def _find_item_by_code(self, code): return self._code_index_get(code) or (None, None) def _add_item_from_cenik(self, item, qty): self._handle_cenpol_click(item,qty=qty) def hide_action_panel(self): self.action_panel.opacity = 0 self.action_panel.size = (0, 0) def action_delete(self): u_main, u_sec = self.on_storno_polozek() self.controller.handle_pos_result(u_main,u_sec,"storno2") new_poloz = [] for p in self.ucet.poloz: if p.selected: if p.sel_pocet == p.pocet: # 🔥 celý riadok zahodiť continue else: p.pocet = p.pocet - p.sel_pocet p.sel_pocet = None p.selected = False new_poloz.append(p) self.ucet.poloz = new_poloz self.refresh_ucet() self.update_action_panel() def show_action_panel(self): self.action_panel.opacity = 1 self.action_panel.size = (dp(180), dp(300)) Clock.schedule_once(self.position_action_panel, 0) def position_action_panel(self, *_): if self.action_panel.opacity == 0: return # 🔥 pozícia pravého panelu (účtu) rx, ry = self.scroll.to_window( self.scroll.x, self.scroll.top ) # panel tesne nalavo od účtu x = rx - self.action_panel.width - dp(6) # zarovnanie hore y = ry - self.action_panel.height # prevod do root koordinát lx, ly = self.action_panel.parent.to_widget(x, y) self.action_panel.pos = (lx, ly) def action_add(self): selected, selmenus = self.get_selected_items_with_qty() new_items = [] for p, qty in selected: cp = p.model_copy(deep=True) cp.edit_key = str(int(time() * 1000)) cp.selected = False cp.sel_pocet = None cp.pocet = 1 cp.group_id=self._new_group_id() cp.line_id=self._new_line_id() new_items.append(cp) for group_id, items in selmenus.items(): parent = next((i for i in items if i.typ_menu == 1), None) if not parent: continue new_items.extend(self.clone_menu(items, 1)) for p in new_items: p.sel_pocet=None p.selected=False self.ucet.poloz.extend(new_items) self.refresh_ucet() def action_add_with_qty(self): def on_done(qty): self._action_add_with_custom_qty(int(qty)) NumberPad( #title="Koľko pridať?", on_accept=on_done ).open() def _action_add_with_custom_qty(self, qty): selected, selmenus = self.get_selected_items_with_qty() new_items = [] for p, _ in selected: cp = p.model_copy(deep=True) cp.pocet = qty cp.edit_key = str(int(time() * 1000)) cp.selected = False cp.sel_pocet = None cp.group_id=self._new_group_id() cp.line_id=self._new_line_id() new_items.append(cp) for group_id, items in selmenus.items(): parent = next((i for i in items if i.typ_menu == 1), None) if not parent: continue new_items.extend(self.clone_menu(items, qty)) for p in new_items: p.sel_pocet=None p.selected=False self.ucet.poloz.extend(new_items) self.refresh_ucet() def _split_item(self, p): if not p.sel_pocet or p.sel_pocet >= p.pocet: return p, None # nič nedelíš moved = p.model_copy(deep=True) moved.pocet = p.sel_pocet p.pocet -= p.sel_pocet # reset selection p.sel_pocet = None p.selected = False moved.sel_pocet = None moved.selected = False self.ucet.poloz.append(moved) return moved, p def action_move_to_guest(self): selected, selmenus = self.get_selected_items_with_qty() if not selected and not selmenus: return def apply(guest_id): new_items = [] for p, qty in selected: if qty==p.pocet: p.guest_id = guest_id p.selected = False p.sel_pocet = None else: cp = p.model_copy(deep=True) p.pocet = p.pocet-qty p.sel_pocet=None p.selected=False cp.pocet = qty cp.edit_key = str(int(time() * 1000)) cp.selected = False cp.sel_pocet = None cp.group_id=self._new_group_id() cp.line_id=self._new_line_id() new_items.append(cp) for group_id, items in selmenus.items(): parent = next((i for i in items if i.typ_menu == 1), None) if not parent: continue new_items.extend(self.clone_menu(items, qty)) for p in new_items: p.sel_pocet=None p.selected=False p.guest_id=guest_id self.ucet.poloz.extend(new_items) new_poloz = [] for p in self.ucet.poloz: if p.selected: if p.sel_pocet == p.pocet: # 🔥 celý riadok zahodiť continue else: p.pocet = p.pocet - p.sel_pocet p.sel_pocet = None p.selected = False new_poloz.append(p) self.ucet.poloz = new_poloz self.refresh_ucet() self.update_action_panel() self.set_active_guest(guest_id) self._show_guest_picker(apply) def action_move_to_course(self): selected, selmenus = self.get_selected_items_with_qty() if not selected and not selmenus: return def apply(course_id): new_items = [] for p, qty in selected: if qty==p.pocet: p.course_id = course_id p.selected = False p.sel_pocet = None else: cp = p.model_copy(deep=True) p.pocet = p.pocet-qty p.sel_pocet=None p.selected=False cp.pocet = qty cp.edit_key = str(int(time() * 1000)) cp.selected = False cp.sel_pocet = None cp.group_id=self._new_group_id() cp.line_id=self._new_line_id() new_items.append(cp) for group_id, items in selmenus.items(): parent = next((i for i in items if i.typ_menu == 1), None) if not parent: continue new_items.extend(self.clone_menu(items, qty)) for p in new_items: p.sel_pocet=None p.selected=False p.course_id=course_id self.ucet.poloz.extend(new_items) new_poloz = [] for p in self.ucet.poloz: if p.selected: if p.sel_pocet == p.pocet: # 🔥 celý riadok zahodiť continue else: p.pocet = p.pocet - p.sel_pocet p.sel_pocet = None p.selected = False new_poloz.append(p) self.ucet.poloz = new_poloz self.refresh_ucet() self.update_action_panel() self.set_active_course(course_id) self._show_course_picker(apply) def _show_course_picker(self, on_select): root = BoxLayout(orientation="vertical", spacing=dp(6), padding=dp(10)) modal = BaseModal(size_hint=(None,None), size=(dp(300), dp(400))) for g in self.ucet.courses: btn = Button(text=g["name"]) btn.bind(on_press=lambda _, gid=g["id"], m=modal: (on_select(gid), m.dismiss())) root.add_widget(btn) modal.add_widget(root) modal.open() def _show_guest_picker(self, on_select): root = BoxLayout(orientation="vertical", spacing=dp(6), padding=dp(10)) modal = BaseModal(size_hint=(None,None), size=(dp(300), dp(400))) for g in self.ucet.guests: btn = Button(text=g["name"]) btn.bind(on_press=lambda _, gid=g["id"], m=modal: (on_select(gid), m.dismiss())) root.add_widget(btn) modal.add_widget(root) modal.open() def action_price(self): # ===== ROOT ===== root = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(6), ) # ===== SCROLL ===== scroll = ScrollView( size_hint=(1, 1), do_scroll_y=True, ) list_box = BoxLayout( orientation="vertical", size_hint_y=None, spacing=dp(6), ) list_box.bind(minimum_height=list_box.setter("height")) # ===== TLAČÍTKA ===== for cena in self.levels: btn = Button( text=f"{cena.ch_name}", size_hint_y=None, height=dp(48), ) btn.bind(on_press=lambda _, c=cena.ch: self.action_apply_price_level(c, popup)) list_box.add_widget(btn) scroll.add_widget(list_box) root.add_widget(scroll) # ===== SPODNÍ BUTTON ===== btn_back = Button( text=self.tr("button.back","Späť"), background_color=(0.6,0.2,0.2,1), background_normal="", size_hint_y=None, height=dp(48), ) btn_back.bind(on_press=lambda *_: popup.dismiss()) root.add_widget(btn_back) # ===== POPUP ===== popup = Popup( title=self.tr('pos.price_level', 'Cenová hladina'), content=root, size_hint=(None, None), size=(dp(360), dp(420)), auto_dismiss=False, ) popup.open() def action_apply_price_level(self, cena, popup): popup.dismiss() selected, selmenus = self.get_selected_items_with_qty() u_main, u_sec = self.on_storno_polozek() self.controller.handle_pos_result(u_main,u_sec,"storno2") #new_poloz = [] new_poloz = (u_main.poloz) new_items = [] for p, qty in selected: cp = p.model_copy(deep=True) cp.edit_key = str(int(time() * 1000)) cp.selected = True cp.sel_pocet = qty cp.group_id = self._new_group_id() cp.line_id = self._new_line_id() cp.pocet = qty cp.delitel = p.delitel cp.group_id=self._new_group_id() cp.line_id=self._new_line_id() new_items.append(cp) for group_id, items in selmenus.items(): parent = next((i for i in items if i.typ_menu == 1), None) if not parent: continue new_items.extend(self.clone_menu(items, qty)) for p in new_items: p.sel_pocet=p.pocet p.selected=True new_poloz.extend(new_items) self.ucet.poloz = new_poloz for p in self.ucet.poloz: if p.selected: self._change_price_level(p, cena) for p in self.ucet.poloz: p.sel_pocet=None p.selected=False u_main, u_sec = self.on_storno_polozek() self.controller.handle_pos_result(u_main,u_sec,"storno2") self.refresh_ucet() self.update_action_panel() def _change_price_level(self, p: UcPolEdit, cc, update_defaults=True, ucet=None): pol = next( (cp for cp in self.cenik.cenpol if cp.id_card == p.id_card), None ) if not pol: return cena = None for c in pol.ceny: if c.name == cc: cena = c break if not cena: return match p.delitel: case 1: cenan = cena.cena case 2: cenan = cena.cena2 case 3: cenan = cena.cena3 if cena.cena3 is not None else cena.cena case 4: cenan = cena.cena4 if cena.cena4 is not None else cena.cena case _: cenan = cena.cena if p.typ_menu == 1: account = ucet or self.ucet group_id=p.group_id p.cena = cenan p.cenhlad = cena.name p.dph = cena.dan p.mena = cena.mena p.selected = False p.sel_pocet = None for pol in account.poloz: if pol.group_id == group_id and pol.typ_menu == 2 : polx = next( (cp for cp in self.cenik.cenpol if cp.id_card == pol.id_card), None ) if not polx: continue cena = None for c in polx.ceny: if c.name == cc: cena = c break if not cena: return match pol.delitel: case 1: cenaxn = cena.cena case 2: cenaxn = cena.cena2 case 3: cenaxn = cena.cena3 if cena.cena3 is not None else cena.cena case 4: cenaxn = cena.cena4 if cena.cena4 is not None else cena.cena case _: cenaxn = cena.cena pol.cenhlad = cena.name pol.cena = cenaxn pol.dph = cena.dan pol.mena = cena.mena pol.selected = False pol.sel_pocet = None if update_defaults: pol.def_cena = pol.cena pol.def_dph = pol.dph pol.def_hlad = pol.cenhlad elif p.typ_menu==10: p.cenhlad = cena.name p.dph = cena.dan p.mena = cena.mena p.selected = False p.sel_pocet = None elif p.typ_menu==11: p.cenhlad = cena.name p.dph = cena.dan p.mena = cena.mena p.selected = False p.sel_pocet = None elif p.typ_menu==12: p.cena = cenan p.cenhlad = cena.name p.dph = cena.dan p.mena = cena.mena p.selected = False p.sel_pocet = None elif p.typ_menu==0: p.cena = cenan p.cenhlad = cena.name p.dph = cena.dan p.mena = cena.mena p.selected = False p.sel_pocet = None if update_defaults: p.def_cena = cenan p.def_dph = p.dph p.def_hlad = p.cenhlad def update_action_panel(self): selected = self.get_selected_items() if not selected: self.hide_action_panel() return pridavaj = True for pol in selected: if pol.typ_menu == 10 or pol.typ_menu == 11 or pol.typ_menu == 12: pridavaj = False exit self.show_action_panel() self.action_panel.clear_widgets() # 🔥 INFO self.action_panel.add_widget(Label( text=f"[b]{self.tr('pos.selected','Vybrané')}: {len(selected)}[/b]", markup=True, size_hint_y=None, height=dp(40) )) # 🔥 AKCIE if pridavaj: self._add_action_btn("➕ "+self.tr("pos.add","Pridať"), self.action_add, self.action_add_with_qty) self._add_action_btn("❌ "+self.tr("button.delete","Zmazať"), self.action_delete) self._add_action_btn("💲 "+self.tr("pos.price_level","Cenová hladina"), self.action_price) if self.mamehosti: self._add_action_btn("👤 "+self.tr("pos.guest","Hosť"), self.action_move_to_guest) if self.mamechody: self._add_action_btn("🍽️ "+self.tr("pos.course","Chod"), self.action_move_to_course) self._add_action_btn("🪑 "+ self.tr("pos.table","Stôl"), self.on_split) Clock.schedule_once(lambda *_: self.position_action_panel(), 0) def action_select_all(self): for p in self.ucet.poloz: p.selected = True p.sel_pocet = p.pocet self.refresh_ucet() self.update_action_panel() def action_clear_selection(self): for p in self.ucet.poloz: p.selected = False p.sel_pocet = None self.refresh_ucet() self.hide_action_panel() def _add_action_btn(self, text, short_cb, long_cb=None): btn = Button( text=text, size_hint_y=None, height=dp(50), background_normal="", background_color=(0.25,0.25,0.25,1) ) def touch_down(widget, touch): if not widget.collide_point(*touch.pos): return False widget._lp_fired = False widget._lp_ev = Clock.schedule_once( lambda dt: long_press(widget), LONG_TOUCH_TIME ) return True def touch_up(widget, touch): if not widget.collide_point(*touch.pos): return False ev = getattr(widget, "_lp_ev", None) if ev: ev.cancel() # long press už prebehol if widget._lp_fired: return True if short_cb: short_cb() return True def long_press(widget): widget._lp_fired = True if long_cb: long_cb() elif short_cb: short_cb() btn.bind(on_touch_down=touch_down) btn.bind(on_touch_up=touch_up) self.action_panel.add_widget(btn) def get_selected_items(self): return [p for p in self.ucet.poloz if p.selected] def get_selected_items_with_qty(self): normal = [] menus = defaultdict(list) #menus = [] for p in self.ucet.poloz: if not p.selected: continue sel = getattr(p, "sel_pocet", 0) if p.typ_menu == 0 or p.typ_menu == 10 or p.typ_menu == 11 or p.typ_menu == 12: if sel and sel < p.pocet: normal.append((p, sel)) # len časť else: normal.append((p, p.pocet)) # celé else: menus[p.group_id].append((p)) return normal, menus def action_toggle_select_all(self): all_selected = all(p.selected for p in self.ucet.poloz if p.pocet > 0) for p in self.ucet.poloz: if p.pocet <= 0: continue if all_selected: p.selected = False p.sel_pocet = None else: p.selected = True p.sel_pocet = p.pocet self.refresh_ucet() self.update_action_panel() def _get_active_guest(self): if not self.ucet.guests: return None if self.active_guest_id == "ALL": return None return next( (g for g in self.ucet.guests if g["id"] == self.active_guest_id), None ) def set_active_guest(self, gid): self.active_guest_id = gid self.refresh_guests_bar() self.refresh_ucet() # 🔥 toto ti chýbalo def rename_guest(self): if self.limit_mode: return guests = self.ucet.guests g = self._get_active_guest() def on_done(txt): if not txt: return g["name"] = txt self.refresh_guests_bar() keyboard = PosKeyboard( on_key=self.dispatch_key, bezokesc=True ) modal = TextMessageDialog(title=self.tr('pos.meno_hosta', 'Meno hosťa'),on_done=on_done, modal_manager=self.modal_manager, keyboard=keyboard) self.modal_manager.open(modal) def rename_course(self, course_id): if self.limit_mode: return course = next(c for c in self.ucet.courses if c["id"] == course_id) def on_done(txt): if not txt: return course["name"] = txt self.refresh_ucet() keyboard = PosKeyboard( on_key=self.dispatch_key, bezokesc=True ) modal = TextMessageDialog(title=self.tr('pos.nazov_chodu', 'Názov chodu'),on_done=on_done, modal_manager=self.modal_manager, keyboard=keyboard) self.modal_manager.open(modal) def _get_active_guest_name(self): if self.active_guest_id == "ALL": return self.tr('pos.vsetci', 'Všetci') for g in self.ucet.guests: if g["id"] == self.active_guest_id: return g["name"] return "?" def bind_long_press(self, widget, on_short, on_long): LONG_PRESS_TIME = 0.4 def touch_down(w, touch): if not w.collide_point(*touch.pos): return False w._lp_fired = False def fire_long(dt): w._lp_fired = True on_long() w._lp_event = Clock.schedule_once(fire_long, LONG_PRESS_TIME) return True def touch_up(w, touch): if not w.collide_point(*touch.pos): return False ev = getattr(w, "_lp_event", None) if ev: ev.cancel() if not getattr(w, "_lp_fired", False): on_short() return True widget.bind(on_touch_down=touch_down) widget.bind(on_touch_up=touch_up) def bind_long_press_old(self, btn, on_short, on_long, delay=0.5): from kivy.clock import Clock btn._lp_trigger = None btn._long_press = False def on_down(instance, touch): if not instance.collide_point(*touch.pos): return False instance._long_press = False def trigger(dt): instance._long_press = True on_long() instance._lp_trigger = Clock.schedule_once(trigger, delay) return True def on_up(instance, touch): if instance._lp_trigger: instance._lp_trigger.cancel() instance._lp_trigger = None # 🔥 dôležité: kontrola času stlačenia if instance._long_press: return True # 👉 iba ak sa pustilo NA BUTTONE if instance.collide_point(*touch.pos): on_short() return True btn.bind(on_touch_down=on_down) btn.bind(on_touch_up=on_up) def add_guest_with_name(self): def on_done(txt): if not txt: return self._add_guest(txt) keyboard = PosKeyboard( on_key=self.dispatch_key, bezokesc=True ) modal = TextMessageDialog(title=self.tr("pos.meno_hosta", "Meno hosťa") ,on_done=on_done, modal_manager=self.modal_manager, keyboard=keyboard) self.modal_manager.open(modal) def _add_guest(self, name=None): guests = self.ucet.guests new_n = len(guests) + 1 new_id = f"g{new_n}" guests.append({ "id": new_id, "name": name or f"{self.tr('pos.guest','Hosť')} {new_n}" }) def after(dt): self.set_active_guest(new_id) Clock.schedule_once(after, 0) def _add_course(self, name=None): courses = self.ucet.courses new_n = len(courses) + 1 new_id = f"c{new_n}" courses.append({ "id": new_id, "name": name or f"{self.tr('pos.course','Chod')} {new_n}" }) self.active_course_id = new_id self.refresh_ucet() def add_course_with_name(self): def on_done(txt): if not txt: return self._add_course(txt) keyboard = PosKeyboard( on_key=self.dispatch_key, bezokesc=True ) modal = TextMessageDialog(title=self.tr('pos.nazov_chodu','Názov chodu'),on_done=on_done, modal_manager=self.modal_manager, keyboard=keyboard) self.modal_manager.open(modal) def refresh_guests_bar(self): self.guests_bar.clear_widgets() guests = self.ucet.guests or [] if not guests and self.active_guest_id != "ALL": return # 👤 aktuálny hosť current = self._get_active_guest() valid_ids = ["ALL"] + [g["id"] for g in guests] if self.active_guest_id not in valid_ids: self.active_guest_id = "ALL" # ⬅️ btn_prev = Button( text="<", size_hint=(None, 1), width=dp(40) ) btn_prev.bind(on_press=lambda *_: self._prev_guest()) if self.mamehosti: self.guests_bar.add_widget(btn_prev) # 👤 názov btn_name = Button( text=self._get_active_guest_name(), size_hint=(None, 1), width=dp(140), background_normal="", background_color=(0.2,0.5,0.9,1) ) self.bind_long_press( btn_name, on_short=lambda: self.toggle_guest_selection(self.active_guest_id), on_long=self.rename_guest ) #btn_name.bind(on_press=lambda *_: self.rename_guest()) if self.mamehosti: self.guests_bar.add_widget(btn_name) # ➡️ btn_next = Button( text=">", size_hint=(None, 1), width=dp(40) ) btn_next.bind(on_press=lambda *_: self._next_guest()) if self.mamehosti: self.guests_bar.add_widget(btn_next) # spacer (roztiahne pravú časť doprava) self.guests_bar.add_widget(Widget()) # ➕ hosť btn_add_guest = Button( text="+"+self.tr("pos.guest","hosť"), size_hint=(None, 1), width=dp(80), background_normal="", background_color=(0.2, 0.6, 0.2, 1) ) if not self.limit_mode: self.bind_long_press( btn_add_guest, on_short=self.add_guest, on_long=self.add_guest_with_name ) if self.mamehosti and not self.limit_mode: self.guests_bar.add_widget(btn_add_guest) # ➕ chod btn_add_course = Button( text="+"+self.tr("pos.course","chod"), size_hint=(None, 1), width=dp(80), background_normal="", background_color=(0.2, 0.6, 0.2, 1) ) if not self.limit_mode: self.bind_long_press( btn_add_course, on_short=self.add_course, on_long=self.add_course_with_name ) if self.mamechody and not self.limit_mode: self.guests_bar.add_widget(btn_add_course) def _prev_guest(self): guests = self.ucet.guests or [] if not guests: return ids = ["ALL"] + [g["id"] for g in guests] idx = ids.index(self.active_guest_id) self.set_active_guest(ids[(idx - 1) % len(ids)]) def _next_guest(self): guests = self.ucet.guests or [] if not guests: return ids = ["ALL"] + [g["id"] for g in guests] idx = ids.index(self.active_guest_id) self.set_active_guest(ids[(idx + 1) % len(ids)]) def add_guest(self): if self.limit_mode: return self._add_guest() def add_course(self): if self.limit_mode: return self._add_course() def _scroll_to_course(self, course_id): # zatiaľ placeholder – neskôr môžeme spraviť presný scroll self.account.scroll_y = 0 def set_active_course(self, course_id): self.active_course_id = course_id self.refresh_ucet() # prefarbenie separatora # Milan 13.03.2026 def _on_window_resize(self, *args): # šírka ľavého panelu gap_x = self._menu_gap_x() gap_y = self._menu_gap_y() total_w = Window.width-self.scroll.width self.cell_w = (total_w - (self.menu_cols-1)*gap_x)/self.menu_cols if self.cell_w<30: self.cell_w=30 cell_h = self._menu_cell_h() for btn in self.left_matrix_buttons : xxspan=btn._span if xxspan<1: xxspan=1 btn_w = self._menu_span_width(xxspan) btn_h = cell_h x = btn._col * (self.cell_w + gap_x) y = self.total_h - (btn._line + 1) * cell_h - btn._line * gap_y btn.size=(btn_w, btn_h) btn.pos=(x, y) self.menu.canvas.ask_update() # ========================================================== def _build_info_bar(self, main): bar = BoxLayout( orientation="horizontal", size_hint=(1, None), height=dp(36), spacing=dp(6), ) self.lbl_info_left = Label( text="", halign="left", valign="middle", ) self.lbl_info_left.bind( size=lambda inst, *_: setattr(inst, "text_size", inst.size) ) self.lbl_info_sum = Label( text="0 "+getattr(self.setup, "zkr_mena", "Kč") if self.setup else "Kč", size_hint=(None, 1), width=dp(160), halign="right", valign="middle", ) self.lbl_info_sum.bind( size=lambda inst, *_: setattr(inst, "text_size", inst.size) ) bar.add_widget(self.lbl_info_left) bar.add_widget(self.lbl_info_sum) main.add_widget(bar) def _build_menu_from_cenik_safe(self, *_): import traceback from kivy.logger import Logger try: self._build_menu_from_cenik() except Exception: traceback.print_exc() def _menu_gap_x(self): return dp(6) def _menu_gap_y(self): return dp(6) def _menu_cell_w(self): # return MENU_BTN_W return self.cell_w def _menu_cell_h(self): return MENU_BTN_H def _menu_span_width(self, span: int) -> float: span = max(1, span) # return self._menu_cell_w() * span + self._menu_gap_x() * (span - 1) return self.cell_w * span + self._menu_gap_x() * (span - 1) def _menu_total_cols(self): return max( ( pos.col + max(1, getattr(pos, "sirka", 1)) - 1 for pol in self.cenik.cenpol for pos in (pol.pos_pc or []) ), default=0, ) + 1 def _menu_total_rows(self): return max( ( pos.line for pol in self.cenik.cenpol for pos in (pol.pos_pc or []) ), default=0, ) + 1 def _on_search_back(self): self._end_search() return True def _build_menu_from_items(self, items): self.menu.clear_widgets() self.left_matrix_buttons.clear() if self.search_toolbar.parent is None: self.leftpanel.add_widget(self.search_toolbar, index=len(self.leftpanel.children)) gap_x = self._menu_gap_x() gap_y = self._menu_gap_y() total_w = self.menu_scroll.width - dp(12) cols = 4 cell_w = (total_w - (cols - 1) * gap_x) / cols cell_h = self._menu_cell_h() row = 0 col = 0 if items: for pol in items: x = col * (cell_w + gap_x) y = row * (cell_h + gap_y) btn = POSMenuButton( size_hint=(None, None), size=(cell_w, cell_h), pos=(x, y), #background_normal="", background_color=(0.2, 0.2, 0.2, 1), ) btn.pol = pol btn.nazev = pol.d_name self._update_button_price(btn) btn._tap_handler = lambda b, pol=pol: self._handle_cenpol_click(pol) btn.on_long_touch = lambda b, pol=pol: self._open_fast_menu(pol) btn.text = pol.d_name btn.halign = "center" btn.valign = "middle" self.menu.add_widget(btn) col += 1 if col >= cols: col = 0 row += 1 total_h = (row + 1) * cell_h + row * gap_y #self.menu.size = (total_w, total_h) self.menu.size = (total_w, total_h) Clock.schedule_once(lambda *_: self._scroll_menu_to_origin(), 0) def _build_menu_from_cenik(self, items=None): if items is not None: return self._build_menu_from_items(items) from kivy.clock import Clock self.menu.clear_widgets() self.left_matrix_buttons.clear() current_page = getattr(self, "current_page", 1) gap_x = self._menu_gap_x() gap_y = self._menu_gap_y() total_w = Window.width-self.scroll.width cell_h = self._menu_cell_h() page_items = [] self.menu_cols = 0 self.menu_rows = 0 # ---------- SBĚR POLOŽEK PRO AKTUÁLNÍ STRÁNKU ---------- for pol in self.cenik.cenpol: if not pol.positions: continue for pos in pol.positions: if pos.page != current_page: continue span = max(1, getattr(pos, "sirka", 1) or 1) page_items.append((pol, pos, span)) self.menu_cols = max(self.menu_cols, pos.col + span) self.menu_rows = max(self.menu_rows, pos.line + 1) if self.menu_cols < 1: self.menu_cols=1 self.cell_w = (total_w- (self.menu_cols-1)*gap_x)/self.menu_cols if self.cell_w < 30: self.cell_w = 30 # ---------- ROZMĚR CELÉ PLOCHY ---------- if not page_items: self.menu.size = (dp(10), dp(10)) return total_w = self.menu_cols * self.cell_w + max(0, self.menu_cols - 1) * gap_x total_h = self.menu_rows * cell_h + max(0, self.menu_rows - 1) * gap_y self.total_h = total_h self.menu.size = (max(total_w, dp(10)), max(total_h, dp(10))) # ---------- VLOŽENÍ BUTTONŮ ---------- for pol, pos, span in page_items: btn_w = self._menu_span_width(span) btn_h = cell_h x = pos.col * (self.cell_w + gap_x) y = total_h - (pos.line + 1) * cell_h - pos.line * gap_y if pol.id_card < 0 or pol.kod < 0: #Milan 12.03.2026 - opraveny target_page a doplneny background_color target_page = abs(pol.id_card) btn = POSMenuButton( text=pol.d_name, size_hint=(None, None), size=(btn_w, btn_h), pos=(x, y), #background_normal="", background_color=self._btn_color(pos.color), ) btn._tap_handler = lambda b, page=target_page: self._switch_page(page) btn._col=pos.col btn._line=pos.line btn._span=pos.sirka self.left_matrix_buttons.append(btn) else: btn = POSMenuButton( size_hint=(None, None), size=(btn_w, btn_h), pos=(x, y), background_color=self._btn_color(pos.color), #background_normal='' ) btn._col=pos.col btn._line=pos.line btn._span=pos.sirka btn.pol = pol btn.nazev = pol.d_name self.left_matrix_buttons.append(btn) self._update_button_price(btn) btn._tap_handler = lambda b, pol=pol: self._handle_cenpol_click(pol) btn.on_long_touch = lambda b, pol=pol: self._open_fast_menu(pol) btn.halign = "center" btn.valign = "middle" btn.text_size = (btn.width - dp(8), None) btn.bind(size=lambda inst, *_: setattr(inst, "text_size", (inst.width - dp(8), None))) self.menu.add_widget(btn) Clock.schedule_once(lambda *_: self._scroll_menu_to_origin(), 0) def open_text_input(self, p: UcPolEdit, parent_modal=None, *_): def on_text_done(txt): if not txt: return if txt not in p.zpravy: p.zpravy.append(txt) self.refresh_ucet() # 🔥 zavri parent modal (cez manager!) if parent_modal: self.modal_manager.close(parent_modal) # 🔥 reopen message menu self._ignore_mark_until = time() + 0.3 self._show_message_menu(p) # 🔥 keyboard pre tento modal keyboard = PosKeyboard( on_key=self.dispatch_key, bezokesc=True ) modal = TextMessageDialog( modal_manager=self.modal_manager, title=f"{self.tr('pos.sprava_pre','Správa pre')}:\n{p.nazev}", on_done=on_text_done, keyboard=keyboard ) self.modal_manager.open(modal) def _set_manual_message(self, txt): if not txt: return if txt not in self.selected_messages: self.selected_messages.append(txt) btn = Button( text=txt, size_hint_y=None, height=dp(48), ) btn.background_normal = "" btn.background_color = (0.05, 0.2, 0.6, 1) # 🔵 hneď selected btn.msg = txt # 🔥 vždy string! btn.bind(on_press=self._toggle_message) self.message_buttons.append(btn) self.msg_grid.add_widget(btn) # 🔥 TERAZ to funguje def _show_message_menu(self, p: UcPolEdit): if p.sent: return messages = self.setup.messages or [] pol = next( (cp for cp in self.cenik.cenpol if cp.id_card == p.id_card), None ) required = [ m.text for m in (pol.messagepol or []) ] povinne = ("povinna" in pol.atributes) messages=required+messages manualne = [ m for m in (p.zpravy or []) if m not in messages ] messages=manualne+messages #if not messages: # return root = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(10), ) modal = BaseModal( size_hint=(None, None), size=(dp(420), dp(420)), auto_dismiss=True, ) # ---------- horní tlačítka ---------- btn_row = BoxLayout( orientation="horizontal", spacing=dp(6), size_hint_y=None, height=dp(48), ) btn_back = Button(text=self.tr("button.ok","OK")) def confirm(*_): self._set_pol_messages(p, modal) Clock.schedule_once(lambda dt: modal.dismiss(), 0) btn_back.bind(on_release=confirm) btn_clear = Button( text=self.tr("button.delete","Zmazať"), background_normal="", background_color=(0.6, 0.2, 0.2, 1), ) def clear(*_): self._clear_pol_messages(p, modal) Clock.schedule_once(lambda dt: modal.dismiss(), 0) btn_clear.bind(on_release=clear) btn_row.add_widget(btn_back) btn_row.add_widget(btn_clear) root.add_widget(btn_row) # ---------- scroll se zprávami ---------- scroll = ScrollView( size_hint=(1,1), bar_width=dp(6) ) grid = GridLayout( cols=1, spacing=dp(6), size_hint_y=None, ) grid.bind(minimum_height=grid.setter("height")) for msg in messages: is_selected = msg in p.zpravy btn = Button( text=msg, size_hint_y=None, height=dp(48), ) # zvýraznění vybraných if is_selected: btn.background_normal = "" btn.background_color = (0.05, 0.2, 0.6, 1) else: if required and msg in required: btn.background_normal = "" if povinne: btn.background_color = (0.35, 0.25, 0.25, 1) else: btn.background_color = (0.25, 0.25, 0.25, 1) else: btn.background_color = (0.25, 0.25, 0.25, 1) btn.background_normal="" btn.bind(on_press=lambda inst, m=msg: self._toggle_pol_message(p, m, required, povinne, inst)) grid.add_widget(btn) btn_manual = Button(text=self.tr("pos.zadat_spravu","Zadať správu"), size_hint_y=None, height=dp(50)) btn_manual.bind(on_release=lambda *_: self.open_text_input(p, modal)) grid.add_widget(btn_manual) scroll.add_widget(grid) root.add_widget(scroll) modal.add_widget(root) modal.open() def _toggle_pol_message(self, p, msg, required, povinne, btn): if msg in p.zpravy: p.zpravy.remove(msg) btn.background_normal = "" if required and msg in required: if povinne: btn.background_color = (0.35, 0.25, 0.25, 1) else: btn.background_color = (0.25, 0.25, 0.25, 1) else: btn.background_color = (0.25, 0.25, 0.25, 1) else: p.zpravy.append(msg) btn.background_normal = "" btn.background_color = (0.05, 0.2, 0.6, 1) self.refresh_ucet() def _clear_pol_messages(self, p: UcPolEdit, modal): p.zpravy.clear() pol = next( (cp for cp in self.cenik.cenpol if cp.id_card == p.id_card), None ) povinne = ("povinna" in pol.atributes) required = { m.text for m in (pol.messagepol or []) } if povinne: spravy = [z for z in p.zpravy if z in required] if not spravy: messagebox(self.tr("pos.sprava_povinna_musis_vybrat","Musíte vybrať povinnú správu")) else: self._ignore_mark_until = time() + 0.3 modal.dismiss() self.refresh_ucet() else: self._ignore_mark_until = time() + 0.3 modal.dismiss() self.refresh_ucet() def _set_pol_messages(self, p: UcPolEdit, modal): pol = next( (cp for cp in self.cenik.cenpol if cp.id_card == p.id_card), None ) povinne = ("povinna" in pol.atributes) required = { m.text for m in (pol.messagepol or []) } if povinne: spravy = [z for z in p.zpravy if z in required] if not spravy: messagebox(self.tr("pos.sprava_povinna_musis_vybrat","Musíte vybrať povinnú správu")) else: self._ignore_mark_until = time() + 0.3 modal.dismiss() self.refresh_ucet() else: self._ignore_mark_until = time() + 0.3 modal.dismiss() self.refresh_ucet() def _calculate_total(self): if not self.ucet or not self.ucet.poloz: return 0.0 total = 0.0 for p in self.ucet.poloz: #den = p.delitel or 1 # qty = p.pocet/den - kedze uz mam priamo vypocitanu cenu za zlomkovu porciu, nemozem este cenu delit zlomkom qty = p.pocet total += qty * p.cena return round(total, 2) def _scroll_ucet_to_top(self, *_): self.scroll.scroll_y = 1 def _update_info_bar(self): if not self.ucet: return app = App.get_running_app() ctrl = self.controller cfg = app.cfg stul = getattr(self.ucet, "table_name", "") or getattr(self.ucet, "stul", "-") user = getattr(ctrl.user_login, "name", "-") if ctrl.user_login else "-" id_kas = cfg.id_kas client_id = cfg.client_id total = self._calculate_total() self.lbl_info_left.text = ( f"{self.tr('pos.table','Stôl')} {stul} " f"{user} " f"{self.tr('pos.kasa','Kasa')} {id_kas} " f"{self.tr('pos.terminal','Terminál')} {client_id}" ) self.lbl_info_sum.text = f"{total:,.2f} "+getattr(self.setup, "zkr_mena", "Kč") if self.setup else "Kč" def on_split(self, *_): if not any(p.selected for p in self.ucet.poloz): self._popup_info( f"{self.tr('pos.prevod','Prevod')}", f"{self.tr('pos.prevod_bez_poloziek','Nie su vybrané žiadne položky na prevod.')}" ) u_main, u_sec, op = self.finalize("split") self.dispatch("on_finish", u_main, u_sec, op) # ukončit POSDialog self._close_self() def _close_self(self): sm = self.manager if not sm: return # přepnout pryč ještě před odebráním (bezpečné pro některé verze Kivy) if sm.current == self.name: sm.current = "account" # odebrat přímo instanci if self in sm.screens: sm.remove_widget(self) def on_finish(self, u_main, u_sec, operation): """ Event tady se NIKDY nic neukládá. Slouží jen jako signál ven. """ pass def on_save(self, *_): u_main, u_sec, op = self.finalize("edit_only") self.dispatch("on_finish", u_main, u_sec, op) self._close_self() def on_esc(self, *_): if not self._has_changes(): # žádné změny → rovnou zavřít self.dispatch("on_finish", None, None, "noop") self._close_self() return self._confirm_cancel() def _select_page_from_menu(self, page, modal): self._ignore_mark_until = time() + 0.3 modal.dismiss() Logger.info(f"POS: switching to page {page}") self.current_page = page self._build_menu_from_cenik() def _show_pages_menu(self): if not self.cenik or not self.cenik.cenpol: return # zjistit unikátní stránky pages = self.menu_pages or sorted({ pos.page for cp in self.cenik.cenpol for pos in (cp.positions or []) }) if not pages: return Logger.info(f"POS: show pages menu {pages}") modal = ModalView( size_hint=(None, None), size=(dp(500), dp(420)), auto_dismiss=True, ) grid = GridLayout( cols=4, spacing=dp(10), padding=dp(10), size_hint_y=None, ) # dynamická výška gridu rows = (len(pages) + 3) // 4 grid.height = rows * dp(70) txtstrana=self.tr("pos.strana","Strana") for page in pages: # zvýraznění aktuální stránky is_current = getattr(self, "current_page", 0) == page btn = Button( text=f"{txtstrana} {page}", size_hint_y=None, height=dp(60), background_normal="", background_color=( (0.2, 0.6, 1, 1) if is_current else (0.2, 0.2, 0.2, 1) ), ) btn.bind( on_press=lambda _, p=page: self._select_page_from_menu(p, modal) ) grid.add_widget(btn) modal.add_widget(grid) modal.open() def on_mark_by_code(self, *_): self.action_clear_selection() def accept(val: str): if not val: return val = val.strip() # SAMOTNÁ "." → menu stránek if val == "0.": self._show_pages_menu() return code, qty = self._parse_code_qty(val) pol, coef = self._find_item_with_ean(code) if not pol: self._popup_not_found(code) return if coef: qty *= coef self._handle_cenpol_click(pol, qty=qty) #tu sa vrat NumberPad( mode="code", allow_fraction=False, # ← důležité, jinak tečku nezadáš allow_text=True, decimal_places=0, max_len=13, on_accept=accept, ).open() def on_loyalty_card(self, *_): self._popup_info( self.tr("button.loyalty_card","Vernostná karta"), "Funkcia vernostnej karty zatiaľ nie je napojená.", ) def _apply_default_price(self, level: str, popup): self.default_price_level = level self._ignore_mark_until = time() + 0.3 popup.dismiss() self.refresh_menu_prices() def _popup_info(self, title, text): box = BoxLayout(orientation="vertical", padding=dp(10), spacing=dp(10)) box.add_widget(Label(text=text, halign="center", valign="middle")) popup = Popup( title=title, content=box, size_hint=(None, None), size=(dp(420), dp(200)), auto_dismiss=False, ) btn = Button(text=self.tr("button.ok","OK"), size_hint_y=None, height=dp(48)) def _dismiss_after_touch(*_): Clock.schedule_once(lambda *_: popup.dismiss(), 0) btn.bind(on_release=_dismiss_after_touch) box.add_widget(btn) popup.open() def _get_default_payment(self): #Vrátí defaultní platební typ nebo None platby = self._available_payment_types() if not platby: return None for p in platby: if getattr(p, "is_default", False): return p # fallback – první v seznamu return platby[0] def _available_payment_types(self): platby = list(self.payments or []) if not platby and self.setup: platby = list(getattr(self.setup, "platby", []) or []) return sorted( platby, key=lambda p: (getattr(p, "poradie", 0) or 0, str(getattr(p, "name", ""))), ) def _get_payment_by_setting(self, value): platby = self._available_payment_types() if not platby or value is None: return None if hasattr(value, "code") and hasattr(value, "name"): return value if isinstance(value, dict): value = value.get("code") or value.get("name") raw = str(value).strip() if not raw: return None raw_upper = raw.upper() for p in platby: if raw_upper in ( str(getattr(p, "code", "")).upper(), str(getattr(p, "name", "")).upper(), ): return p if raw.isdigit(): idx = int(raw) if 1 <= idx <= len(platby): return platby[idx - 1] if 0 <= idx < len(platby): return platby[idx] return None def _get_quick_payment_types(self): slots = [ getattr(self.setup, "objednavka_tlacitko1", "") if self.setup else "", getattr(self.setup, "objednavka_tlacitko2", "") if self.setup else "", getattr(self.setup, "objednavka_tlacitko3", "") if self.setup else "", ] quick_payments = [] seen = set() for slot in slots: ptype = self._get_payment_by_setting(slot) if not ptype: continue key = str(getattr(ptype, "code", "") or getattr(ptype, "name", "")).upper() if key in seen: continue quick_payments.append(ptype) seen.add(key) if not quick_payments: p_def = self._get_default_payment() if p_def: quick_payments.append(p_def) return quick_payments[:3] def _payment_discounts(self): ctrl = self.controller zlavy = getattr(ctrl, "zlavy", None) return list(getattr(zlavy, "zlavy", []) or []) def _payment_discount_permissions(self): return list(getattr(self.controller, "_discounts", []) or []) def _payment_discounts_all_allowed(self): user = getattr(self.controller, "user_login", None) return bool(getattr(user, "is_admin", False)) def _set_payment_printer(self, printer_no): self.default_printer = printer_no self._refresh_printer_button() def _open_payment_dialog(self, ucet, done, preferred_payment=None, quick_complete=False): dialog = payment.PaymentDialog( ucet=ucet, payment_types=self._available_payment_types(), setup=self.setup, on_done=done, on_cancel=lambda *_: None, discounts=self._payment_discounts(), discount_permissions=self._payment_discount_permissions(), discounts_all_allowed=self._payment_discounts_all_allowed(), printers=self.printers, bankterms=self.bankterms, default_printer=self.default_printer, on_printer_change=self._set_payment_printer, preferred_payment=preferred_payment, handler_runner=self._run_payment_handler, discount_runner=self._run_payment_discount, kasutxt=self.kasutxt, cenik_map=self.cenik_map, quick_complete=quick_complete, controller=self.controller ) if quick_complete: self._quick_payment_dialog = dialog if not dialog.run_quick_payment(preferred_payment): self._quick_payment_dialog = None return dialog dialog.open() return dialog def _run_payment_discount(self, zlava, ucet, dialog=None): platbatxt=self.tr('button.pay','Platba') typ = int(getattr(zlava, "typ_zlavy", 1) or 1) if typ != 2: return True try: price_level = str(int(float(getattr(zlava, "zl_koef", 0) or 0))) except Exception: messagebox(self.tr("pos.neplatna_cen_hladina","Neplatná cenová hladina v nastavení zľavy."), platbatxt) return False if not price_level or price_level == "0": messagebox(self.tr("pos.neplatna_cen_hladina","Neplatná cenová hladina v nastavení zľavy."), platbatxt) return False allowed_line_ids = None if dialog is not None and hasattr(dialog, "_discountable_items"): allowed_line_ids = { str(getattr(pol, "line_id", "") or "") for pol in dialog._discountable_items(zlava) if str(getattr(pol, "line_id", "") or "") } if not allowed_line_ids: return False self._apply_payment_price_level(ucet, price_level, allowed_line_ids=allowed_line_ids) return True def _run_payment_handler(self, ptype, ucet, dialog=None): name, params = self._parse_payment_handler(getattr(ptype, "handler", "")) if not name: return True if name == "dotaz_re": return self._handler_dotaz_re(ucet, params) if name == "dotaz_st": return self._handler_dotaz_st(ptype, ucet, dialog, params) if name == "dotaz_ho": return self._handler_dotaz_ho(ptype, ucet, dialog, params) messagebox(f"Obslužný program platby {name} zatiaľ nie je napojený.", self.tr('button.pay','Platba')) return True def _parse_payment_handler(self, handler): text = str(handler or "").strip() if not text: return "", [] text = text.lstrip("#") text = re.sub(r"\.py\b", "", text, flags=re.IGNORECASE) match = re.match(r"^\s*([A-Za-z0-9_]+)\s*(?:\((.*)\)|(.*))?$", text) if not match: return text.lower(), [] name = match.group(1).lower() raw_params = match.group(2) if match.group(2) is not None else (match.group(3) or "") if raw_params.strip() and "," in raw_params: params = [ p.strip().strip("\"'") for p in raw_params.split(",") ] return name, params params = [ p.strip().strip("\"'") for p in re.split(r"[,;:\s]+", raw_params) if p.strip() ] return name, params def _handler_dotaz_re(self, ucet, params): price_level_id = self._handler_price_level_id(params[0] if params else "") discount_param = params[1] if len(params) > 1 else "" result = {} if params and not price_level_id: return False if price_level_id: self._apply_payment_price_level(ucet, price_level_id) result["price_level"] = price_level_id discount = self._parse_handler_discount(discount_param) if discount: key, value = discount result[key] = value if not result: return True note_parts = [] if price_level_id: note_parts.append(f"hladina id {price_level_id}") if discount: note_parts.append(f"zľava {discount[1]:g}{'%' if discount[0] == 'discount_pct' else ''}") result["note"] = "dotaz_re: " + ", ".join(note_parts) return result def _handler_dotaz_st(self, ptype, ucet, dialog, params): price_level_id = self._handler_price_level_id(params[0] if params else "") discount_param = params[1] if len(params) > 1 else "" if params and params[0] and not price_level_id: return False discount = self._parse_handler_discount(discount_param) if discount_param and not discount: messagebox(self.tr("pos.neplatny_param_zlavy_dotaz_st","Neplatný parameter zľavy pre dotaz_st."), self.tr('button.pay','Platba')) return False try: companies = api_call.load_uvery_API(self.controller.ctx) except Exception as e: messagebox(f"{self.tr('pos.nenacitane_firmy','Firmy pre úverový záznam sa nepodarilo načítať')}:\n{e}", self.tr('button.pay','Platba')) return False def open_edit(firma=None): modal = CreditCompanyEditDialog( parent=self, firma=firma, on_done=lambda zaznam: self._finish_dotaz_st( ptype, ucet, dialog, zaznam, price_level_id, discount, ), ) self.modal_manager.open(modal) modal = CreditCompanySelectDialog( parent=self, companies=companies, on_select=open_edit, on_new=lambda: open_edit(data.UverFirma()), ) self.modal_manager.open(modal) return False def _finish_dotaz_st(self, ptype, ucet, dialog, zaznam, price_level_id, discount): try: saved = api_call.save_uver_API( self.controller.ctx, data.UverFirma( id=zaznam.id, hjmeno=zaznam.hjmeno, adresa1=zaznam.adresa1, adresa2=zaznam.adresa2, adresa3=zaznam.adresa3, ico=zaznam.ico, icdph=zaznam.icdph, dic=zaznam.dic, ), ) except Exception as e: messagebox(f"{self.tr('pos.neulozena_firma','Firmu sa nepodarilo uložiť')}:\n{e}", self.tr('button.pay','Platba')) return record = data.UverZaznam( **saved.model_dump(), akcia=zaznam.akcia, #hjmeno=zaznam.hjmeno, #adresa1=zaznam.adresa1, #adresa2=zaznam.adresa2, #adresa3=zaznam.adresa3, #ico=zaznam.ico, #icdph=zaznam.icdph, #dic=zaznam.dic, schvalil=zaznam.schvalil, ) ucet.uver = record result = {} note_parts = [] if record.akcia: note_parts.append(f"akcia {record.akcia}") note_parts.append(f"hjmeno {record.hjmeno}") note_parts.append(f"adresa1 {record.adresa1}") note_parts.append(f"adresa2 {record.adresa2}") note_parts.append(f"adresa3 {record.adresa3}") note_parts.append(f"ico {record.ico}") note_parts.append(f"dic {record.dic}") note_parts.append(f"icdph {record.icdph}") note_parts.append(f"schvalil {record.schvalil}") if price_level_id: self._apply_payment_price_level(ucet, price_level_id) result["price_level"] = price_level_id note_parts.append(f"hladina id {price_level_id}") if discount: key, value = discount result[key] = value note_parts.append(f"zlava {value:g}{'%' if key == 'discount_pct' else ''}") note = "dotaz_st: " + ", ".join(note_parts) result["note"] = note result["handler_info"] = note if dialog: dialog._apply_handler_result(result) dialog._ask_payment_amount(ptype) def _handler_dotaz_ho(self, ptype, ucet, dialog, params): default_prefix = str(params[0] if params else "").strip() price_level_param = params[1] if len(params) > 1 else "" price_level_id = self._handler_price_level_id(price_level_param) if price_level_param and not price_level_id: return False try: receptions = api_call.load_hotel_receptions_API(self.controller.ctx) except Exception as e: messagebox(f"{self.tr('pos.recepcia_nenacitane','Recepcie sa nepodarilo načítať')}:\n{e}", self.tr('button.pay','Platba')) return False if not receptions: messagebox(self.tr("pos.recepcia_nenastavene", "Nie je nastavená žiadna recepcia"), self.tr('button.pay','Platba')) return False selected = None if default_prefix: selected = next( ( reception for reception in receptions if str(getattr(reception, "hor_prefix", "")).strip().lower() == default_prefix.lower() ), None, ) if selected is not None: self._dotaz_ho_open_targets(ptype, ucet, dialog, selected, price_level_id) return False if len(receptions) == 1: self._dotaz_ho_open_targets(ptype, ucet, dialog, receptions[0], price_level_id) return False modal = HotelReceptionSelectDialog( parent=self, receptions=receptions, on_select=lambda reception: self._dotaz_ho_open_targets( ptype, ucet, dialog, reception, price_level_id, ), ) self.modal_manager.open(modal) return False def _dotaz_ho_open_targets(self, ptype, ucet, dialog, reception, price_level_id): try: rooms_response = api_call.load_hotel_rooms_API( self.controller.ctx, getattr(reception, "id"), ) except Exception as e: messagebox(f"{self.tr('pos.recepcia_izby_nenacitane','Izby sa nepodarilo načítať')}:\n{e}", self.tr('button.pay','Platba')) return modal = HotelTargetSelectDialog( parent=self, reception=reception, rooms_response=rooms_response, on_room=lambda room: self._dotaz_ho_room_selected( ptype, ucet, dialog, reception, room, price_level_id, ), on_manual=lambda room_code: self._dotaz_ho_manual_room( ptype, ucet, dialog, reception, room_code, price_level_id, ), on_card=lambda: self._dotaz_ho_read_card( ptype, ucet, dialog, reception, price_level_id, ), ) self.modal_manager.open(modal) def _dotaz_ho_room_selected(self, ptype, ucet, dialog, reception, room, price_level_id): if getattr(room, "type", "") == "group": target = data.HotelChargeTarget( reception_id=getattr(reception, "id"), reception_name=getattr(reception, "hotel", ""), typ_hotel=getattr(reception, "typ_hotel", 0), target_type="group", group_id=getattr(room, "id", ""), group_name=getattr(room, "room_name", "") or getattr(room, "room_code", ""), account_id=getattr(room, "account_id", ""), room_code=getattr(room, "room_code", ""), building=getattr(room, "building", ""), ) self._finish_dotaz_ho(ptype, ucet, dialog, target, price_level_id) return self._dotaz_ho_load_guests(ptype, ucet, dialog, reception, room, price_level_id) def _dotaz_ho_manual_room(self, ptype, ucet, dialog, reception, room_code, price_level_id): room_code = str(room_code or "").strip() room = data.HotelRoom( type="room", id=room_code, room_code=room_code, room_name=room_code, ) self._dotaz_ho_load_guests(ptype, ucet, dialog, reception, room, price_level_id) def _dotaz_ho_read_card(self, ptype, ucet, dialog, reception, price_level_id): modal = HotelTextInputDialog( parent=self, title=self.tr("pos.recepcia_nacitanie_karty","Nacitanie hotelovej karty"), hint_text=self.tr("pos.recepcia_cislo_karty","Číslo karty"), on_done=lambda card_code: self._dotaz_ho_card_loaded( ptype, ucet, dialog, reception, card_code, price_level_id, ), ) self.modal_manager.open(modal) def _dotaz_ho_card_loaded(self, ptype, ucet, dialog, reception, card_code, price_level_id): try: card = api_call.check_hotel_card_API( self.controller.ctx, getattr(reception, "id"), card_code, ) except Exception as e: messagebox(f"{self.tr('pos.recepcia_neoverena_karta','Hotelovú kartu sa nepodarilo overiť')}:\n{e}", self.tr('button.pay','Platba')) return room = data.HotelRoom( type="room", id=card.room_id, room_code=card.room_code, room_name=card.room_code, account_id=card.account_id, ) if card.guest_id or card.guest_name: guest = data.HotelGuest( id=card.guest_id, guest_name=card.guest_name, room_id=card.room_id, room_code=card.room_code, account_id=card.account_id, ) self._dotaz_ho_guest_selected(ptype, ucet, dialog, reception, room, guest, price_level_id) return self._dotaz_ho_load_guests(ptype, ucet, dialog, reception, room, price_level_id) def _dotaz_ho_load_guests(self, ptype, ucet, dialog, reception, room, price_level_id): try: guests = api_call.load_hotel_guests_API( self.controller.ctx, getattr(reception, "id"), room_id=getattr(room, "id", ""), room_code=getattr(room, "room_code", ""), account_id=getattr(room, "account_id", ""), ) except Exception as e: messagebox(f"{self.tr('pos.recepcia_nenacitani_hostia','Nepodarilo sa načítať hostí z izby')}:\n{e}", self.tr('button.pay','Platba')) return guests = [ guest for guest in guests if int(getattr(guest, "result", 0) or 0) == 0 ] if not guests: messagebox(self.tr("pos.recepcia_ziaden_host", "Na izbe sa nenašiel žiadny hosť."), self.tr('button.pay','Platba')) return if len(guests) == 1: self._dotaz_ho_guest_selected(ptype, ucet, dialog, reception, room, guests[0], price_level_id) return modal = HotelGuestSelectDialog( parent=self, reception=reception, room=room, guests=guests, on_select=lambda guest: self._dotaz_ho_guest_selected( ptype, ucet, dialog, reception, room, guest, price_level_id, ), ) self.modal_manager.open(modal) def _dotaz_ho_guest_selected(self, ptype, ucet, dialog, reception, room, guest, price_level_id): target = data.HotelChargeTarget( reception_id=getattr(reception, "id"), reception_name=getattr(reception, "hotel", ""), typ_hotel=getattr(reception, "typ_hotel", 0), target_type="guest", room_id=getattr(room, "id", "") or getattr(guest, "room_id", ""), room_code=getattr(room, "room_code", "") or getattr(guest, "room_code", ""), building=getattr(room, "building", ""), account_id=getattr(guest, "account_id", "") or getattr(room, "account_id", ""), guest_id=getattr(guest, "id", ""), guest_name=getattr(guest, "guest_name", ""), ) self._finish_dotaz_ho(ptype, ucet, dialog, target, price_level_id) def _finish_dotaz_ho(self, ptype, ucet, dialog, target, price_level_id): result = {} result["hotel_charge"] = target if ucet is not None: ucet.hotel_charge = target if target.target_type == "group": note_parts = [f"skupina {target.group_name}"] note_parts.append(f"recepcia {target.reception_name}") else: note_parts = [f"izba {target.room_code}"] note_parts.append(f"host {target.guest_name}") note_parts.append(f"recepcia {target.reception_name}") if price_level_id: self._apply_payment_price_level(ucet, price_level_id) result["price_level"] = price_level_id note_parts.append(f"hladina id {price_level_id}") note = "dotaz_ho: " + ", ".join(x for x in note_parts if x) result["note"] = note result["handler_info"] = note if dialog: dialog._apply_handler_result(result) dialog._ask_payment_amount(ptype) def _handler_price_level_id(self, value): level_id = str(value or "").strip() if not level_id: return "" levels = list(getattr(self, "alllevels", []) or getattr(self, "levels", []) or []) valid_ids = { str(getattr(level, "ch", "")).strip() for level in levels if str(getattr(level, "ch", "")).strip() } if valid_ids and level_id not in valid_ids: messagebox( f"Cenova hladina s ID {level_id} nie je definovana.", self.tr('button.pay','Platba'), ) return "" return level_id def _parse_handler_discount(self, value): text = str(value or "").strip().replace(",", ".") if not text: return None lower = text.lower() absolute = ( lower.startswith("abs=") or lower.endswith("kc") or lower.endswith("kč") or lower.endswith("czk") or lower.endswith("eur") ) text = re.sub(r"^(abs=)", "", text, flags=re.IGNORECASE) text = re.sub(r"(kc|kč|czk|eur)$", "", text, flags=re.IGNORECASE).strip() pct = text.endswith("%") if pct: text = text[:-1].strip() try: number = float(text) except Exception: return None if number <= 0: return None if absolute: return "discount_abs", number if pct: return "discount_pct", number return "discount_pct", number def _apply_payment_price_level(self, ucet, price_level, allowed_line_ids=None): if not ucet: return allowed_line_ids = set(allowed_line_ids or []) for pol in list(getattr(ucet, "poloz", []) or []): if pol.typ_menu == 2: continue if allowed_line_ids and str(getattr(pol, "line_id", "") or "") not in allowed_line_ids: continue self._change_price_level( pol, price_level, update_defaults=False, ucet=ucet, ) def _pay_full_with_payment(self, p_def): if not self.ucet or not self.ucet.poloz: messagebox(self.tr("pos.nie_je_co_platit","Nie je čo platiť.")) return if not p_def: self.on_btn_payment() return u_main, u_sec = self._build_u_sec_for_payment() if not u_sec or not u_sec.poloz: messagebox(self.tr("pos.nie_je_co_platit","Nie je čo platiť.")) return if not self._validate_pohladavka_payment(u_sec): return def done(u_sec_paid): self._quick_payment_dialog = None op = "pay_full" if not u_main or not u_main.poloz else "pay_part" self.dispatch("on_finish", u_main, u_sec_paid, op) self._close_self() self._open_payment_dialog(u_sec, done, preferred_payment=p_def, quick_complete=True) def on_btn_quick_payment(self, ptype, *_): self._pay_full_with_payment(ptype) def on_btn_payment_default(self, *_): self._pay_full_with_payment(self._get_default_payment()) def split_ucet_for_payment(self, u_src): if not u_src or not u_src.poloz: return u_src, None u_main = deepcopy(u_src) u_sec = deepcopy(u_src) u_main.poloz = [] u_sec.poloz = [] for p in u_src.poloz: # --- výběr pouze podle počtu kusů if p.sel_pocet and p.sel_pocet > 0: # placená část p_pay = deepcopy(p) p_pay.pocet = p.sel_pocet p_pay.selected = False p_pay.sel_pocet = None u_sec.poloz.append(p_pay) # zbytek rest = p.pocet - p.sel_pocet if rest > 0: p_rest = deepcopy(p) p_rest.pocet = rest p_rest.selected = False p_rest.sel_pocet = None u_main.poloz.append(p_rest) continue # --- nevybrané položky u_main.poloz.append(deepcopy(p)) return u_main, u_sec def on_select_default_price(self, *_): # ===== ROOT ===== root = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(6), ) # ===== SCROLL ===== scroll = ScrollView( size_hint=(1, 1), do_scroll_y=True, ) list_box = BoxLayout( orientation="vertical", size_hint_y=None, spacing=dp(6), ) list_box.bind(minimum_height=list_box.setter("height")) # ===== DATA ===== levels = sorted({ c.name for cp in self.cenik.cenpol for c in (cp.ceny or []) }) if self.controller.user_login.is_admin: levely=levels else: levely= [ ll for ll in levels if ll in self.controller.user_login.levels ] # ===== BUTTONY ===== for lvl in levely: btn = Button( text=lvl, size_hint_y=None, height=dp(48), ) if lvl == self.default_price_level: btn.background_normal = "" btn.background_color = (0.2, 0.4, 0.8, 1) btn.bind(on_press=lambda _, l=lvl: self._apply_default_price(l, popup)) list_box.add_widget(btn) scroll.add_widget(list_box) root.add_widget(scroll) # ===== ZPĚT ===== btn_back = Button( text=self.tr("button.back","Späť"), background_color=(0.6,0.2,0.2,1), background_normal="", size_hint_y=None, height=dp(48), ) btn_back.bind(on_press=lambda *_: popup.dismiss()) root.add_widget(btn_back) # ===== POPUP ===== popup = Popup( title=self.tr("pos.def_cen_hlad","Východzia cenová hladina"), content=root, size_hint=(None, None), size=(dp(360), dp(420)), auto_dismiss=False, ) popup.open() def _find_cenpol_by_code(self, code: int): found = self._code_index_get(str(code)) return found[0] if found else None def _popup_not_found(self, code): box = BoxLayout( orientation="vertical", spacing=dp(10), padding=dp(10), ) box.add_widget(Label( text=f"{self.tr('pos_polozka_xy_nie_je_v_cenniku','Položka s kódem {code} nebyla nalezena v ceníku.').format(code=code)}", halign="center", valign="middle", )) btn_ok = Button( text=self.tr("button.ok","OK"), size_hint_y=None, height=dp(48), ) box.add_widget(btn_ok) popup = Popup( title=self.tr("pos.kod_nenajdeny","Kód nenájdený"), content=box, size_hint=(None, None), size=(dp(420), dp(200)), auto_dismiss=False, # ⬅️ důležité ) def close_popup(*_): self._ignore_mark_until = time() + 0.35 Clock.schedule_once(lambda *_: popup.dismiss(), 0.05) btn_ok.bind(on_release=close_popup) popup.open() def _confirm_cancel(self): box = BoxLayout( orientation="vertical", spacing=dp(10), padding=dp(10), ) box.add_widget(Label( text=self.tr("pos.naozaj_zrusit_editaciu_uctu","Naozaj chcete zrušiť editáciu účtu?")+"\n"+self.tr("pos.neulozene_zmenu_sa_stratia","Neuložené zmeny sa stratia"), halign="center", valign="middle", )) btns = BoxLayout( size_hint_y=None, height=dp(44), spacing=dp(10), ) btn_yes = Button(text=self.tr("common.yes","Áno")) btn_no = Button(text=self.tr("common.no","Nie")) btns.add_widget(btn_yes) btns.add_widget(btn_no) box.add_widget(btns) popup = Popup( title=self.tr("pos.zrusit_editaciu","Zrušiť editáciu"), content=box, size_hint=(None, None), size=(dp(420), dp(200)), auto_dismiss=False, ) btn_no.bind(on_press=lambda *_: popup.dismiss()) btn_yes.bind(on_press=lambda *_: ( popup.dismiss(), self.dispatch("on_finish", None, None, "noop"), self._close_self(), )) popup.open() # ----------------------------------------------------------------------- def _search_items(self, text): text = self._normalize_text(text) if not text: return [] if not hasattr(self, "_search_index") or not self._search_index: self._build_search_index() starts = [] contains = [] if text.isdigit() and len(text) >= 6: for row in self._search_index: eans = row["eans"] item = row["item"] matched_starts = False matched_contains = False for e in eans: if e.startswith(text): matched_starts = True break elif text in e: matched_contains = True if matched_starts: starts.append(item) elif matched_contains: contains.append(item) else: for row in self._search_index: item = row["item"] name = row["name"] kod = row["kod"] eans = row["eans"] matched_starts = False matched_contains = False # 🔹 NAME if name.startswith(text): matched_starts = True elif text in name: matched_contains = True # 🔹 KÓD elif kod.startswith(text): matched_starts = True elif text in kod: matched_contains = True # 🔹 EAN else: for e in eans: if e.startswith(text): matched_starts = True break elif text in e: matched_contains = True if matched_starts: starts.append(item) elif matched_contains: contains.append(item) return (starts + contains)[:50] def _fill_search_results(self, container, items): container.clear_widgets() for item in items: btn = self._create_menu_button(item) container.add_widget(btn) #milan def _create_search_button(self, item, dialog_root): btn = Button( text=f"{item.nazev}\n{item.cena:.2f}", size_hint=(1, None), height=dp(70), background_normal="", background_color=(0.25, 0.25, 0.25, 1) ) def on_press(*_): # 🔥 použij EXISTUJÚCU logiku self.on_menu_item_click(item) # zavri dialog self.remove_widget(dialog_root) btn.bind(on_press=on_press) return btn def _handle_cenpol_click(self, cenpol, qty=1): if getattr(self, "_ignore_touches", False): return self.action_clear_selection() self._request_messages_then( cenpol, callback=lambda zpravy: self._after_messages(cenpol, zpravy, qty) ) def _request_messages_then(self, cenpol, callback): messages = cenpol.messagepol or [] povinne = ("povinna" in cenpol.atributes) volnacena = ("volnacena" in cenpol.atributes) pohladavka = ("pohladavka" in cenpol.atributes) # 🟢 bez správ → rovno pokračuj if not messages: callback([]) return # 🔥 VŽDY otvor výber správ def after_select(zpravy): # ❌ povinné a nič nevybrané → stop if povinne and not zpravy: return # 🟢 nepovinné → môže byť prázdne callback(zpravy or []) self._select_required_message( cenpol, messages, on_done=after_select ) def _after_messages(self, cenpol, zpravy, qty=1, delitel=1): if self.is_fstmenu(cenpol): self._start_menu_flow(cenpol, zpravy, qty, delitel) elif self.is_volnacena(cenpol): self._open_price_input(cenpol) elif self.is_pohladavka(cenpol): self._open_price_input(cenpol) elif self.is_vazena(cenpol): self._open_quantity_input(cenpol,zpravy) else: self._mark_cenpol(cenpol, zpravy, qty, delitel) def _start_menu_flow(self, cenpol, zpravy, qty=1, delitel=1): menu = self.fstmenu_map.get(cenpol.id_card) if not menu: return state = self.build_menu_state(menu) group_id = self._new_group_id() parent = self._add_menu_header(cenpol, group_id, zpravy, qty, delitel) state.group_id = group_id state.parent_line_id = parent.line_id state.base_messages = zpravy # 🔥 KRITICKÉ self.process_menu_step(state) def _select_required_message(self, cenpol, messages, on_done): modal = ModalView(size_hint=(0.6, 0.7), auto_dismiss=False) root = BoxLayout(orientation="vertical", spacing=10, padding=10) grid = GridLayout(cols=1, spacing=5, size_hint_y=None) grid.bind(minimum_height=grid.setter("height")) scroll = ScrollView() scroll.add_widget(grid) def choose(msg_text): self._ignore_mark_until = time() + 0.3 modal.dismiss() on_done([msg_text]) for m in messages: btn = Button( text=m.text, size_hint_y=None, height=60 ) # fix closure (žiadny m=m.text hack problém) btn.bind(on_release=lambda instance, t=m.text: choose(t)) grid.add_widget(btn) title = Label( text=f"{self.tr('pos.vyber_spravu_pre', 'Vyber správu pre')}:\n[b]{cenpol.ch_name}[/b]", markup=True, size_hint_y=None, height=dp(60) ) root.add_widget(title) root.add_widget(scroll) # len cancel (voliteľné) btn_cancel = Button(text=self.tr("button.cancel","Zrušiť"), size_hint_y=None, height=70) def cancel(*_): self._ignore_mark_until = time() + 0.3 modal.dismiss() on_done([]) btn_cancel.bind(on_release=cancel) root.add_widget(btn_cancel) modal.add_widget(root) modal.open() def _open_fast_menu(self, cenpol): modal = self.FastItemMenu( self, cenpol, tr=self.tr, setup=self.setup, modal_manager=self.modal_manager, on_done=lambda zpravy, qty, delitel: self._fastmenu_confirm(cenpol, zpravy, qty, delitel) ) self.modal_manager.open(modal) def _open_quantity_input(self, cenpol, zpravy): def accept_quantity(value): try: qty = float(value.replace(",", ".")) except: return self._add_qty_item(cenpol, qty, zpravy) NumberPad( mode="number", initial_value="", allow_fraction=False, show_dot=True, decimal_places=3, on_accept=accept_quantity ).open() def _add_qty_item(self, cenpol, qty, zpravy): cenax = self._resolve_price_for_pol(cenpol, self.default_price_level) guest_id = self.active_guest_id if guest_id == "ALL": guest_id = self.ucet.guests[0]["id"] course_id = self.active_course_id limit_rov_id, limit_hlad_id = self._limit_context_ids(course_id, guest_id) self.ucet.poloz.append( UcPolEdit( id_card=cenpol.id_card, c_druh=getattr(cenpol, "c_druh", 0), druh=getattr(cenpol, "druh", ""), prn_no=getattr(cenpol, "prn_no", ""), nazev=cenpol.ch_name, cena=cenax.cena, # 🔥 TU override dph=cenax.dan, mena=cenax.mena, cenhlad=cenax.name, delitel=1, pocet=qty, zpravy=zpravy, edit_key=str(int(time() * 1000)), selected=False, def_cena=cenax.cena, def_dph=cenax.dan, def_hlad=cenax.name, typ_menu=12, line_id=self._new_line_id(), group_id=self._new_group_id(), pol_pocet=1, guest_id=guest_id, course_id=course_id, limit_rov_id=limit_rov_id, limit_hlad_id=limit_hlad_id ) ) self.refresh_ucet() def _open_price_input(self, cenpol): def accept_price(value): try: cena = float(value.replace(",", ".")) except: return self._open_fast_menu_for_price(cenpol, cena) NumberPad( mode="number", initial_value="", on_accept=accept_price ).open() def _open_fast_menu_for_price(self, cenpol, cena): class FakeCenpol: pass cp = FakeCenpol() cp.id_card = cenpol.id_card cp.ch_name = cenpol.ch_name cp.messagepol = cenpol.messagepol cp.atributes = cenpol.atributes # uložíme override cenu cp._free_price = cena modal = self.FastItemMenu( posdialog=self, cenpol=cp, tr=self.tr, setup=self.setup, modal_manager=self.modal_manager, on_done=lambda msgs, qty, deli: self._add_free_item( cenpol, cena, msgs, qty, deli ) ) self.modal_manager.open(modal) def _add_free_item(self, cenpol, cena, msgs, qty, deli): cenax = self._resolve_price_for_pol(cenpol, self.default_price_level) guest_id = self.active_guest_id if guest_id == "ALL": guest_id = self.ucet.guests[0]["id"] course_id = self.active_course_id limit_rov_id, limit_hlad_id = self._limit_context_ids(course_id, guest_id) self.ucet.poloz.append( UcPolEdit( id_card=cenpol.id_card, c_druh=getattr(cenpol, "c_druh", 0), druh=getattr(cenpol, "druh", ""), prn_no=getattr(cenpol, "prn_no", ""), nazev=cenpol.ch_name, cena=cena, # 🔥 TU override dph=cenax.dan, mena=cenax.mena, cenhlad=cenax.name, delitel=deli, pocet=qty, zpravy=msgs, edit_key=str(int(time() * 1000)), selected=False, def_cena=cena, def_dph=cenax.dan, def_hlad=cenax.name, typ_menu=10, line_id=self._new_line_id(), group_id=self._new_group_id(), pol_pocet=1, guest_id=guest_id, course_id=course_id, limit_rov_id=limit_rov_id, limit_hlad_id=limit_hlad_id ) ) self.refresh_ucet() def _fastmenu_confirm(self, cenpol, zpravy, qty, delitel): if self.is_fstmenu(cenpol): self._start_menu_flow(cenpol, zpravy, qty, delitel) elif self.is_volnacena(cenpol): self._open_price_input(cenpol) elif self.is_pohladavka(cenpol): self._open_price_input(cenpol) elif self.is_vazena(cenpol): self._open_quantity_input(cenpol, zpravy) else: self._mark_cenpol(cenpol, zpravy, qty, delitel) # ----------------------------------------------------------------------- class FastItemMenu(BaseModal): def __init__(self, posdialog, cenpol, on_done=None, modal_manager=None, setup=None, tr=None, **kwargs): super().__init__(**kwargs) self.on_done = on_done self.modal_manager = modal_manager spravy = [ m for m in (cenpol.messagepol or []) ] menu = posdialog.fstmenu_map.get(cenpol.id_card) self.posdialog = posdialog self.cenpol = cenpol self.tr=tr self.forced_message = spravy self.on_done = on_done self.qty_value = "1" self.qty_cleared = False self.delitel = 1 self.frac_buttons = [] self.selected_messages = [] self.message_buttons = [] self.setup=setup self.mametretiny = getattr(self.setup, "is_tretiny", True) if self.setup else True self.mamestvrtiny = getattr(self.setup, "is_stvrtiny", True) if self.setup else True self.size_hint = (None, None) self.size = (dp(500), dp(600)) self.auto_dismiss = False self.background_normal="" self.background_color = (0,0,0,0.4) if menu: self.is_menu = True else: self.is_menu = False self.pohladavka=("pohladavka" in cenpol.atributes) root = BoxLayout(orientation="vertical", spacing=dp(8), padding=dp(10)) # ====================== # DISPLAY množstva # ====================== title = Label( text=f"[b]{cenpol.ch_name}[/b]", markup=True, size_hint_y=None, height=dp(40), font_size=dp(20) ) root.add_widget(title) self.qty_label = Label( text=self.qty_value, size_hint_y=None, height=dp(50), font_size=dp(24) ) root.add_widget(self.qty_label) # ====================== # NUMPAD # ====================== numpad = GridLayout(cols=3, spacing=dp(6), size_hint_y=None) numpad.height = dp(240) def open_text_input(self, cenpol): keyboard = PosKeyboard( on_key=self.modal_manager.dispatch_key, bezokesc=True ) modal = TextMessageDialog( modal_manager=self.modal_manager, title=f"{self.tr('pos.sprava_pre','Správa pre')}:\n{cenpol.ch_name}", on_done=self._set_manual_message, keyboard=keyboard ) self.modal_manager.open(modal) def add_btn(txt, cb): b = Button(text=txt) b.bind(on_press=cb) numpad.add_widget(b) for t in ["1","2","3","4","5","6","7","8","9","C","0","OK"]: add_btn(t, self._on_numpad) content = BoxLayout(orientation="horizontal", spacing=dp(8)) left = BoxLayout(orientation="vertical", size_hint_x=0.5, spacing=dp(8)) right = BoxLayout(orientation="vertical", size_hint_x=0.5, spacing=dp(8)) left.add_widget(numpad) #root.add_widget(numpad) # ====================== # ZLOMKY # ====================== if not self.pohladavka: frac = GridLayout(cols=3, size_hint_y=None, height=dp(60), spacing=dp(6)) for txt, val in [("1/2",2),("1/3",3),("1/4",4)]: if val == 2 : b = Button(text=txt) b.bind(on_press=lambda inst, v=val, btn=b: self._set_fraction(v, btn)) self.frac_buttons.append(b) frac.add_widget(b) if self.mametretiny and val == 3 : b = Button(text=txt) b.bind(on_press=lambda inst, v=val, btn=b: self._set_fraction(v, btn)) self.frac_buttons.append(b) frac.add_widget(b) if self.mamestvrtiny and val == 4 : b = Button(text=txt) b.bind(on_press=lambda inst, v=val, btn=b: self._set_fraction(v, btn)) self.frac_buttons.append(b) frac.add_widget(b) left.add_widget(frac) #root.add_widget(frac) # ====================== # SPRÁVY # ====================== msgs = getattr(cenpol, "messagepol", None) self.povinna = ("povinna" in cenpol.atributes) self.msg_grid = GridLayout(cols=1, spacing=dp(6), size_hint_y=None) self.msg_grid.bind(minimum_height=self.msg_grid.setter("height")) if not self.pohladavka: if msgs: for m in msgs: btn = Button( text=m.text, size_hint_y=None, height=dp(48), ) if self.povinna: btn.background_color=(0.35,0.25,0.25,1) else: btn.background_color=(0.25,0.25,0.25,1) btn.background_normal="" btn.msg = m btn.bind(on_press=self._toggle_message) self.message_buttons.append(btn) self.msg_grid.add_widget(btn) scroll = ScrollView(size_hint=(1,1)) scroll.add_widget(self.msg_grid) right.add_widget(scroll) btn = Button(text=self.tr("pos.zadat_spravu", "Zadať správu"), size_hint_y=None, height=dp(50)) #btn.bind(on_release=partial(self.open_text_input, cenpol)) btn.bind(on_release=lambda *_: open_text_input(self, cenpol)) #btn.bind(on_release=open_text_input) right.add_widget(btn) # ====================== # POTVRDIŤ # ====================== confirm = Button( text=self.tr("button.ok","Potvrdiť"), size_hint_y=None, height=dp(60), background_normal="", background_color=(0.05,0.5,0.2,1) ) confirm.bind(on_release=self._confirm) #root.add_widget(confirm) # ====================== # SPÄŤ # ====================== back = Button(text=self.tr("button.back","Späť"), size_hint_y=None, height=dp(50), background_color=(0.6,0.2,0.2,1),background_normal="") back.bind(on_release=lambda *_: self._cancel()) #root.add_widget(back) #root.add_widget(self.qty_label) content.add_widget(left) content.add_widget(right) #root.add_widget(self.qty_label) root.add_widget(content) root.add_widget(confirm) root.add_widget(back) self.add_widget(root) def _cancel(self, *_): self._ignore_mark_until = time() + 0.3 self.dismiss() def _set_fraction(self, val, btn): self.delitel = val for b in self.frac_buttons: b.background_color = (1, 1, 1, 1) b.background_normal="" # zvýrazni kliknuté btn.background_color = (0.2, 0.6, 1, 1) btn.background_normal="" def _on_numpad(self, btn): t = btn.text self._apply_numpad_key(t) return def _apply_numpad_key(self, t): if t == "C": self.qty_value = "1" self.qty_cleared = False elif t == "OK": self._confirm() return elif t == "BACKSPACE": if not self.qty_cleared: self.qty_value = "" self.qty_cleared = True else: self.qty_value = self.qty_value[:-1] else: # 👇 prvý vstup vymaže default "1" if not self.qty_cleared: self.qty_value = "" self.qty_cleared = True self.qty_value += t if not self.qty_value: self.qty_value = "0" self.qty_label.text = self.qty_value def handle_key(self, key): raw_key = key key = str(key or "").strip().upper() if key in {"ESC", "ESCAPE"} or raw_key == 27: self._cancel() return True if key in {"ENTER", "NUMPADENTER", "NUMENTER", "KP_ENTER"} or raw_key in (13, 271): self._confirm() return True if key in {"BACKSPACE"} or raw_key == 8: self._apply_numpad_key("BACKSPACE") return True if key in {"DELETE", "C"} or raw_key == 127: self._apply_numpad_key("C") return True digit = None key_digit_map = { "0": "0", "1": "1", "2": "2", "3": "3", "4": "4", "5": "5", "6": "6", "7": "7", "8": "8", "9": "9", "NUMPAD0": "0", "NUMPAD1": "1", "NUMPAD2": "2", "NUMPAD3": "3", "NUMPAD4": "4", "NUMPAD5": "5", "NUMPAD6": "6", "NUMPAD7": "7", "NUMPAD8": "8", "NUMPAD9": "9", "KP_0": "0", "KP_1": "1", "KP_2": "2", "KP_3": "3", "KP_4": "4", "KP_5": "5", "KP_6": "6", "KP_7": "7", "KP_8": "8", "KP_9": "9", } if key in key_digit_map: digit = key elif isinstance(raw_key, int) and 48 <= raw_key <= 57: digit = str(raw_key - 48) elif isinstance(raw_key, int) and 96 <= raw_key <= 105: digit = str(raw_key - 96) elif ord(key) in range(256, 266): # numpad digit = str(ord(key) - 256) elif isinstance(raw_key, int) and 256 <= raw_key <= 265: digit = str(raw_key - 256) if digit is not None: digit = key_digit_map.get(digit, digit) self._apply_numpad_key(digit) return True return False def _set_manual_message(self, txt): if not txt: return if txt not in self.selected_messages: self.selected_messages.append(txt) btn = Button( text=txt, size_hint_y=None, height=dp(48) ) btn.background_color=(0.05,0.2,0.6,1) btn.background_normal="" btn.msg = txt btn.bind(on_press=self._toggle_message) self.message_buttons.append(btn) self.msg_grid.add_widget(btn) # 🔥 ak chceš UI feedback: if hasattr(self, "msg_preview"): self.msg_preview.text = " | ".join(self.selected_messages) def _toggle_message(self, btn): msg = btn.msg if isinstance(btn.msg, str) else btn.msg.text if msg in self.selected_messages: self.selected_messages.remove(msg) if self.povinna: btn.background_color = (0.35,0.25,0.25,1) else: btn.background_color = (0.25,0.25,0.25,1) else: self.selected_messages.append(msg) btn.background_color = (0.05,0.2,0.6,1) btn.background_normal="" def _confirm(self, *_): try: qty = int(self.qty_value) except: qty = 1 zpravy = self.selected_messages try: delitel = int(self.delitel) except: delitel = 1 if self.pohladavka and not zpravy: messagebox(self.tr("pos.povinne_cislo_faktury","Musíte zadať číslo faktúry")) else: self._ignore_mark_until = time() + 0.3 self.dismiss() if self.on_done: #self.on_done(qty, delitel, zpravy, chod) self.on_done(zpravy, qty, delitel) #-------------------------------------- def refresh_menu_prices(self): for btn in self.left_matrix_buttons: self._update_button_price(btn) #-------------------------------------- def _update_button_price(self, btn): if not hasattr(btn, "pol"): #placeholdery a linky nemaji pol return pol = btn.pol level = self.default_price_level cena = next( (c for c in (pol.ceny or []) if c.name == level), None ) if not cena: cena = next( (c for c in (pol.ceny or []) if c.name == "1"), None ) if cena: btn.text = f"{btn.nazev}\n{cena.cena:.0f} {cena.mena}" else: btn.text = f"{btn.nazev}\n—" #-------------------------------------- def _switch_page(self, page: int): from kivy.logger import Logger Logger.info(f"POS: switching to page {page}") self.current_page = page self._build_menu_from_cenik() def _scroll_menu_to_origin(self): # levý horní roh self.menu_scroll.scroll_x = 0 self.menu_scroll.scroll_y = 1 def _menu_metrics(self): total_rows = int(self.menu.height // MENU_BTN_H) visible_rows = int(self.menu_scroll.height // MENU_BTN_H) total_cols = self._menu_total_cols() visible_cols = int(self.menu_scroll.width // MENU_BTN_W) return total_rows, visible_rows, total_cols, visible_cols def _scroll_menu_to_pos(self, pos): if not pos: return total_rows, visible_rows, total_cols, visible_cols = self._menu_metrics() row = getattr(pos, "line", 0) col = getattr(pos, "col", 0) # ---------- VERTIKÁLA ---------- if total_rows > visible_rows: max_row_offset = total_rows - visible_rows if max_row_offset <= 0: self.menu_scroll.scroll_y = 1 else: target_row = min(row, max_row_offset) # scroll_y: 1 = nahoře, 0 = dole self.menu_scroll.scroll_y = 1 - (target_row / max_row_offset) else: self.menu_scroll.scroll_y = 1 # ---------- HORIZONTÁLA ---------- if total_cols > visible_cols: max_col_offset = total_cols - visible_cols if max_col_offset <= 0: self.menu_scroll.scroll_x = 0 else: target_col = min(col, max_col_offset) self.menu_scroll.scroll_x = target_col / max_col_offset else: self.menu_scroll.scroll_x = 0 def _find_price(self, pol): # shoda hladiny for c in pol.ceny: if c.name == self.default_price_level: #if c.name == self.price_level: return c # fallback – standardní CZK for c in pol.ceny: if c.name == self.default_price_level: return c # 4. poslední možnost – první cena return pol.ceny[0] if pol.ceny else None def _btn_color(self, color: int): COLORS = { 0: (0.25, 0.25, 0.25, 1), 1: (0.2, 0.5, 0.8, 1), 2: (0.6, 0.2, 0.2, 1), 3: (0.2, 0.6, 0.3, 1), # 4: (0.976, 1.000, 0.733, 1), # 5: (0.902, 0.620, 0.306, 1), # 6: (1.000, 0.435, 0.808, 1), # 7: (0.671, 0.298, 0.345, 1), # 8: (0.137, 0.576, 0.612, 1), # 9: (1.000, 1.000, 1.000, 1), # 10: (0.996, 0.929, 0.710, 1), # 11: (0.639, 0.494, 0.329, 1), # 12: (0.902, 0.517, 0.839, 1), # 13: (0.435, 0.674, 1.000, 1), # 14: (0.380, 0.490, 0.541, 1), # 15: (0.302, 0.722, 0.612, 1), # 16: (0.973, 0.773, 0.557, 1), # 17: (0.992, 0.553, 0.545, 1), # 18: (0.745, 0.635, 0.890, 1), # 19: (0.459, 0.400, 0.518, 1), # 20: (0.804, 0.855, 0.286, 1), # 21: (0.110, 0.674, 0.435, 1), # 22: (0.996, 0.792, 0.631, 1), # 23: (0.820, 0.290, 0.357, 1), # 24: (0.662, 0.423, 0.827, 1), # 25: (0.122, 0.737, 0.823, 1), # 26: (0.576, 0.973, 0.541, 1), # 27: (0, 0, 0, 1) 4: ('#F9FFBB'), 5: ('#E69E4E'), 6: ('#FF6FCE'), 7: ('#AB4C58'), 8: ('#23939C'), 9: ('#FFFFFF'), 10: ('#FEEDB5'), 11: ('#A37E54'), 12: ('#E684D6'), 13: ('#6FACFF'), 14: ('#617D8A'), 15: ('#4DB89C'), 16: ('#F8C58E'), 17: ('#FD8D8B'), 18: ('#BEA2E3'), 19: ('#756684'), 20: ('#CDDA49'), 21: ('#1CAC6F'), 22: ('#FECAA1'), 23: ('#D14A5B'), 24: ('#A96CD3'), 25: ('#1FBCD2'), 26: ('#92F88A') } return COLORS.get(color, (0.3, 0.3, 0.3, 1)) def _scroll_ucet_to_bottom(self, *_): # scroll_y = 0 → dole (v Kivy) self.scroll.scroll_y = 0 def _autoscroll_ucet(self, *_): # účet = content (GridLayout), scroll = viewport (ScrollView) if self.account.height <= self.scroll.height: # nic nepřetéká → drž nahoře self.scroll.scroll_y = 1 else: # přetéká → skoč dolů (poslední položky) self.scroll.scroll_y = 0 def is_fstmenu(self, item: dict) -> bool: return "fstmenu" in item.atributes def is_volnacena(self, item: dict) -> bool: return "volnacena" in item.atributes def is_vazena(self, item: dict) -> bool: return "vazena" in item.atributes def is_pohladavka(self, item: dict) -> bool: return "pohladavka" in item.atributes def _pol_is_pohladavka(self, pol) -> bool: cenpol = self.cenik_map.get(getattr(pol, "id_card", None)) return bool(cenpol and "pohladavka" in (getattr(cenpol, "atributes", []) or [])) def _validate_pohladavka_payment(self, ucet) -> bool: has_pohladavka = False has_regular = False for pol in (getattr(ucet, "poloz", []) or []): if self._pol_is_pohladavka(pol): has_pohladavka = True else: has_regular = True if has_pohladavka and has_regular: messagebox( self.tr("pos.pohladavka_musi_byt_sama","Pohľadávku nie je možné kombinovať s inými položkami na jednom účte."), title=self.tr("pos.uhrada_pohladavky","Úhrada pohľadávky"), ) return False ucet.pohladavka = 1 if has_pohladavka else None return True def _add_menu_item(self, state, item, zpravy=None): group_id = state.group_id parent = next( (p for p in self.ucet.poloz if p.group_id == group_id and p.typ_menu == 1), None ) cenik_item = self.cenik_map.get(item.c_karty) cena = self._resolve_price_for_pol(cenik_item, item.hladina) final_zpravy = [] # 🔥 správy z konkrétnej položky if zpravy: final_zpravy.extend(zpravy) guest_id = self.active_guest_id if guest_id == "ALL": guest_id = self.ucet.guests[0]["id"] course_id = getattr(self, "active_course_id", None) limit_rov_id, limit_hlad_id = self._limit_context_ids(course_id, guest_id) child = UcPolEdit( id_card=item.c_karty, c_druh=getattr(cenik_item, "c_druh", 0), druh=getattr(cenik_item, "druh", ""), prn_no=getattr(cenik_item, "prn_no", ""), nazev=cenik_item.d_name, cena=cena.cena, dph=cena.dan, mena=cena.mena, cenhlad=cena.name, pocet=item.hruba*parent.pocet, pol_pocet=item.hruba, sklad=getattr(cenik_item, "sklad", "00"), group_id=group_id, parent_id=parent.line_id, line_id=self._new_line_id(), typ_menu=2, zpravy=final_zpravy, selected=False, def_cena=cena.cena, def_dph=cena.dan, def_hlad=cena.name, edit_key=str(int(time() * 1000)), guest_id=guest_id, course_id=course_id, limit_rov_id=limit_rov_id, limit_hlad_id=limit_hlad_id ) self.ucet.poloz.append(child) def _finalize_add_menu_item(self, state, item, zpravy=None, qty=1, deli=1): zpravy = zpravy or [] group_id = self._new_group_id() parent_id = self._new_line_id() guest_id = self.active_guest_id if guest_id == "ALL": guest_id = self.ucet.guests[0]["id"] course_id = getattr(self, "active_course_id", None) limit_rov_id, limit_hlad_id = self._limit_context_ids(course_id, guest_id) # 🔵 HLAVIČKA MENU parent = UcPolEdit( id_card=state.menu_id, c_druh=getattr(self.cenik_map[state.menu_id], "c_druh", 0), druh=getattr(self.cenik_map[state.menu_id], "druh", ""), prn_no=getattr(self.cenik_map[state.menu_id], "prn_no", ""), nazev=self.cenik_map[state.menu_id].d_name, cena=0, dph="0", mena="EUR", pocet=qty, delitel=deli, sklad=getattr(self.cenik_map[state.menu_id], "sklad", "00"), group_id=group_id, parent_id=None, typ_menu=1, zpravy=zpravy.copy(), edit_key=str(int(time() * 1000)), guest_id=guest_id, course_id=course_id, limit_rov_id=limit_rov_id, limit_hlad_id=limit_hlad_id ) self.ucet.poloz.append(parent) # 🟢 CHILD cenpol = self.cenik_map[item.c_karty] child = UcPolEdit( id_card=item.c_karty, c_druh=getattr(cenpol, "c_druh", 0), druh=getattr(cenpol, "druh", ""), prn_no=getattr(cenpol, "prn_no", ""), nazev=cenpol.d_name, cena=cenpol.cena, dph=cenpol.dph, mena=cenpol.mena, pocet=qty*item.hruba, delitel=deli, sklad=getattr(cenpol, "sklad", "00"), group_id=group_id, parent_id=parent_id, typ_menu=2, zpravy=zpravy.copy(), pol_pocet=item.hruba, edit_key=str(int(time() * 1000) + 1), def_cena=cenpol.cena if cenpol.cena else 0, def_dph=cenpol.dan if cenpol.dan else "0", def_hlad=cenpol.name if cenpol.name else "", guest_id=guest_id, course_id=course_id, limit_rov_id=limit_rov_id, limit_hlad_id=limit_hlad_id ) self.ucet.poloz.append(child) def _menu_select_wrapper(self, callback, choice): # zavrie overlay if hasattr(self, "_menu_overlay"): self.remove_widget(self._menu_overlay) # 🔥 otvor FastMenu pre MENU ITEM cenpol = self.cenik_map.get(choice.c_karty) modal = POSDialog.FastItemMenu( self, cenpol, tr=self.tr, setup=self.setup, modal_manager = self.modal_manager, on_confirm=lambda qty, deli, zpravy: self._menu_item_confirm(callback, choice, qty, deli, zpravy) ) self.modal_manager.open(modal) def _menu_item_confirm(self, callback, choice, qty, deli, zpravy): self._finalize_add_menu_item( self.current_menu_state, choice, zpravy=zpravy, qty=qty, deli=deli ) callback(choice) # zobraz UI výberu správy def _open_menu_fast_edit(self, parent): items = [ i for i in self.ucet.poloz if i.group_id == parent.group_id ] modal = POSDialog.FastItemMenu( self, cenpol=self.cenik_map[parent.id_card], setup=self.setup, tr=self.tr, modal_manager = self.modal_manager, on_confirm=lambda qty, deli, zpravy: self._update_menu_group(items, qty, deli, zpravy) ) self.modal_manager.open(modal) def _update_menu_group(self, items, qty, deli, zpravy): for i in items: i.pocet = qty i.delitel = deli i.sel_delitel = deli if zpravy: i.zpravy = zpravy.copy() self.refresh_ucet() def _on_menu_back(self, state): if state.index == 0: return state.index -= 1 # odstráň posledný child z tejto group for i in reversed(range(len(self.ucet.poloz))): p = self.ucet.poloz[i] if p.group_id == state.group_id and p.parent_id: self.ucet.poloz.pop(i) break state.selected.pop() self.process_menu_step(state) def build_menu_state(self, menu) -> data.MenuState: groups = defaultdict(list) for p in menu.polozky: if p.c_karty in self.cenik_map: groups[p.skupina].append(p) # 🔥 zoradiť skupiny abecedne groups_order = sorted(groups.keys()) # 🔥 zoradiť položky v skupinách podľa poradie for g in groups: groups[g].sort(key=lambda x: x.poradie) return data.MenuState( menu_id=menu.c_karty, groups_order=groups_order, groups=dict(groups) ) def process_menu_step(self, state): if state.index >= len(state.groups_order): return self.finish_menu(state) group_name = state.groups_order[state.index] options = state.groups[group_name] # 🔥 auto výber if len(options) == 1: item = options[0] self._handle_menu_item_with_messages(state, item) state.index += 1 return self.process_menu_step(state) # UI výber self.show_menu_choice( state, group_name, options, on_select=lambda opt: self._handle_menu_item_with_messages(state, opt), on_back=lambda: self._on_menu_back(state) ) def _handle_menu_item_with_messages(self, state, item): cenpol = self.cenik_map.get(item.c_karty) if not cenpol: return self._request_messages_then( cenpol, callback=lambda zpravy: self._after_menu_item_messages(state, item, zpravy) ) def _after_menu_item_messages(self, state, item, zpravy): self._add_menu_item(state, item, zpravy) state.selected.append(item) state.index += 1 self.process_menu_step(state) def _on_menu_select(self, state: data.MenuState, choice): self._add_menu_item(state, choice) state.selected.append((state.groups_order[state.index], choice)) state.index += 1 self.process_menu_step(state) def _new_group_id(self): return uuid.uuid4().hex def _new_line_id(self): return uuid.uuid4().hex def _limit_context_ids(self, course_id=None, guest_id=None) -> tuple[int | None, int | None]: if not getattr(self, "limit_mode", False) or not self.ucet: return None, None course_id = course_id if course_id is not None else getattr(self, "active_course_id", None) guest_id = guest_id if guest_id is not None else getattr(self, "active_guest_id", None) if guest_id == "ALL": guest_id = self.ucet.guests[0]["id"] if getattr(self.ucet, "guests", None) else None course = next((c for c in (self.ucet.courses or []) if c.get("id") == course_id), {}) guest = next((g for g in (self.ucet.guests or []) if g.get("id") == guest_id), {}) id_rov = course.get("id_rov") c_hlad = guest.get("c_hlad") if id_rov is None and isinstance(course_id, str) and course_id.startswith("rov:"): try: id_rov = int(course_id.split(":", 1)[1]) except Exception: id_rov = None if c_hlad is None and isinstance(guest_id, str) and guest_id.startswith("hlad:"): try: c_hlad = int(guest_id.split(":", 1)[1]) except Exception: c_hlad = None return id_rov, c_hlad def _add_menu_header(self, cenpol, group_id, spravy=None, qty=1, delitel=1): cena = self._resolve_price_for_pol(cenpol, self.default_price_level) if not cena: return guest_id = self.active_guest_id if guest_id == "ALL": guest_id = self.ucet.guests[0]["id"] course_id = getattr(self, "active_course_id", None) limit_rov_id, limit_hlad_id = self._limit_context_ids(course_id, guest_id) line = UcPolEdit( id_card=cenpol.id_card, c_druh=getattr(cenpol, "c_druh", 0), druh=getattr(cenpol, "druh", ""), prn_no=getattr(cenpol, "prn_no", ""), nazev=cenpol.ch_name.strip(), cena=cena.cena, dph=cena.dan, mena=cena.mena, cenhlad=cena.name, pocet=qty, delitel=delitel, sklad=cenpol.sklad, edit_key=str(int(time() * 1000)), selected=False, kstornu=None, line_id=self._new_line_id(), group_id=group_id, parent_id=None, typ_menu=1, pol_pocet=1, def_cena=cena.cena, def_dph=cena.dan, def_hlad=cena.name, zpravy=spravy, guest_id=guest_id, course_id=course_id, limit_rov_id=limit_rov_id, limit_hlad_id=limit_hlad_id ) self.ucet.poloz.append(line) return line def show_menu_choice(self, state, group_name, options, on_select, on_back): from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.label import Label from kivy.uix.gridlayout import GridLayout from kivy.uix.floatlayout import FloatLayout from kivy.uix.scrollview import ScrollView from kivy.graphics import Color, Rectangle # 🔥 overlay (BEZ fade) overlay = FloatLayout() with overlay.canvas: Color(0, 0, 0, 0.6) self._overlay_rect = Rectangle(size=self.size, pos=self.pos) def update_rect(*args): self._overlay_rect.size = self.size self._overlay_rect.pos = self.pos self.bind(size=update_rect, pos=update_rect) # 📦 window window = BoxLayout( orientation="vertical", size_hint=(0.6, 0.75), pos_hint={"center_x": 0.5, "center_y": 0.5}, spacing=15, padding=15 ) # 🔝 title menu_item = self.cenik_map.get(state.menu_id) menu_name = menu_item.d_name.strip() if menu_item else "Menu" title = Label( text=f"{menu_name}\n[ {group_name} ] ({state.index+1}/{len(state.groups_order)})", size_hint=(1, 0.2) ) window.add_widget(title) # 🔘 GRID grid = GridLayout( cols=1, spacing=15, size_hint_y=None ) grid.bind(minimum_height=grid.setter('height')) valid_count = 0 for opt in options: cenik_item = self.cenik_map.get(opt.c_karty) if not cenik_item: continue valid_count += 1 btn = Button( text=cenik_item.d_name.strip(), size_hint_y=None, height=90, halign="left", valign="middle" ) # zalamovanie btn.bind( size=lambda inst, *_: setattr(inst, "text_size", (inst.width - 20, None)) ) # 🔥 OKAMŽITÝ CLICK (bez animácie) def on_click(btn_instance, o=opt): btn_instance.disabled = True self._menu_select_wrapper(on_select, o) btn.bind(on_release=on_click) grid.add_widget(btn) # ⚠️ prázdna skupina if valid_count == 0: state.index += 1 self.process_menu_step(state) return # 🔥 scroll scroll = ScrollView( size_hint=(1, 0.6), do_scroll_x=False ) scroll.add_widget(grid) window.add_widget(scroll) # 🔙 BACK (bez animácie) back_btn = Button( text=self.tr("button.back","Späť"), background_color=(0.6,0.2,0.2,1), background_normal="", size_hint=(1, 0.2), height=80 ) def on_back_click(*_): back_btn.disabled = True self._menu_back_wrapper(on_back) back_btn.bind(on_release=on_back_click) window.add_widget(back_btn) overlay.add_widget(window) self._menu_overlay = overlay self.add_widget(overlay) # 🔝 scroll hore (okamžite) scroll.scroll_y = 1 def finish_menu(self, state): self.current_menu_state = None self.refresh_ucet() Clock.schedule_once(self._autoscroll_ucet, 0) def _storno_menu_group(self, group_id): self.ucet.poloz = [ p for p in self.ucet.poloz if p.group_id != group_id] def _menu_select_wrapper(self, callback, choice): self._close_menu_overlay() callback(choice) def _menu_back_wrapper(self, callback): self._close_menu_overlay() callback() def _close_menu_overlay(self): if hasattr(self, "_menu_overlay") and self._menu_overlay: self.remove_widget(self._menu_overlay) self._menu_overlay = None def _finalize_menu_header(self, state: data.MenuState): # nájdi hlavičku for p in self.ucet.poloz: if ( p.group_id == state.group_id and p.parent_id is None ): # zatiaľ nič nemeníme return def _mark_cenpol(self, cenpol, zpravy=None, qty=1, delitel=1): guest_id = self.active_guest_id if guest_id == "ALL": guest_id = self.ucet.guests[0]["id"] course_id = getattr(self, "active_course_id", None) limit_rov_id, limit_hlad_id = self._limit_context_ids(course_id, guest_id) cena = self._resolve_price_for_pol(cenpol, self.default_price_level) if not cena: return line_id = self._new_line_id() group_id = line_id match delitel: case 1: cenan = cena.cena case 2: cenan = cena.cena2 case 3: cenan = cena.cena3 if cena.cena3 is not None else cena.cena case 4: cenan = cena.cena4 if cena.cena4 is not None else cena.cena self.ucet.poloz.append( UcPolEdit( id_card=cenpol.id_card, c_druh=getattr(cenpol, "c_druh", 0), druh=getattr(cenpol, "druh", ""), prn_no=getattr(cenpol, "prn_no", ""), nazev=cenpol.ch_name, cena=cenan, dph=cena.dan, mena=cena.mena, cenhlad=cena.name, pocet=qty, delitel=delitel, sel_delitel=delitel, sklad=cenpol.sklad, edit_key=str(int(time() * 1000)), selected=False, line_id=line_id, group_id=group_id, parent_id=None, typ_menu=0, pol_pocet=0, zpravy=zpravy or [], def_cena=cenan, def_dph=cena.dan, def_hlad=cena.name, guest_id=guest_id, course_id=course_id, limit_rov_id=limit_rov_id, limit_hlad_id=limit_hlad_id ) ) self.refresh_ucet() Clock.schedule_once(self._autoscroll_ucet, 0) def validate_groups(poloz): groups = {} for p in poloz: groups.setdefault(p.group_id, []).append(p) for g in groups.values(): if any(p.is_menu for p in g): # musí mať aspoň 1 child if len(g) < 2: raise Exception("Neplatné menu") def on_btn_storno(self, *_): if not self.ucet or not self.ucet.poloz: self._popup_info( self.tr("button.storno", "Storno"), self.tr("pos.prazdny_ucet","Účet je prázdny.")+"\n"+self.tr("pos.nie_je_co_stornovat","Nie je čostornovať") ) return if not any(p.selected for p in self.ucet.poloz): self._popup_info( self.tr("button.storno", "Storno"), self.tr("pos.nic_nevybrane_na_storno","Nie su vybrané žiadné položky na storno.") ) try: u_main, u_sec = self.on_storno_polozek() except Exception as e: Logger.warning(f"STORNO ERROR: {e}") return # jednotný návratový mechanismus self.dispatch("on_finish", u_main, u_sec, "storno") # zavření POSDialogu je řešeno controllerem def on_storno_polozek(self) -> tuple[UcetEdit, UcetEdit | None]: assert self.ucet is not None # --- kopie původního účtu --- u_main = self.ucet.model_copy(deep=True) u_sec = UcetEdit( **{ **self.ucet.model_dump(exclude={"poloz"}), "poloz": [] } ) new_poloz_main: list[UcPolEdit] = [] for p in u_main.poloz: if not p.selected: new_poloz_main.append(p) continue # kolik se stornuje sp = p.sel_pocet or 0 sd = p.sel_delitel or 1 storno_units = sp * sd orig_units = p.pocet * p.delitel if storno_units <= 0 or storno_units > orig_units: raise ValueError("Neplatné množství ke stornu") remain_units = orig_units - storno_units # --- zbytek do u_main --- if remain_units > 0: p_rem = p.model_copy(deep=True) p_rem.pocet = remain_units // p.delitel p_rem.delitel = p.delitel p_rem.selected = False p_rem.sel_pocet = None p_rem.sel_delitel = None new_poloz_main.append(p_rem) # --- storno do u_sec (jen pokud bylo odesláno) --- if p.sent: p_st = p.model_copy(deep=True) p_st.nazev = f"STORNO: {p.nazev}" p_st.pocet = -sp p_st.delitel = sd p_st.sent = False p_st.selected = False p_st.sel_pocet = None p_st.sel_delitel = None u_sec.poloz.append(p_st) u_main.poloz = new_poloz_main # pokud není co tisknout if not u_sec.poloz: u_sec = None return u_main, u_sec def units(p: UcPolEdit) -> int: return p.pocet * p.delitel # ------------------------------------------------- def set_ucet(self, ucet: UcetEdit): Logger.info(f"POS: set_ucet stul={ucet.stul}") self.ucet = ucet for p in self.ucet.poloz: if not p.delitel or p.delitel < 1: p.delitel = 1 if p.sel_delitel is None or p.sel_delitel < 1: p.sel_delitel = p.delitel if not hasattr(self, "active_course_id") or not self.active_course_id: if self.ucet.courses: self.active_course_id = self.ucet.courses[0]["id"] self._orig_checksum = data.ucet_edit_to_ucet(self.ucet).checksum() self.refresh_guests_bar() self.refresh_ucet() self._update_info_bar() Clock.schedule_once(self._scroll_ucet_to_top, 0) def _has_changes(self) -> bool: if not self.ucet or not self._orig_checksum: return False current = data.ucet_edit_to_ucet(self.ucet).checksum() return current != self._orig_checksum # ------------------------------------------------- def _left_qty_text(self, p: UcPolEdit) -> str: # levý: sel_pocet + (vždy stejný) sel_delitel if p.sel_pocet is None: return " " # netisknout den = p.sel_delitel if p.sel_delitel else 1 return f"{p.sel_pocet}/{den}" if den != 1 else str(p.sel_pocet) def _right_qty_text(self, p: UcPolEdit) -> str: # pravý: pocet + sel_delitel (globální delitel řádku) den = p.sel_delitel if p.sel_delitel else 1 if p.typ_menu==12: return f"{str(p.pocet)}" else: return f"{int(p.pocet)}/{den}" if den != 1 else str(int(p.pocet)) def _add_course_separator(self, course): is_active = course["id"] == self.active_course_id row = BoxLayout( orientation="horizontal", size_hint=(1, None), height=ACC_ROW_H, spacing=dp(4) ) btn_left = Button( text="☑" if self._is_course_selected(course['id']) else "☐", size_hint=(None, 1), background_normal="", background_color=(0.2,0.5,0.9,1) if is_active else (0.15,0.15,0.15,1), width=ACC_QTY_W ) btn_left.bind(on_press=lambda *_: self.toggle_course_selection(course['id'])) row.add_widget(btn_left) btn_center = Button( text=f"[b]{course['name']}[/b]", markup=True, size_hint=(1, None), height=ACC_ROW_H, background_normal="", background_color=(0.2,0.5,0.9,1) if is_active else (0.15,0.15,0.15,1), halign="left", valign="middle", ) btn_center._lp_trigger = None btn_center._long_press = False btn_center.course_id = course["id"] btn_center.bind( size=lambda inst, *_: setattr(inst, "text_size", (inst.width - dp(10), None)) ) btn_center.bind(on_touch_down=self._course_touch_down) btn_center.bind(on_touch_up=self._course_touch_up) row.add_widget(btn_center) btn_right = Button( text="🍽", size_hint=(None, 1), background_normal="", background_color=(0.2,0.5,0.9,1) if is_active else (0.15,0.15,0.15,1), width=ACC_QTY_W ) btn_right.bind(on_press=lambda *_: self.fire_course(course['id'])) row.add_widget(btn_right) row.bind( width=lambda inst, val: setattr(inst, "width", self.account.width) ) self.account.add_widget(row) def toggle_guest_selection(self, guest_id): if guest_id == 'ALL': return any_selected = any( p.selected for p in self.ucet.poloz if p.guest_id == guest_id ) for p in self.ucet.poloz: if p.guest_id == guest_id: if any_selected: p.selected = False p.sel_pocet = None else: p.selected = True p.sel_pocet = p.pocet self.refresh_ucet() self.update_action_panel() def _is_course_selected(self, course_id): return any( p.selected for p in self.ucet.poloz if p.course_id == course_id ) def _is_guest_selected(self, guest_id): return any( p.selected for p in self.ucet.poloz if p.guest_id == guest_id ) def fire_course(self, course_id): print(f"FIRE COURSE {course_id}") def toggle_course_selection(self, course_id): if getattr(self, "limit_mode", False) or self.active_guest_id == 'ALL': any_selected = any( p.selected for p in self.ucet.poloz if p.course_id == course_id ) for p in self.ucet.poloz: if p.course_id == course_id: if any_selected: p.selected = False p.sel_pocet = None else: p.selected = True p.sel_pocet = p.pocet else: any_selected = any( p.selected for p in self.ucet.poloz if p.course_id == course_id and p.guest_id == self.active_guest_id ) for p in self.ucet.poloz: if p.course_id == course_id and p.guest_id == self.active_guest_id: if any_selected: p.selected = False p.sel_pocet = None else: p.selected = True p.sel_pocet = p.pocet self.refresh_ucet() self.update_action_panel() def _course_touch_down(self, btn, touch): if not btn.collide_point(*touch.pos): return False touch.grab(btn) btn._long_press = False if self.limit_mode: btn._lp_trigger = None btn._touch_uid = touch.uid return True # 🔥 uložiť konkrétny touch id btn._touch_uid = touch.uid def trigger(dt, b=btn, uid=touch.uid): # 🔥 over že ide stále o ten istý touch if getattr(b, "_touch_uid", None) != uid: return b._long_press = True self.rename_course(b.course_id) btn._lp_trigger = Clock.schedule_once(trigger, LONG_TOUCH_TIME) return True def _course_touch_up(self, btn, touch): if touch.grab_current is not btn: return False touch.ungrab(btn) if btn._lp_trigger: btn._lp_trigger.cancel() # 🔥 extra safety: ignoruj staré touches if getattr(btn, "_touch_uid", None) != touch.uid: return True if btn._long_press: return True self.set_active_course(btn.course_id) return True def refresh_ucet(self): if self._refresh_event: self._refresh_event.cancel() self.account.clear_widgets() self._refresh_payment_button_text() #if not self.ucet or not self.ucet.poloz: #self.account.add_widget(Label(text="(Účet je prázdný)")) #self._update_info_bar() #return def build_rows(dt): active_guest = getattr(self, "active_guest_id", None) show_all = active_guest == "ALL" for course in self.ucet.courses: course_id = course["id"] # 1️⃣ separator vždy if self.mamechody: self._add_course_separator(course) # 2️⃣ položky pre tento chod if self.ucet.poloz: items = [ p for p in self.ucet.poloz if p.course_id == course_id and (show_all or p.guest_id==active_guest) ] for p in items: if p.typ_menu==2: continue else: self._add_row(p) self._refresh_event = Clock.schedule_once(build_rows, 0) self._update_info_bar() # ------------------------------------------------- def _normalize_row_after_right_edit(self, p: UcPolEdit): """ Po editaci pravého sloupce: - sel_delitel vždy existuje a je >=1 - sel_delitel je jediný "globální" delitel řádku - levý delitel se tím automaticky mění (protože je stejný atribut) - sel_pocet se NIKDY automaticky nemění (jen uživatelským výběrem vlevo / klikem na název) """ if p.sel_delitel is None or p.sel_delitel < 1: p.sel_delitel = 1 # optional: když někdo zvenku nastavil sel_pocet a je 0, tak ho smaž (0=netisknout) if p.sel_pocet == 0: p.sel_pocet = None p.selected = False def _display_name_for_pol(self, pol) -> str: text_map = getattr(self.controller, "cenik_texts", {}) or {} try: text = text_map.get(int(getattr(pol, "id_card", 0) or 0)) except Exception: text = None if text and getattr(text, "ch_name", ""): return text.ch_name return getattr(pol, "nazev", "") or "" def _add_row(self, p: UcPolEdit): is_menu = p.typ_menu == 1 swipe_threshold = dp(40) # ---------- SWIPE ---------- def touch_down(widget, touch): if not widget.collide_point(*touch.pos): return False widget._touch_start_x = touch.x widget._touch_start_y = touch.y widget._touch_swipe = False widget._long_triggered = False # long touch timer widget._long_ev = Clock.schedule_once( lambda dt: long_touch(widget), LONG_TOUCH_TIME ) return False def touch_move(widget, touch): if not hasattr(widget, "_touch_start_x"): return False dx = touch.x - widget._touch_start_x if abs(dx) > swipe_threshold: widget._touch_swipe = True return False def touch_up(widget, touch): if not hasattr(widget, "_touch_start_x"): return False # cancel long touch timer ev = getattr(widget, "_long_ev", None) if ev: ev.cancel() dx = touch.x - widget._touch_start_x # LONG TOUCH už proběhl if getattr(widget, "_long_triggered", False): return True # SWIPE if widget._touch_swipe: if p.sent: return True if dx > swipe_threshold: p.pocet += 1 elif dx < -swipe_threshold: if p.pocet > 1: p.pocet -= 1 # kontrola levého výběru if p.sel_pocet is not None and p.sel_pocet > p.pocet: p.sel_pocet = p.pocet if p.sel_pocet <= 0: p.sel_pocet = None p.selected = False self.refresh_ucet() return True # SHORT TAP toggle() return True # ---------- LONG TOUCH ---------- def long_touch(widget): widget._long_triggered = True if p.sent: return self._show_message_menu(p) # ---------- LEVÝ ---------- def edit_left(*_): initial = str(p.sel_pocet) if p.sel_pocet is not None else "1" group_id = p.group_id def accept(val: str): val = val.strip() if not val: if is_menu: for pol in self.ucet.poloz: if pol.group_id == group_id: pol.sel_pocet = None pol.selected = False else: p.sel_pocet = None p.selected = False self.refresh_ucet() self.update_action_panel() return try: num = int(val) except ValueError: return if num <= 0: if is_menu: for pol in self.ucet.poloz: if pol.group_id == group_id: pol.sel_pocet = None pol.selected = False else: p.sel_pocet = None p.selected = False else: if is_menu: for pol in self.ucet.poloz: if pol.group_id == group_id: max_qty = pol.pocet pol.sel_pocet = min(num*pol.pol_pocet, max_qty) pol.selected = True else: max_qty = p.pocet p.sel_pocet = min(num, max_qty) p.selected = True self.refresh_ucet() self.update_action_panel() if p.typ_menu == 10 or p.typ_menu == 11 or p.typ_menu == 12: if p.selected: p.selected=False p.sel_pocet=None else: p.selected=True p.sel_pocet=p.pocet self.refresh_ucet() self.update_action_panel() else: NumberPad( mode="number", allow_fraction=False, show_dot=False, decimal_places=0, max_len=3, initial_value=initial, on_accept=accept, ).open() # ---------- PRAVÝ ---------- def edit_right(*_): if p.sent: return if p.pocet != int(p.pocet): return if p.typ_menu == 10 or p.typ_menu == 11 or p.typ_menu == 12: return initial = self._right_qty_text(p) def accept(val: str): val = val.strip() try: if "/" in val: a, b = val.split("/", 1) new_pocet = int(a) new_den = int(b) else: new_pocet = int(val) new_den = p.sel_delitel if p.sel_delitel else 1 except ValueError: return if new_den < 1: new_den = 1 if new_pocet < 0: new_pocet = 0 if is_menu: group_id=p.group_id for pol in self.ucet.poloz: if pol.group_id == group_id: pol.pocet = new_pocet*pol.pol_pocet pol.delitel = new_den pol.sel_delitel = new_den pol.sel_pocet = None pol.selected = False else: p.pocet = new_pocet if p.delitel != new_den : p.delitel = new_den cenik_item = self.cenik_map.get(p.id_card) cena = self._resolve_price_for_pol(cenik_item, self.default_price_level) match new_den: case 1: cenan = cena.cena case 2: cenan = cena.cena2 case 3: cenan = cena.cena3 if cena.cena3 is not None else cena.cena case 4: cenan = cena.cena4 if cena.cena4 is not None else cena.cena p.cena = cenan p.def_cena = cenan p.sel_delitel = new_den p.sel_pocet = None p.selected = False self._normalize_row_after_right_edit(p) self.refresh_ucet() NumberPad( allow_fraction=True, show_dot=False, initial_value=initial, on_accept=accept, ).open() # ---------- TOGGLE ---------- def toggle(*_): group_id=p.group_id if is_menu: if p.sel_pocet is None: for pol in self.ucet.poloz: if pol.group_id == group_id: pol.sel_pocet = pol.pocet pol.selected = True else: for pol in self.ucet.poloz: if pol.group_id == group_id: pol.sel_pocet = None pol.selected = False else: if p.sel_pocet is None: p.sel_pocet = p.pocet p.selected = True else: p.sel_pocet = None p.selected = False self.refresh_ucet() self.update_action_panel() # ---------- BARVA ---------- if p.selected: bg = (0.2, 0.4, 0.8, 1) elif p.sent: bg = (0.18, 0.18, 0.18, 1) else: bg = (0.30, 0.30, 0.30, 1) # ---------- NÁZEV ---------- if is_menu: items = [ i for i in self.ucet.poloz if i.typ_menu == 2 and i.group_id == p.group_id ] if p.cena==0: text = self._display_name_for_pol(p) else: text = f"{self._display_name_for_pol(p)} {p.cena:.2f}" if p.zpravy: text += "\n[i]" + " | ".join(p.zpravy) + "[/i]" lines = [text] for i in items: if i.pol_pocet == 1: if i.cena == 0: text_item=f"{self._display_name_for_pol(i)}" else: text_item=f"{self._display_name_for_pol(i)} {i.cena:.2f}" else: text_item=f"{self._display_name_for_pol(i)} {i.pol_pocet:.2f} x {i.cena:.2f}" if i.zpravy: text_item += "\n[i]" + " | ".join(i.zpravy) + "[/i]" lines.append(text_item) else: text = self._display_name_for_pol(p) if p.zpravy: text += "\n[i]" + " | ".join(p.zpravy) + "[/i]" lines = [text] # if p.cenhlad != self.default_price_level: cenhlad = (p.cenhlad or "").strip() if cenhlad: cenhlad_text = self.price_level_map.get(cenhlad, cenhlad) lines.append(f"[size={int(dp(12))}][color=b8b8b8]{cenhlad_text}[/color][/size]") text = "\n".join(lines) if not self.account.width: text_width = dp(200) else: text_width = self.account.width - ACC_QTY_W*2 - ACC_PRICE_W - dp(16) row = None valign = "top" name_btn = POSButton( text=text, markup=True, padding=(0, dp(2)), size_hint=(1, None), height=ACC_ROW_H, background_color=bg, background_normal="", background_down="", halign="left", valign=valign ) name_btn.pol=p name_btn.bind(on_touch_down=touch_down) name_btn.bind(on_touch_move=touch_move) name_btn.bind(on_touch_up=touch_up) name_btn.text_size = (text_width, None) # 🔥 zalamovanie textu def update_text_size(inst, *_): inst.text_size = (inst.width - dp(8), None) name_btn.bind(size=update_text_size) def update_height(inst, size): if row is None: return new_h = size[1] + dp(10) row.height = max(ACC_ROW_H, new_h) name_btn.bind(texture_size=update_height) row = BoxLayout( orientation="horizontal", spacing=dp(4), padding=(0, dp(2)), size_hint=(1, None), height=ACC_ROW_H, ) def sync_children_height(*_): for child in row.children: child.height = row.height row.bind(height=sync_children_height) Clock.schedule_once(lambda *_: update_height(name_btn, name_btn.texture_size), 0) # ---------- LEVÝ ---------- row.add_widget(POSButton( text=self._left_qty_text(p), markup=True, padding=(0, dp(2)), size_hint=(None, None), width=ACC_QTY_W, height=row.height, background_color=bg, background_normal="", background_down="", halign="center", valign="top", on_press=edit_left, )) row.add_widget(name_btn) # ---------- PRAVÝ ---------- row.add_widget(POSButton( text=self._right_qty_text(p), markup=True, padding=(0, dp(2)), size_hint=(None, None), width=ACC_QTY_W, height=row.height, background_color=bg, background_normal="", background_down="", halign="center", valign="top", on_press=edit_right, )) # ---------- CENA ---------- if is_menu: total = sum( i.cena * i.pol_pocet for i in self.ucet.poloz if i.typ_menu == 2 and i.group_id == p.group_id ) price_text = f"{(total+p.cena):.2f}" else: price_text = f"{p.cena:.2f}" price_btn = POSButton( text=price_text, markup=True, padding=(0, dp(2)), size_hint=(None, None), width=ACC_PRICE_W, height=row.height, background_color=bg, background_normal="", background_down="", halign="right", valign="top", on_press=lambda *_: self._select_price_level(p), ) price_btn.bind( size=lambda inst, *_: setattr(inst, "text_size", (inst.width - dp(6), None)) ) row.add_widget(price_btn) if is_menu: items_count = sum( 1 for i in self.ucet.poloz if i.typ_menu == 2 and i.group_id == p.group_id ) self.account.add_widget(row) self.account.do_layout() def _clear_payment_selection(self): if not self.ucet: return for p in self.ucet.poloz: p.selected = False p.sel_pocet = None p.sel_delitel = None def _build_u_sec_for_payment(self, selected_all = False) -> tuple[UcetEdit | None, UcetEdit | None]: assert self.ucet is not None if getattr(self, "limit_mode", False): self._clear_payment_selection() self.refresh_ucet() self.update_action_panel() return None, self.ucet.model_copy(deep=True) u_main = UcetEdit(**self.ucet.model_dump(exclude={"poloz"}), poloz=[]) u_sec = UcetEdit(**self.ucet.model_dump(exclude={"poloz"}), poloz=[]) any_selected = False for p in self.ucet.poloz: # nic nevybráno → zatím nevíme if p.sel_pocet is None: u_main.poloz.append(p) continue any_selected = True qty_pay = p.sel_pocet qty_rem = p.pocet - qty_pay # zbytek do main if qty_rem > 0: pm = p.model_copy() pm.pocet = qty_rem pm.selected = False pm.sel_pocet = None u_main.poloz.append(pm) # část k zaplacení ps = p.model_copy() ps.pocet = qty_pay ps.selected = False ps.sel_pocet = None u_sec.poloz.append(ps) # NIC NEVYBRÁNO = PLATBA VŠEHO if not any_selected or selected_all: return None, self.ucet.model_copy(deep=True) # ČÁSTEČNÁ PLATBA if not u_sec.poloz: return None, None return u_main, u_sec def on_btn_payment(self, *_): #Logger.warning("PAYMENT BUTTON PRESSED") if not self.ucet or not self.ucet.poloz: messagebox(self.tr("pos.nie_je_co_platit", "Nie je čo platiť.")) return u_main, u_sec = self._build_u_sec_for_payment() #Logger.warning(f"u_sec = {u_sec}") if not u_sec: #Logger.warning("NOTHING TO PAY") return if not self._validate_pohladavka_payment(u_sec): return def done(u_sec_paid): # rozhodnutí typu operace op = "pay_full" if not u_main or not u_main.poloz else "pay_part" self.dispatch("on_finish", u_main, u_sec_paid, op) self._close_self() payment.PaymentDialog( ucet=u_sec, payment_types=self._available_payment_types(), discounts=self._payment_discounts(), discount_permissions=self._payment_discount_permissions(), discounts_all_allowed=self._payment_discounts_all_allowed(), printers=self.printers, bankterms=self.bankterms, default_printer=self.default_printer, on_printer_change=self._set_payment_printer, handler_runner=self._run_payment_handler, discount_runner=self._run_payment_discount, cenik_map=self.cenik_map, setup = self.setup, on_done = done, kasutxt=self.kasutxt, controller=self.controller, on_cancel=lambda *_: None, ).open() def on_btn_parcpay(self, *_): if getattr(self, "limit_mode", False): messagebox(self.tr("pos.limit_iba_cely","Limitový stôl sa dá zaplatiť iba celý naraz.")) return # --- guard if not self.ucet or not any( p.selected or p.sel_pocet for p in self.ucet.poloz ): messagebox(self.tr("pos.nevybrane_nic_na_platbu","Nie su vybrané žiadné položky na platbu.")) return # --- split účtu u_main, u_sec = self.split_ucet_for_payment(self.ucet) if not u_sec or not u_sec.poloz: messagebox(self.tr("pos.vybrane_polozky_sa_nedaju_platit", "Vybrané položky sa nedajú pripraviť na platbu.")) return if not self._validate_pohladavka_payment(u_sec): return # --- callback po dokončení platby def done(u_sec_paid): # parciální platba vždy op = "pay_part" self.dispatch("on_finish", u_main, u_sec_paid, op) self._close_self() # --- otevřeme STEJNÝ platební dialog payment.PaymentDialog( ucet=u_sec, payment_types=self._available_payment_types(), discounts=self._payment_discounts(), discount_permissions=self._payment_discount_permissions(), discounts_all_allowed=self._payment_discounts_all_allowed(), printers=self.printers, bankterms=self.bankterms, default_printer=self.default_printer, on_printer_change=self._set_payment_printer, handler_runner=self._run_payment_handler, discount_runner=self._run_payment_discount, cenik_map=self.cenik_map, setup=self.setup, on_done=done, kasutxt=self.kasutxt, controller=self.controller, on_cancel=lambda *_: None, ).open() def _resolve_price_for_pol(self, cenpol, hladina=None): #Vrací (Cena | None). #Řeší defaultní hladinu + fallbacky + hlášky. if not hladina: hladina=self.default_price_level if not cenpol.ceny: self._popup_info( self.tr("pos.chyba_cenika","Chyba cenníka"), self.tr("pos.polozkanema_ziadnu_cenu","Položka nemá žiadnu cenovú hladinu.\nNedá sa nablokovať.") ) return None # zkus defaultní hladinu for c in cenpol.ceny: if c.name == hladina: return c # default neexistuje → upozorni + fallback self._popup_info( self.tr("pos.price_level", "Cenová hladina"), self.tr( "pos.price_level_not_available", "Hladina {level} nie je pre túto položku dostupná.\nPoužije sa prvá dostupná cena." ).format(level=hladina) ) return cenpol.ceny[0] def _select_price_level(self, p: UcPolEdit): # ===== ROOT ===== root = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(6), ) # ===== SCROLL ===== scroll = ScrollView( size_hint=(1, 1), do_scroll_y=True, ) list_box = BoxLayout( orientation="vertical", size_hint_y=None, spacing=dp(6), ) list_box.bind(minimum_height=list_box.setter("height")) # ===== TLAČÍTKA ===== for cena in self.levels: is_active = (cena.ch == p.cenhlad) btn = Button( text=f"{cena.ch_name}", size_hint_y=None, height=dp(48), ) if is_active: btn.background_normal = "" btn.background_color = (0.2, 0.4, 0.8, 1) btn.bind(on_press=lambda _, c=cena.ch: self._apply_price_level(p, c, popup)) list_box.add_widget(btn) scroll.add_widget(list_box) root.add_widget(scroll) # ===== SPODNÍ BUTTON ===== btn_back = Button( text=self.tr("button.back", "Späť"), background_color=(0.6,0.2,0.2,1), background_normal="", size_hint_y=None, height=dp(48), ) btn_back.bind(on_press=lambda *_: popup.dismiss()) root.add_widget(btn_back) # ===== POPUP ===== popup = Popup( title=self.tr("pos.price_level","Cenová hladina"), content=root, size_hint=(None, None), size=(dp(360), dp(420)), auto_dismiss=False, ) popup.open() def _apply_price_level(self, p: UcPolEdit, cenhl, popup): self._change_price_level(p, cenhl) if popup: self._ignore_mark_until = time() + 0.3 popup.dismiss() self.refresh_ucet() def on_select_all(self, *_): if not self.ucet: return for p in self.ucet.poloz: # vybrat celý počet kusů p.sel_pocet = p.pocet p.selected = True self.refresh_ucet() self.update_action_panel() def on_clear_selection(self, *_): if not self.ucet: return for p in self.ucet.poloz: p.sel_pocet = None p.selected = False self.refresh_ucet() self.action_clear_selection() # ------------------------------------------------- def _mark(self, name: str): Logger.info(f"MARK {name}") for p in self.ucet.poloz: if p.nazev == name: p.pocet += 1 self.refresh_ucet() return from time import time self.ucet.poloz.append( UcPolEdit( nazev=name, cena=40.0, pocet=1, edit_key=str(int(time() * 1000)), ) ) self.refresh_ucet() def clone_menu(self, items, new_parent_qty): new_group_id = self._new_group_id() parent = next(i for i in items if i.typ_menu == 1) new_items = [] new_parent = parent.model_copy() new_parent.group_id = new_group_id new_parent.line_id = self._new_line_id() new_parent.pocet = new_parent_qty new_items.append(new_parent) for i in items: if i.typ_menu != 2: continue ni = i.model_copy() ni.group_id = new_group_id ni.parent_id = new_parent.line_id ni.line_id = self._new_line_id() # 🔥 KĽÚČOVÉ ni.pocet = new_parent_qty * (ni.pol_pocet or 1) new_items.append(ni) return new_items def menu_signature(self, parent, children): items = sorted( [ ( c.id_card, c.pol_pocet, c.cena, c.cenhlad, c.sklad, c.guest_id, c.course_id ) for c in children ] ) return ( parent.id_card, parent.cena, parent.cenhlad, parent.sklad, parent.guest_id, parent.course_id, tuple(items) ) def merge_menus(self, polozky): normal = [] menus = defaultdict(list) # rozdelenie for p in polozky: if p.typ_menu == 0 or p.typ_menu == 10 or p.typ_menu == 11 or p.typ_menu == 12: normal.append(p) else: menus[p.group_id].append(p) merged = {} result = [] for group_id, items in menus.items(): parent = next(i for i in items if i.typ_menu == 1) children = [i for i in items if i.typ_menu == 2] sig = self.menu_signature(parent, children) if sig not in merged: merged[sig] = { "parent": parent, "children": children } else: # 🔥 merge merged[sig]["parent"].pocet += parent.pocet for c in children: target = next( x for x in merged[sig]["children"] if x.id_card == c.id_card ) target.pocet += c.pocet # spätné zloženie for data in merged.values(): result.append(data["parent"]) result.extend(data["children"]) # pridaj normálne položky result.extend(normal) return result def finalize(self, operation: str): """ Rozloží aktuální účet na: - u_main_edit : UcetEdit - u_sec_edit : UcetEdit | None """ if not self.ucet: return None, None, "noop" u_main = UcetEdit(**self.ucet.model_dump(exclude={"poloz"}), poloz=[]) u_sec = UcetEdit(**self.ucet.model_dump(exclude={"poloz"}), poloz=[]) normal = [] menus = defaultdict(list) for p in self.ucet.poloz: if p.typ_menu == 0 or p.typ_menu == 10 or p.typ_menu == 11 or p.typ_menu == 12: normal.append(p) else: menus[p.group_id].append(p) for group_id, items in menus.items(): parent = next((i for i in items if i.typ_menu == 1), None) if not parent: continue # 👉 nič nevybrané → celé menu do main if parent.sel_pocet is None: u_main.poloz.extend(items) continue sec_qty = parent.sel_pocet main_qty = parent.pocet - sec_qty else: sec_qty = parent.sel_pocet main_qty = parent.pocet - sec_qty if not parent.sent: if main_qty > 0: u_main.poloz.extend(self.clone_menu(items, main_qty)) u_sec.poloz.extend(self.clone_menu(items, sec_qty)) else: if main_qty > 0: u_main.poloz.extend(self.clone_menu(items, main_qty)) st_items = self.clone_menu(items, sec_qty) for i in st_items: i.kstornu = sec_qty u_sec.poloz.extend(st_items) for p in normal: # ================================================= # 1️⃣ POLOŽKA NENÍ VYBRANÁ → CELÁ DO MAIN # ================================================= if p.sel_pocet is None: u_main.poloz.append(p.model_copy()) continue den = p.sel_delitel or p.delitel or 1 sec_qty = p.sel_pocet main_qty = p.pocet - sec_qty # ================================================= # 2️⃣ NEODESLANÁ DO KUCHYNĚ # ================================================= if not p.sent: if main_qty > 0: pm = p.model_copy() pm.pocet = main_qty pm.selected = False pm.sel_pocet = None u_main.poloz.append(pm) ps = p.model_copy() ps.pocet = sec_qty ps.selected = False ps.sel_pocet = None u_sec.poloz.append(ps) continue # ================================================= # 3️⃣ ODESLANÁ DO KUCHYNĚ → STORNO # ================================================= if main_qty > 0: pm = p.model_copy() pm.pocet = main_qty pm.selected = False pm.sel_pocet = None u_main.poloz.append(pm) st = p.model_copy() st.pocet = sec_qty st.kstornu = sec_qty # ⬅️ důležité – backend ví, že je to storno st.selected = False st.sel_pocet = None u_sec.poloz.append(st) if u_main.poloz: u_main.poloz = self.merge_menus(u_main.poloz) if u_sec.poloz: u_sec.poloz = self.merge_menus(u_sec.poloz) if not u_sec.poloz: return u_main, None, "edit_only" return u_main, u_sec, operation """ class DummyController: def has_perm(self, *_): return True class PosTestApp(App): def build(self): sm = ScreenManager() dummy_setup = type("Setup", (), {"platby": []})() pos = POSDialog( controller=DummyController(), default_price_level=cenik.cenpol[0].ceny[0].name, cenik=cenik, setup=dummy_setup, fstmenu=None name="pos" ) pos.set_ucet(tst_data.make_dummy_ucet()) sm.add_widget(pos) sm.current = "pos" return sm """