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