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

7314 lines
244 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
"""