# --- 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)