1687 lines
50 KiB
Python
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)
|