7314 lines
244 KiB
Python
7314 lines
244 KiB
Python
# 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
|
||
"""
|