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

1687 lines
50 KiB
Python

# --- ZAKLADNI DATOVE STRUKTURY, data.py
# --- verze 1.0, 30.12.2025, PK
# --------------------------------------
from collections import defaultdict
import json, hashlib
from pydantic import BaseModel, Field, ConfigDict, model_validator,field_validator
from datetime import date, datetime
from typing import List, Optional, Dict, Any
from enum import Enum
import re
from dataclasses import dataclass, field
# -------------------------------
# --- zaznam v databazi uzivatelu
#--------------------------------
#class Perm:
# PLATBA = "PLATBA" #platba delena, se slevou
# PL_HOTOVE = "PL_HOTOVE" #platba vseho hotove
# SPLIT = "SPLIT" #rozdeleni uctu
# PL_SELECT = "PL_SELECT" #platba vybranych polozek
# DISCOUNT = "DISCOUNT" #sleva pri platbe
# STORNO_UCT = "STORNO_UCT" #storno uctu
# PL_CHG = "PL_CHG" #zmena druhu platby
# CENHLAD = "CENHLAD" #vyber cenove hladiny
# STORNO_PL = "STORNO_PL" #storno odeslanych polozek
# CLOSE = "CLOSE" #uzavreni pokladny (uzaverka)
@dataclass
class MenuState:
menu_id: int
groups_order: list[str]
groups: dict[str, list]
index: int = 0
selected: list = field(default_factory=list)
group_id: str | None = None # 🔥 kľúčové
parent_line_id: str | None = None # 🔥 kľúčové
#Milan 15.04.26 - doplnene jazykove mutacie popisov permitions
class Perm(Enum):
PLATBA = ("PLATBA", "Platba dělená, se slevou", "Platba delená, zo zľavou","","","")
PL_HOTOVE = ("PL_HOTOVE", "Platba vseho hotove", "Platba všetkého v hotovosti","","","")
SPLIT = ("SPLIT", "Rozdelení účtu", "Rozdelenie účtu","","","")
PL_SELECT = ("PL_SELECT", "Platba vybraných položek", "Platba vybraných položiek","","","")
DISCOUNT = ("DISCOUNT", "Sleva při platbě", "Zľava pri platbe","","","")
STORNO_UCT = ("STORNO_UCT", "Storno účtu", "Storno účtu","","","")
PL_CHG = ("PL_CHG", "Změna druhu platby", "Zmena druhu platby","","","")
CENHLAD = ("CENHLAD", "Výběr cenove hladiny", "Výber cenovej hladiny","","","")
STORNO_PL = ("STORNO_PL", "Storno odeslaných polozek", "Storno odoslaných položiek","","","")
CLOSE = ("CLOSE", "Uzavření pokladny", "Uzávierka pokladne","","","")
def __init__(self, code, descriptioncz, descriptionsk, descriptionit, descriptionen, descriptionpl):
self.code = code
self.descriptioncz = descriptioncz
self.descriptionsk = descriptionsk
self.descriptionit = descriptionit
self.descriptionen = descriptionen
self.descriptionpl = descriptionpl
class PermitOut(BaseModel):
code: str
text: str
def admheslo():
today = datetime.today()
day = today.day
month = today.month
day_of_month = (today.weekday()+2)%8
if day_of_month == 0:
day_of_month = 1
return (f"{day*month}.{day*day_of_month}{day_of_month*month}")
#Milan 15.04.26 - doplnena funkcia kvoli sk heslu
def reverse_two_digits(n: int) -> str:
s = str(n)
return s[::-1].ljust(2, "0")
#Milan 15.04.26 - funkcia na detekciu zadaneho sk hesla
def admskheslo():
today = datetime.today()
day = today.day
month = today.month
day_of_month = (today.weekday()+1)%8
if day_of_month == 0:
day_of_month = 1
return (f"{reverse_two_digits(day+month)}{reverse_two_digits(day+day_of_month)}{reverse_two_digits(day_of_month+month)}")
class PolozkyFstMenu(BaseModel):
c_karty: int
hruba: float
skupina: str
hladina: str
poradie: int
class PrnDefData(BaseModel):
prn_no_alt: str
ip: str
port: str
cupsname: str
p_reset: str
p_crlf: str
p_ff: str
p_boldon: str
p_boldoff: str
p_ulineon: str
p_ulineoff: str
p_wideon: str
p_wideoff: str
p_italon: str
p_italoff: str
p_draft: str
p_nlq: str
p_fullcut: str
p_partcut: str
p_ucetfeed: int
p_width: str
typ_diakr: str
cmd32_on: str
p_paragon: str
p_paragoff: str
typ_fiskal: str
is_cook: int
p_highon: str
p_highoff: str
convert_charset: str
printer_type: str
p_kopii: int
template_bon: str = ""
template_ucet: str = ""
class PrnDef(BaseModel):
prn_no: str
prn_name: str
id_term: str = ""
data: PrnDefData
@model_validator(mode="before")
@classmethod
def _migrate_id_term(cls, values):
if isinstance(values, dict) and not values.get("id_term"):
data_values = values.get("data")
if isinstance(data_values, dict):
values["id_term"] = data_values.get("id_term", "") or ""
return values
class PrnDefShort(BaseModel):
prn_no: str
prn_name: str
poradie: int
id_term: str = ""
cmd32_on: str = ""
class BankTermData(BaseModel):
typ: str
protokol: str
eft_ipadr: str
eft_lclpor: int
eft_rempor: int
eft_reqadr: str
eft_com: int
eft_baud: int
eft_stopb: int
eft_datb: int
eft_parity: int
sale_id: str = ""
terminal_id: str = ""
terminal_user: str = ""
terminal_password: str = ""
@model_validator(mode="before")
@classmethod
def _migrate_terminal_password(cls, values):
if isinstance(values, dict) and not values.get("terminal_password"):
values["terminal_password"] = values.get("terminal_pasword", "") or ""
return values
class BankTerm(BaseModel):
id_term: str
term_name: str
term_data: BankTermData
class PrintJobCreate(BaseModel):
id_kas: str
printer_no: str = ""
job_type: str = "other"
document_type: str = ""
receipt_no: str | None = None
required: bool = False
priority: int = 100
max_attempts: int = 3
payload: Dict[str, Any] = Field(default_factory=dict)
class PrintJob(BaseModel):
id: int
id_kas: str
printer_no: str = ""
agent_id: str | None = None
job_type: str = "other"
document_type: str = ""
receipt_no: str | None = None
required: bool = False
status: str = "queued"
priority: int = 100
attempts: int = 0
max_attempts: int = 3
payload: Dict[str, Any] = Field(default_factory=dict)
result: Dict[str, Any] = Field(default_factory=dict)
error: str = ""
created_at: str = ""
claimed_at: str | None = None
started_at: str | None = None
finished_at: str | None = None
updated_at: str = ""
class PrintJobClaimRequest(BaseModel):
id_kas: str = ""
agent_id: str
printers: list[str] = Field(default_factory=list)
limit: int = 10
class PrintJobStatusUpdate(BaseModel):
status: str
result: Dict[str, Any] = Field(default_factory=dict)
error: str = ""
class PrinterStatusIn(BaseModel):
id_kas: str
prn_no: str
agent_id: str = ""
online: bool = False
status: str = "unknown"
printer_type: str = ""
cmd32_on: str = ""
message: str = ""
queue_size: int = 0
failed_jobs: int = 0
details: Dict[str, Any] = Field(default_factory=dict)
class PrinterStatusOut(PrinterStatusIn):
prn_name: str = ""
id_term: str = ""
checked_at: str = ""
updated_at: str = ""
class PrintTemplateOut(BaseModel):
name: str
kind: str = "bon"
template_bon: str
template_ucet: str = ""
template_closure: str = ""
scope: str = "custom"
printer_no: str = ""
size: int = 0
modified_at: str = ""
class FstMenu(BaseModel):
id_kas: str
c_karty: int
polozky: list[PolozkyFstMenu]
class FstMenuKasa(BaseModel):
c_karty: int
polozky: list[PolozkyFstMenu]
class KasaUcpPrinters(BaseModel):
prn_no: str
poradie: int
class KasaUcp(BaseModel):
id_kas: str
printers: list[KasaUcpPrinters]
class KasUtxtRiadky(BaseModel):
userhead1: str | None = None
userhead2: str | None = None
userhead3: str | None = None
userhead4: str | None = None
userhead5: str | None = None
userhead6: str | None = None
userhead7: str | None = None
userhead8: str | None = None
userhead9: str | None = None
usertail1: str | None = None
usertail2: str | None = None
usertail3: str | None = None
usertail4: str | None = None
usertail5: str | None = None
usertail6: str | None = None
class KasUtxt(BaseModel):
id_kas: str
riadky: KasUtxtRiadky
class HladinyRiadky(BaseModel):
ch: str
ch_name: str
class Hladiny(BaseModel):
id_kas: str
riadky: list[HladinyRiadky]
#Milan 15.04.26
class UserPermission(BaseModel):
id_kas: str
permits: list[str]
#Milan 15.04.26
class PaymentPermission(BaseModel):
id_kas: str
payments: list[str]
#Milan 15.04.26
class DiscountPermission(BaseModel):
id_kas: str
discounts: list[str]
#Milan 15.04.26
class LevelPermission(BaseModel):
id_kas: str
levels: list[str]
#Milan 15.04.26 - doplnene dalsie polia
class UserIn(BaseModel):
heslo: str
heslo_karta: str
name: str
user_id: str
jazyk: str = "sk"
is_admin: bool
permits: list[UserPermission]
payments: list[PaymentPermission]
discounts: list[DiscountPermission]
levels: list[LevelPermission]
@field_validator("jazyk", mode="before")
@classmethod
def _normalize_lang(cls, v):
text = str(v or "sk").strip().lower()
return "cs" if text == "cz" else (text or "sk")
#server heslo nevraci
class UserOut(BaseModel):
id: int
name: str
user_id: str
jazyk: str = "sk"
is_admin: bool
permits: list[UserPermission] = Field(default_factory=list)
payments: list[PaymentPermission] = Field(default_factory=list)
discounts: list[DiscountPermission] = Field(default_factory=list)
levels: list[LevelPermission] = Field(default_factory=list)
#Milan 15.04.26 - doplnene id kasy, ku ktorej sa hlasim
class UserLoginIn(BaseModel):
heslo: str
kas: str
def extract_for_kas(items, kas: str, key: str) -> list[str]:
return [
value
for item in items
if item.id_kas == kas
for value in getattr(item, key)
]
# --- druhy plateb v setupu pokladny
class PaymentType(BaseModel):
code: str # "CASH", "CARD", "SODEXO", "QR"
name: str # "Hotově", "Kartou", "QR platba"
unit: str # "Kč", "€"
rate: float = 1.0 # smenny kurz pro Kč
poradie: int = 0
p_kopii: int = 1
round50: int = 0
allow_partial: bool = True
is_cash: bool = False
handler: str | None = None # např. "#charge_kredit_c.py"
color: str | None = None
is_default: bool = False
fiscal: bool = True
is_bankterm: bool = False
odvod: int = 0
odovzdat: str = ""
#Milan 15.04.26 - doplnene payments a discounts
class UserLoginOut(BaseModel):
id: int
name: str
user_id: str
jazyk: str = "sk"
is_admin: bool
permits: list[str]
payments: list[PaymentType]
discounts: list[str]
levels: list[str]
class LoginResponse(BaseModel):
access_token: str
refresh_token: str
token_type: str = "Bearer"
user: UserOut
# -----------------------------------
# --- setup pokladny
SETUP_VERSION = 2
def normalize_setup_parameter_value(value: Any, var_type: str | None = None) -> Any:
typ = (var_type or "").strip().upper()
def clean_string(raw: Any) -> str:
s = "" if raw is None else str(raw).strip()
while len(s) >= 2 and s[0] == s[-1] and s[0] in ("'", '"'):
if s[0] == '"':
try:
decoded = json.loads(s)
if isinstance(decoded, str):
s = decoded.strip()
continue
except Exception:
pass
s = s[1:-1].strip()
return s
if typ == "L":
if isinstance(value, bool):
return value
s = clean_string(value).lower()
if s in (".t.", "t", "true", "1", "yes", "ano"):
return True
if s in (".f.", "f", "false", "0", "no", "nie", ""):
return False
return bool(value)
if typ == "N":
if isinstance(value, bool):
return int(value)
if isinstance(value, int) or isinstance(value, float):
return value
s = clean_string(value).replace(",", ".")
if not s:
return 0
try:
number = float(s)
except ValueError:
return 0
return int(number) if number.is_integer() else number
if typ == "C":
return clean_string(value)
return value
class SetupParameterValue(BaseModel):
var_name: str
var_value: Any = None
var_type: str = "C"
@model_validator(mode="before")
@classmethod
def accept_legacy_names(cls, value):
if not isinstance(value, dict):
return value
data = dict(value)
if "var_name" not in data and "key" in data:
data["var_name"] = data["key"]
if "var_value" not in data:
if "parsed_value" in data:
data["var_value"] = data["parsed_value"]
elif "value" in data:
data["var_value"] = data["value"]
if "var_type" not in data and "var_typ" in data:
data["var_type"] = data["var_typ"]
return data
@model_validator(mode="after")
def normalize_value(self):
self.var_type = (self.var_type or "C").strip().upper()
self.var_value = normalize_setup_parameter_value(self.var_value, self.var_type)
return self
class PosSetup(BaseModel):
model_config = ConfigDict(extra="allow")
config_version: int = SETUP_VERSION
# --- identita ---
pos_name: str = "Alto Praha"
id_kas: str = "K01"
pokladna: str = "Kasa test K01"
iban: str = "CZ0855000000000168640001"
# --- UI ---
max_rows: int = 8
max_cols: int = 6
allow_fractions: List[int] = [2, 3]
# --- chování ---
allow_split: bool = True
allow_partial_payment: bool = True
allow_storno: bool = True
allow_price_edit: bool = False
sum_items: bool = True
# --- účetnictví ---
mena: str = "Kc"
rounding: int = 2
default_price_level: str = "1"
is_sleva: bool = False
loyalty_system: str | bool = ""
# --- druhy plateb ---
prvni_platba_idx: int = 0
druha_platba_idx: int = 0
platba1: str = "CASH"
platba2: str = ""
platba3: str = ""
platby: List["PaymentType"] = Field(default_factory=list)
# --- technické ---
printer_type: str = "tcp"
printer_host: str = "192.168.1.55"
printer_port: int = 9100
offline_allowed: bool = False
api_version: str = "1.0"
# --- data ---
messages: List[str] = Field(default_factory=list)
# AUTOMATICKÁ MIGRACE MODELU
@model_validator(mode="before")
@classmethod
def upgrade_model(cls, data):
if not isinstance(data, dict):
return data
version = data.get("config_version", 1)
if version < 2:
data.setdefault("messages", ["Děkujeme za návštěvu"])
version = 2
data["config_version"] = version
return data
# incrementace ucisla s ochranou pred pretecenim
def next_ucislo(ucislo: str) -> str:
if not ucislo or len(ucislo) < 3:
return ucislo
prefix = ucislo[:2] # číslo pokladny
seq_part = ucislo[2:] # číselná část
width = len(seq_part)
try:
next_seq = str(int(seq_part) + 1).zfill(width)
except ValueError:
return ucislo # fallback, pokud není číselné
if len(next_seq)>width:
next_seq = str(int(next_seq[1:])+1).zfill(width)
return prefix + next_seq
# --- vraci datetime, pozdeji prejmenuj na stime_str()
def now_clk_str() -> str:
return datetime.now().strftime("%H:%M:%S")
def stime_str() -> str:
return datetime.now().strftime("%Y%m%d %H:%M:%S")[-15:]
# --- kodovani a dekodovani (pouzito na heslo)
class Krypt():
def code(self, s_in):
s_out=chr((ord(s_in[0])+len(s_in))&0x7F)
for i in range(1,len(s_in)):s_out+=chr((ord(s_in[i])+ord(s_in[i-1]))&0x7F)
return s_out
def decode(self, s_in):
s_out=chr((ord(s_in[0])-len(s_in))&0x7F)
for i in range(1,len(s_in)):s_out+=chr((ord(s_in[i])-ord(s_out[i-1]))&0x7F)
return s_out
# ----------------------------------------------------
# --- AUTENTIFIKACE, TOKENY
# ----------------------------------------------------
class AuthContext(BaseModel):
prefix: str #prefix do tabulek
username: str #jmeno identifikujici zakazku
client_id: str #cislo terminalu (2 znaky)
# --- odaje pro prihlaseni
class LoginData(BaseModel):
username: str
password: str
id_kas: str
# --- kontrola prefixu, po kodovani smi obsahovat jen tyto znaky
def safe_prefix(prefix: str) -> str:
if not re.match(r"^[A-Za-z0-9_]+$", prefix):
raise ValueError("Invalid table prefix")
return prefix
# --- token struktura
class RefreshRequest(BaseModel):
refresh_token: str
# L.L. (22.06.2026) datetime poslednej zmeny dát z foodu - cenník, setup a mapa stolov
class FoodManDataChange(BaseModel):
zmena: datetime
# ---------------------------------------
# --- Mapa stolu
# ---------------------------------------
class Table(BaseModel):
id:str #prevazne cislo stolu v otevrenych stolech
# (muze to byt libovolny string)
#je-li na vice pokladnach napr. 01 a 07 pak
#default id 0107|xxx kde xxx je ident
name:str #nazev pro ucely mapy stolu
pos_x:int
pos_y:int
width:int
height:int
radius:float #0 hranate, 1 kulate (x,y,100,100,1) je kulaty stul
class Room(BaseModel):
room_name:str
stoly:list[Table]
class MapaStolu(BaseModel):
rooms:list[Room]
pokladny:list[str] #seznam pokladen, kde je mistnost pristupna
class LimitTable(BaseModel):
table_id: str = ""
id_limit: int = 0
id_den: int = 0
menolimit: str = ""
datum: str = ""
txt_kasy: str = ""
name: str = ""
row_count: int = 0
class LimitLockResult(BaseModel):
ok: bool = False
table_id: str = ""
id_limit: int = 0
id_den: int = 0
message: str = ""
class FoodDat(BaseModel):
id: str = ""
c_stredisk: int = 0
id_zkratka: str = ""
pgm: str = ""
@field_validator("id", "id_zkratka", "pgm", mode="before")
@classmethod
def _strip_text(cls, v):
return "" if v is None else str(v).strip()
# ---------------------------------------
# --- MODELY CENIKU
# --- pozice buttonu na objednavce ------
class Position(BaseModel):
page:int #stranka
line:int #radka
col:int #sloupec
#Milan
color:int=0
sirka:int=1
# --- cenova hladina
#Milan 11.03.26
class Cena(BaseModel):
cena:float #cena
mena:str #oznaceni meny
dan:str #DPH
name:str #oznaceni ceny (standard, personalni)
cena2:float #cena polovicna
cena3:float | None = None #cena tretinova
cena4:float | None = None #cena stvrtinova
# --- eany
class Ean(BaseModel):
ean: str
koeficient: float = 1
# --- zpravy
class MessagePol(BaseModel):
text: str
class ZlavaDruh(BaseModel):
c_druh: int = 0
id_zlavy_hlav: int = 0
class ZlavaKalka(BaseModel):
c_karty: int = 0
id_zlavy_hlav: int = 0
class Zlava(BaseModel):
meno: str = ""
den1: int = 0
den2: int = 0
den3: int = 0
den4: int = 0
den5: int = 0
den6: int = 0
den7: int = 0
mes01: int = 0
mes02: int = 0
mes03: int = 0
mes04: int = 0
mes05: int = 0
mes06: int = 0
mes07: int = 0
mes08: int = 0
mes09: int = 0
mes10: int = 0
mes11: int = 0
mes12: int = 0
casod: str = ""
casdo: str = ""
datumod: date | None = None
datumdo: date | None = None
typ_zlavy: int = 1
zl_koef: float = 0.0
zl_zadanie: int = 1
vs_druhy: int = 1
napolozku: int = 0
naucet: int = 0
ajpocenovejhladine: int = 0
ibajedna: int = 0
ajpozlavenapolozku: int = 0
idriadok: int
prg_dotaz: str = ""
prg_natiz: str = ""
premenna: str = ""
druhy: list[ZlavaDruh] = Field(default_factory=list)
kalky: list[ZlavaKalka] = Field(default_factory=list)
@field_validator(
"meno", "casod", "casdo", "prg_dotaz", "prg_natiz", "premenna",
mode="before",
)
@classmethod
def _strip_pg_char(cls, v):
if v is None:
return ""
return str(v).strip()
@field_validator("datumod", "datumdo", mode="before")
@classmethod
def _empty_date_to_none(cls, v):
if v == "":
return None
return v
@field_validator("druhy", "kalky", mode="before")
@classmethod
def _null_to_list(cls, v):
return [] if v is None else v
class Zlavy(BaseModel):
id_kas: str
zlavy: list[Zlava] = Field(default_factory=list)
class UverFirma(BaseModel):
id: int | None = None
hjmeno: str = ""
adresa1: str = ""
adresa2: str = ""
adresa3: str = ""
ico: str = ""
icdph: str = ""
dic: str = ""
@field_validator("hjmeno", "adresa1", "adresa2", "adresa3", "ico", "icdph", "dic", mode="before")
@classmethod
def _strip_text(cls, v):
return "" if v is None else str(v).strip()
class UverZaznam(UverFirma):
akcia: str = ""
hjmeno: str = ""
adresa1: str = ""
adresa2: str = ""
adresa3: str = ""
ico: str = ""
icdph: str = ""
dic: str = ""
schvalil: str = ""
@field_validator("akcia", "schvalil", mode="before")
@classmethod
def _strip_record_text(cls, v):
return "" if v is None else str(v).strip()
class Recepcia(BaseModel):
id: int
hotel: str
hor_ip: str
hor_port: int
hor_meno: str
hor_heslo: str
api_meno: str
api_heslo: str
typ_hotel: int
hor_prefix: str
class HotelReception(BaseModel):
id: int
hotel: str = ""
typ_hotel: int = 0
hor_prefix: str = ""
supports_rooms: bool = False
supports_groups: bool = False
supports_card: bool = True
class HotelRoom(BaseModel):
type: str = "room"
id: str = ""
account_id: str = ""
room_code: str = ""
room_name: str = ""
guest_name: str = ""
checkin_date: str = ""
checkout_date: str = ""
building: str = ""
can_charge: bool = True
message: str = ""
extra: dict[str, Any] = Field(default_factory=dict)
class HotelRoomsResponse(BaseModel):
reception: HotelReception
manual_room: bool = False
rooms: list[HotelRoom] = Field(default_factory=list)
message: str = ""
class HotelGuest(BaseModel):
id: str = ""
guest_name: str = ""
room_id: str = ""
room_code: str = ""
account_id: str = ""
checkin_date: str = ""
checkout_date: str = ""
building: str = ""
result: int = 0
message: str = ""
class HotelCardRequest(BaseModel):
reception_id: int
id_kas: str
card_code: str
class HotelCardResult(BaseModel):
room_id: str = ""
room_code: str = ""
account_id: str = ""
guest_id: str = ""
guest_name: str = ""
class HotelChargeTarget(BaseModel):
reception_id: int
reception_name: str = ""
typ_hotel: int = 0
target_type: str = "guest"
room_id: str = ""
room_code: str = ""
building: str = ""
time_attribute: str = ""
account_id: str = ""
guest_id: str = ""
guest_name: str = ""
group_id: str = ""
group_name: str = ""
class HotelChargeItem(BaseModel):
line_id: str = ""
id_card: int = 0
name: str = ""
c_druh: int = 0
price_level: str = ""
dph: str = ""
quantity: float = 0.0
unit_price: float = 0.0
amount: float = 0.0
class HotelChargeLine(BaseModel):
raster_id: str = ""
tax_code: str = ""
c_druh: int = 0
price_level: str = ""
dph: str = ""
description: str = ""
quantity: float = 0.0
unit_price: float = 0.0
amount: float = 0.0
items: list[HotelChargeItem] = Field(default_factory=list)
class HotelChargePreparation(BaseModel):
ready: bool = False
id_kas: str = ""
typ_hotel: int = 0
reception_id: int = 0
reception_name: str = ""
raster_table: str = ""
receipt_number: str = ""
currency: str = ""
target: HotelChargeTarget | None = None
lines: list[HotelChargeLine] = Field(default_factory=list)
total: float = 0.0
warnings: list[str] = Field(default_factory=list)
errors: list[str] = Field(default_factory=list)
class HotelChargeSendResult(BaseModel):
ok: bool = False
message: str = ""
request_number: int | None = None
preparation: HotelChargePreparation | None = None
class PostgresConnection(BaseModel):
enabled: bool = False
host: str = ""
port: int = 5432
database: str = ""
user: str = ""
password: str = ""
schema_: str = Field(default="food600", alias="schema")
sslmode: str = "prefer"
connect_timeout: int = 5
model_config = ConfigDict(populate_by_name=True)
@field_validator("host", "database", "user", "password", "schema_", "sslmode", mode="before")
@classmethod
def _strip_pg_text(cls, v):
return "" if v is None else str(v).strip()
@field_validator("port", "connect_timeout", mode="before")
@classmethod
def _positive_int(cls, v):
try:
return max(0, int(v))
except Exception:
return 0
class PostgresConnectionOut(PostgresConnection):
password: str = ""
password_set: bool = False
class PostgresStatus(BaseModel):
available: bool = False
installation_enabled: bool = False
cashier_enabled: bool = False
connection_configured: bool = False
connection_ok: bool = False
message: str = ""
class HotRastre(BaseModel):
id: int
id_kas: str
id_hotel: int
c_druh: int = 0
raster1: int
raster2: int
raster3: int
raster4: int
raster5: int
raster6: int
raster7: int
raster8: int
raster9: int
dph1: float
dph2: float
dph3: float
dph4: float
dph5: float
dph6: float
dph7: float
dph8: float
dph9: float
tmatr: str
budova: str
class MewsRastre(BaseModel):
id: int
id_kas: str
id_hotel: int
c_druh: int = 0
raster1: str
raster2: str
raster3: str
raster4: str
raster5: str
raster6: str
raster7: str
raster8: str
raster9: str
dph1: float
dph2: float
dph3: float
dph4: float
dph5: float
dph6: float
dph7: float
dph8: float
dph9: float
tmatr: str
budova: str
class FidRastre(BaseModel):
id: int
id_kas: str
id_hotel: int
c_druh: int = 0
raster: str
raster1: str
raster2: str
raster3: str
raster4: str
raster5: str
raster6: str
raster7: str
raster8: str
raster9: str
dph1: float
dph2: float
dph3: float
dph4: float
dph5: float
dph6: float
dph7: float
dph8: float
dph9: float
tmatr: str
budova: str
class HotPlatby(BaseModel):
id: int
id_hotel: int
druh_pl: str
hot_platba_id: int
hot_karta_id: int
hot_platba: str
hot_karta: str
po_uctoch: int
payment: str
id_meny: int
class MewsDph(BaseModel):
id: int
id_hotel: int
mews_taxrate: str
koefdph: float
class Uvery(BaseModel):
hjmeno: str
adresa1: str
adresa2: str
adresa3: str
ico: str
dic: str
icdph: str
class ClientSettings(BaseModel):
prn_no: str
room_name: str
# --- polozka ceniku
class CenPolCreate(BaseModel):
id_card:int #identifikator polozky jako z foodu
kod:int | None = None #kod pro rucni markovani
c_druh:int = 0 #druh karty pre hotelove rastre
druh:str = "" #nazov druhu pre reporty
spart:str = "" #skupina druhov pre reporty
prn_no:str = "" #tlaciaren bonu
d_name:str #jmeno pro display
ch_name:str #txt na ucet, jednotka za '|' nebo posledni 3 znaky
color:int=0 #barva buttonu
pokl:str #oznaceni pokladny (asi 2 znaky)
sklad:str #oznaceni skladu (asi 2 znaky)
ceny:list[Cena] #seznam cen (nemusi byt steny pro vsechny polozky)
pos_pc: list[Position] = Field(default_factory=list)
pos_mb: list[Position] = Field(default_factory=list)
eany: list[Ean] = Field(default_factory=list)
messagepol: list[MessagePol] = Field(default_factory=list)
# pritomnost atributu znamena atribut == True
atributes: list[str] = Field(default_factory=list)
@property
def ean_set(self):
return {e.ean: e.koeficient for e in self.eany}
@property
def positions(self):
return self.pos_pc
# ---------- NORMALIZACE NULL ----------
@field_validator("pos_pc", "pos_mb", "eany", "messagepol",
"atributes", mode="before")
@classmethod
def _null_to_list(cls, v):
if v is None:
return []
return v
# ---------- AUTOMATICKÁ MIGRACE ----------
@model_validator(mode="before")
@classmethod
def _migrate_old_model(cls, data):
if not isinstance(data, dict):
return data
if not data.get("kod"):
data["kod"] = data.get("id_card")
data.setdefault("c_druh", 0)
data.setdefault("druh", "")
if "spart" not in data and "Spart" in data:
data["spart"] = data.get("Spart")
data.setdefault("spart", "")
data.setdefault("prn_no", "")
if isinstance(data.get("pos_pc"), dict):
data["pos_pc"] = [data["pos_pc"]]
if isinstance(data.get("pos_mb"), dict):
data["pos_mb"] = [data["pos_mb"]]
data.setdefault("pos_pc", [])
data.setdefault("pos_mb", [])
data.setdefault("eany", [])
data.setdefault("messagepol", [])
# ✔ FIX
for p in data.get("pos_pc", []):
if isinstance(p, dict):
p.setdefault("sirka", 1)
for p in data.get("pos_mb", []):
if isinstance(p, dict):
p.setdefault("sirka", 1)
return data
# --- polozka ceniku pro DB
class CenPol(CenPolCreate):
id:int | None = None #eventualne vytvori DB
class CenikText(BaseModel):
id_card: int
jazyk: str = "sk"
d_name: str = ""
ch_name: str = ""
dat_cas_zm: str = ""
@field_validator("jazyk", mode="before")
@classmethod
def _normalize_lang(cls, v):
text = str(v or "sk").strip().lower()
return "cs" if text == "cz" else (text or "sk")
class CenikTexty(BaseModel):
texty: list[CenikText] = Field(default_factory=list)
# ---cenik, pri behu klienta je v pameti
class Cenik(BaseModel):
cenpol:list[CenPol]
def get_price(self, pol_id: int, price_name: str) -> Cena | None:
# najdi položku
pol = next((p for p in self.cenpol if p.id == pol_id), None)
if not pol:
return None
# najdi cenu podle názvu hladiny
return next(
(c for c in pol.ceny if c.name == price_name),
None )
def djson(self):
return self.model_dump(mode='json')
def prn(self):
print(f"Cenik\n{self.model_dump(mode='json')}")
# --- vrati pouzite nazvy hladin, serazene bez duplicit
def used_price_levels(self) -> list[str]:
return sorted({
cena.name
for pol in self.cenpol
for cena in pol.ceny })
# -----------------------------------------------------
# --- MODELY UCTU
# --- cislo uctu --------------------------------------
class UCislo(BaseModel):
ucis: str
def djson(self):
return self.model_dump(mode='json')
# --- polozka uctu
class UcPol(BaseModel):
id_card:int #identifikator karty (FOOD id_card)
c_druh:int = 0 #druh karty pre hotelove rastre
druh:str = "" #nazov druhu pre reporty
spart:str = "" #skupina druhov pre reporty
prn_no:str = "" #tlaciaren bonu z cennika
nazev:str #nazev polozky
cena:float #cena polozky
cena_puv:float| None=None #puvodni cena pred slevou
dph:str #sazba DPH
mena:str = 'Kc' #oznaceni meny
cenhlad:str = '0' #oznaceni cenove hladiny
pocet:float #pocet kusu
delitel:int = 1 #zlomky porci dle setupu ()1/2, 1/3)
sklad:str|None=None #sklad pro odtezovani
kstornu:int | None = None #kusu ke stornu polozky, None vse k stornu
line_id:str
group_id:str
parent_id:str | None = None
typ_menu:int = 0
pol_pocet:float
def_cena:float
def_dph:str
def_hlad:str = '0'
guest_id: str | None = None
course_id: str | None = None
limit_item_id: int | None = None
limit_rov_id: int | None = None
limit_hlad_id: int | None = None
limit_fmenu_id: str = ""
zpravy: list[str] = Field(default_factory=list) #zprava do kuchyne
def model_post_init(self, __context):
if self.cena_puv is None:
self.cena_puv = self.cena
if self.sklad is None:
self.sklad = '00'
def djson(self):
return self.model_dump(mode='json')
def sleva(self) -> float:
return (self.cena_puv or self.cena) - self.cena
# --- ucet pro editaci v UI, neuklada se do DB
class UcPolEdit(UcPol):
#bon_printer: str
#bind_printer_cmd: str
#text_message: str
edit_key: str #cas editace polozky v ms
selected: bool = False #sluzebni udaj pro PosDialog
sel_pocet: int | None = None #pocet vybranych kusu/ pouziva Pyside6
sel_delitel: int | None = None #delitel vybranych kusu/ pouziva Pyside6
sent:bool = False #odeslan do kuchyne
#zpravy: list[str] = Field(default_factory=list) #zprava do kuchyne
# --- konverze polozky uctu editacniho na DB tvar
def ucpol_edit_to_ucpol(pol: UcPolEdit) -> UcPol:
data = pol.model_dump(exclude={
"edit_key", "selected", "sel_pocet", "sel_delitel", })
return UcPol(**data)
# --- konverze polozky z DB tvaru na editacni
def ucpol_to_edit(pol: UcPol) -> UcPolEdit:
return UcPolEdit( **pol.model_dump(),
edit_key=datetime.now().isoformat(timespec="milliseconds"),
selected=False, sel_pocet=None, sel_delitel=None, sent=True, )
# --- struktura platby uctu
class Platba(BaseModel):
code:str #typ platby pro trideni (CASH, CREDIT...)
nazev:str #nazev (Hotove, Master card...)
suma:float #castka v mene
unit: str #Kč, Euro...
rate: float #kurz v okamziku platby
suma_czk: float #kurz v zakladni uctovaci mene
fiscal: bool=False #pouziva se ve vykaznictvi -> EET
is_bankterm: bool = False
p_kopii: int = 1
tip: float = 0
poznamka:str|None=None #pro platbu kartou ci jinak
hotel_charge: HotelChargeTarget | None = None
terminal_result: Dict[str, Any] = Field(default_factory=dict)
def djson(self):
return self.model_dump(mode='json')
# --- pole plateb pro ucet
class Platby(BaseModel):
platby:list[Platba]
def djson(self):
return self.model_dump(mode='json')
# --- dph
class Dph(BaseModel):
rate: str #sazba dph
zaklad: float #danovy zaklad
def djson(self):
return self.model_dump(mode='json')
# --- pole dph
class Dane(BaseModel):
dane:list[Dph]
def djson(self):
return self.model_dump(mode='json')
class UcetDiscount(BaseModel):
id: str = ""
id_zlavy: int | None = None
name: str = ""
typ_zlavy: int = 1
source: str = "account"
value: float = 0.0
amount: float = 0.0
pct: float | None = None
price_level: str = ""
ibajedna: int = 0
line_ids: list[str] = Field(default_factory=list)
payment_code: str = ""
payment_name: str = ""
# --- zakladni struktura a metody uctu
class UcetBase(BaseModel):
open_at: str | None = None #ucet otevren
closed_at: str | None = None #ucet uzavren
datetime: str = "" #cas a datum uzavreni (pro uzaverku)
id_kas: str = "" #identifikator pokladny (2 znaky)
ucislo: Optional[str] = None #cislo uctu (PP999999 jako z FOODu)
c_uzaverka: int | None = None #vazba na radek uzaverky v clsrep
autor: str = "" #jmeno cisnika
stul: Optional[str] = None #cislo stolu
table_name: str = "" #zobrazovany nazov stola
room_name: str = "" #meno miestnosti/strediska zo stolovej mapy
storno: Optional[str] = None #cislo uctu, ktery tento stornoval
is_storno: Optional[str] = None#jsem storno ucet
origin: str | None = None #Normal, Storno, Zmena_Platby
pohladavka: int | None = None #1 = specialny fiskalny doklad uhrada pohladavky
cash_operation: str | None = None #manual_deposit/manual_withdrawal/auto_deposit/auto_withdrawal
blocked_by: str = "" #cislo terminalu a cas blokace (ID|--:--)
discount_abs: float = 0 #absolutni sleva
discount_id: int | None = None
discount_name: str = ""
discounts_applied: list[UcetDiscount] = Field(default_factory=list, exclude=True)
discounts_prorated: bool = False
loyalty_card: str = ""
loyalty_name: str = ""
loyalty_info: str = ""
uver: UverZaznam | None = None
hotel_charge: HotelChargeTarget | None = None
hotel_charge_preparation: HotelChargePreparation | None = None
hotel_charge_send_result: HotelChargeSendResult | None = None
fiscal_result: Dict[str, Any] = Field(default_factory=dict)
bill_printer: str = ""
send_receipt_email: bool = False
receipt_email: str = ""
total_base_currency: float = 0 #suma uctu v zakladni mene
round50: float = 0 #suma zaokruhleni na ucte: round50 = suma platieb - suma poloziek
limit_id: int | None = None
limit_den_id: int | None = None
limit_rov_ids: list[int] = Field(default_factory=list)
limit_cenhlad: str = ""
limit_mode: bool = False
#total_slev: float = 0 #suma slev na ucte
checksum_val: str = "" #DB ucty maji checksum (rychla identifikace pro udate)
platby: List[Platba] = Field(default_factory=list) #pole plateb
dane: List[Dph] = Field(default_factory=list) #pole dph
guests: list = [{"id": "g1", "name": "Hosť 1"}]
courses: list = [{"id": "c1", "name": "Chod 1"}]
guest_count: int = 1
course_count: int = 1
# ---------------------
def open_time(self) -> str:
return self.open_at or "--:--"
# ---------------------
def blocked_client(self) -> str | None:
if not self.blocked_by:
return None
return self.blocked_by.split("|", 1)[0]
# --- účetní projekce (urci, maji-li ucty ruznych tvaru stejny obsah položek
def accounting_projection(self):
acc = defaultdict(int)
for p in self.poloz:
key = ( p.id_card, round(p.cena, 4), p.dph, p.mena, p.cenhlad, p.delitel, )
acc[key] += p.pocet
return sorted(acc.items())
# --- checksum uctu, pro zabezpeceni a identifikaci zmen
def checksum(self) -> str:
payload = {
"polozky": self.accounting_projection(),
"platby": sorted(
(
p.nazev,
round(p.suma, 4),
round(getattr(p, "tip", 0) or 0, 4),
p.poznamka,
)
for p in self.platby ),
"round50": round(getattr(self, "round50", 0) or 0, 4),
"dane": sorted(
(d.rate, round(d.zaklad, 4))
for d in self.dane ),}
raw = json.dumps(
payload,
separators=(",", ":"),
ensure_ascii=False, )
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
# ---------------------
def djson(self):
return self.model_dump(mode='json')
# ---------------------
def prn(self):
print(f"Ucet\n{self.model_dump(mode='json')}")
# ---------------------
def poladd(self, pol:UcPol):
self.poloz.append( pol)
# --- prida polozku z ceniku na ucet (vola PosDialog)
def cenpoladd( self, cenpol: CenPol, cena: Cena, pocet: int, delitel: int,):
pol = UcPol( id_card=cenpol.id_card, nazev=cenpol.ch_name, cena=cena.cena, pocet=pocet, delitel=delitel,
c_druh=getattr(cenpol, "c_druh", 0),
druh=getattr(cenpol, "druh", ""),
spart=getattr(cenpol, "spart", ""),
prn_no=getattr(cenpol, "prn_no", ""),
dph=cena.dan, cenhlad=cena.name, sklad=cenpol.sklad, line_id=cenpol.line_id, group_id=cenpol.group_id, parent_id=cenpol.parent_id, typ_menu=cenpol.typ_menu)
self.poloz.append(pol)
return pol
# ---------------------
def pltadd(self, platba: Platba):
self.platby.append( platba)
# ---------------------
def total_czk(self) -> float:
total = 0.0
for p in self.poloz:
qty = p.pocet / (p.delitel or 1)
total += qty * p.cena
return round(total, 2)
# ---------------------
def total_slev(self) -> float:
total = self.total_czk()
total_puv = 0.0
for p in self.poloz:
qty = p.pocet / (p.delitel or 1)
cena_puv = p.cena_puv if p.cena_puv is not None else p.cena
total_puv += qty * cena_puv
return round(total_puv - total, 2)
# ---------------------
def sumdph(self):
self.dane = []
total_zaklad = 0.0
total_with_dph_all = 0.0 # 👈 nově
# spočti základy BEZ slevy
for i in self.poloz:
qty = i.pocet / (i.delitel or 1)
total_with_dph = qty * i.cena
total_with_dph_all += total_with_dph # 👈 nově
rate = float(i.dph) # např. 1.21
zaklad = total_with_dph if rate == -1 else total_with_dph / rate
total_zaklad += zaklad
for d in self.dane:
if float(d.rate) == rate:
d.zaklad += zaklad
break
else:
self.dane.append(Dph(rate=i.dph, zaklad=zaklad))
discount = round(getattr(self, "discount_abs", 0.0) or 0.0, 2)
if getattr(self, "discounts_prorated", False):
discount = 0.0
# rozpočti slevu (vč. DPH!) do základů
if discount > 0 and total_zaklad > 0:
for d in self.dane:
podil = d.zaklad / total_zaklad
discount_with_dph = discount * podil
d_rate = float(d.rate)
discount_base = discount_with_dph if d_rate == -1 else discount_with_dph / d_rate
d.zaklad -= discount_base
total_with_dph_all -= discount
# finální zaokrouhlení DPH základů
for d in self.dane:
d.zaklad = round(d.zaklad, 4)
# ulož celkovou částku včetně DPH
self.total_base_currency = round(total_with_dph_all, 2)
# --- zakladni tvar uctu pro DB
class Ucet(UcetBase):
poloz: List[UcPol] = Field(default_factory=list)
class KitchenPrintRequest(BaseModel):
id_kas: str
kind: str = "bon"
ucet: Ucet
room_name: str = ""
pos_name: str = ""
required: bool = True
priority: int = 50
class ReceiptPrintRequest(BaseModel):
id_kas: str
kind: str = "receipt"
ucet: Ucet
printer_no: str = ""
title: str = ""
pos_name: str = ""
headers: list[str] = Field(default_factory=list)
footers: list[str] = Field(default_factory=list)
required: bool = False
priority: int = 40
copies: int = 1
class ClosurePrintRequest(BaseModel):
id_kas: str
kind: str = "closure"
printer_no: str = ""
clsrep_no: str | None = None
title: str = "Uzavierka"
text: str
required: bool = False
priority: int = 35
copies: int = 1
class UsageItem(BaseModel):
id_card: int = 0
name: str = ""
c_druh: int = 0
druh: str = ""
spart: str = ""
quantity: float = 0.0
amount: float = 0.0
receipts: int = 0
class UsageCategory(BaseModel):
key: str = ""
name: str = ""
c_druh: int = 0
spart: str = ""
quantity: float = 0.0
amount: float = 0.0
items: list[UsageItem] = Field(default_factory=list)
class UsageReportOut(BaseModel):
id_kas: str = ""
mode: str = "current"
date_from: str = ""
date_to: str = ""
ucislo_from: str = ""
ucislo_to: str = ""
closed_at_from: str = ""
closed_at_to: str = ""
categories: list[UsageCategory] = Field(default_factory=list)
total_quantity: float = 0.0
total_amount: float = 0.0
class ReceiptPrintPreviewOut(BaseModel):
text: str
meta: Dict[str, Any] = Field(default_factory=dict)
class FiscalReceiptPrintRequest(BaseModel):
id_kas: str
ucet: Ucet
printer_no: str = ""
title: str = ""
pos_name: str = ""
headers: list[str] = Field(default_factory=list)
footers: list[str] = Field(default_factory=list)
class FiscalReceiptCopyRequest(BaseModel):
id_kas: str
ucet: Ucet
printer_no: str = ""
bill_id: str = ""
class FiscalReceiptPrintOut(BaseModel):
ok: bool = True
ucet: Ucet
response: Dict[str, Any] = Field(default_factory=dict)
fiscal_result: Dict[str, Any] = Field(default_factory=dict)
class FiscalCashOperationRequest(BaseModel):
id_kas: str
operation: str
amount: float
payment: PaymentType
printer_no: str = ""
author: str = ""
pos_name: str = ""
class FiscalCashOperationOut(BaseModel):
ok: bool = True
ucet: Ucet
response: Dict[str, Any] = Field(default_factory=dict)
fiscal_result: Dict[str, Any] = Field(default_factory=dict)
# --- tvar uctu v PosDialog
class UcetEdit(UcetBase):
poloz: List[UcPolEdit] = Field(default_factory=list)
# --- tvar uctu pro AccountSelectDialog
class UcetSelect(BaseModel):
ucislo: str = ""
id_kas: str = ""
c_uzaverka: int | None = None
stul: str | None = None
room_name: str = ""
blocked_by: str = ""
open_at: str | None = None
closed_at: str | None = None
closed: bool = False
autor: str = ""
total_base_currency: float = 0 #suma uctu v zakladni mene
payments_text: str = ""
status_text: str = ""
is_storno: str | None = None #Jsem storno uctu
storno: str | None = None #Ucet je stronovan by
origin: str | None = None # Normal, Zmena_Platby, Storno
pohladavka: int | None = None
cash_operation: str | None = None
# --- konverze DB uctu na PosDialog ucet
def ucet_to_edit(ucet: Ucet) -> UcetEdit:
data = ucet.model_dump(exclude={"poloz"})
return UcetEdit(
**data, poloz=[ucpol_to_edit(pol) for pol in ucet.poloz],)
# --- konverze PosDialog uctu na DB ucet
def ucet_edit_to_ucet(ucet_edit: UcetEdit) -> Ucet:
data = ucet_edit.model_dump(exclude={"poloz"})
return Ucet( **data, poloz=[ucpol_edit_to_ucpol(pol) for pol in ucet_edit.poloz], )
# --- trida ucty pro ulozeni uctu v RAM (zatim nepouzivas)
class Ucty(BaseModel):
ucty: list[Ucet]
#---------------------
def getucet(self,ucislo:str)->Ucet:
ret=None
for i in self.ucty:
if i.ucislo==ucislo:
ret=i.model_dump(mode='json')
return ret
#---------------------
def delucet(self,ucislo:str)->Ucet:
for i in self.ucty:
if i.ucislo==ucislo:
self.ucty.remove(i)
ret="Deleted " + ucislo
break
else:
ret="Not found" + ucislo
return ret
#---------------------
def adducet(self,ucet:Ucet):
self.ucty.append(ucet)
#---------------------
def updtpolucet(self,ucet:Ucet):
for i in self.ucty:
if i.ucislo==ucet.ucislo:
i.poloz=ucet.poloz
ret="updtpolucet OK"
else:
ret="updtpolucet Fail"
return ret
#---------------------
def dump(self)->str:
s = ''
for i in self.ucty:
s+=i.ucislo+'\n'
print(s)
return 'Seznam uctu\n'+s
#---------------------
def prn(self):
print('\nUcty')
for i in self.ucty:
print(i.ucislo)
i.prn()
#---------------------
def djson(self):
return self.model_dump(mode='json')
class MergeUcetRequest(BaseModel):
ucet: Ucet
target_stul: str
#----------------------------------------------------------
#--- Model pro uzaverku
#----------------------------------------------------------
class ClosureDetailOut(BaseModel):
clsrep: dict
data: dict
ucty: list[dict]
# ----------clsheader
class ClosureIntervalOut(BaseModel):
clsrep_no: str
ucislo_od: str
ucislo_do: str
closed_at_od: str
closed_at_do: str
# ----------interval
class ClosureInterval(BaseModel):
ucislo_od: str
ucislo_do: str
closed_at_od: str
closed_at_do: str
# ----------------SUMMARY
class ClosureSummary(BaseModel):
pocet_uctu: int
total_base_currency: float
total_payments: float
difference: float
# ----------------UZIVATEL
class ClosureUser(BaseModel):
total_base_currency: float
hotovost: float
# ----------------DPH
class ClosureVAT(BaseModel):
zaklad: float
dan: float
celkem: float
class ClosureCarryInput(BaseModel):
prn_no: str = ""
payment_code: str = ""
carry_amount: float = 0.0
class ClosureSaveRequest(BaseModel):
cash_carry: list[ClosureCarryInput] = Field(default_factory=list)
# ----------------HLAVNI MODEL
class ClosureReportOut(BaseModel):
blocked_by: str
clsrep_no: str | None = None
created_at: str | None = None
interval: ClosureInterval
summary: ClosureSummary
platby: Dict[str, float]
uzivatele: Dict[str, ClosureUser]
dph: Dict[str, ClosureVAT]
open_ucty: list[dict[str, Any]] = []
cash_state: list[dict[str, Any]] = Field(default_factory=list)
transfers: list[dict[str, Any]] = Field(default_factory=list)
sections: Dict[str, Any] = Field(default_factory=dict)
closure_settings: Dict[str, Any] = Field(default_factory=dict)
warnings: list[str] = Field(default_factory=list)