from __future__ import annotations
import datetime as dt
import html
import re
import time
import xml.etree.ElementTree as ET
import requests
from requests.auth import HTTPBasicAuth
import data
class HotelServiceError(Exception):
pass
def _s(value) -> str:
return "" if value is None else str(value).strip()
def _bool_value(value) -> bool:
if isinstance(value, bool):
return value
return _s(value).lower() in {"1", "true", "t", "yes", "y", "ano", "a"}
def _int_value(value, default: int = 0) -> int:
try:
return int(float(str(value).strip()))
except Exception:
return default
def setup_bool(params: dict, name: str, default: bool = False) -> bool:
return _bool_value(params.get(name, default))
def setup_int(params: dict, name: str, default: int = 0) -> int:
return _int_value(params.get(name, default), default)
def normalize_card_code(card_code: str, params: dict) -> str:
code = _s(card_code)
card_type = _s(params.get("hotel_karta_typ") or "SALTO").upper()
length = setup_int(params, "hotel_karta_length", 14)
if card_type == "SALTO":
normalized = ""
while code:
if len(code) > 1:
normalized += code[-2:]
code = code[:-2]
else:
normalized += code
code = ""
code = normalized.ljust(14, "0")
elif length:
code = code.ljust(length, "0")
return code
def supports_rooms(typ_hotel: int) -> bool:
return int(typ_hotel or 0) in {17, 18, 21}
def supports_groups(typ_hotel: int) -> bool:
return int(typ_hotel or 0) == 17
def supports_card(typ_hotel: int) -> bool:
return int(typ_hotel or 0) in {6, 10, 17, 18, 21}
def manual_room_required(typ_hotel: int) -> bool:
return not supports_rooms(typ_hotel)
def reception_public(reception: data.Recepcia) -> data.HotelReception:
typ_hotel = int(getattr(reception, "typ_hotel", 0) or 0)
return data.HotelReception(
id=int(reception.id),
hotel=_s(reception.hotel),
typ_hotel=typ_hotel,
hor_prefix=_s(reception.hor_prefix),
supports_rooms=supports_rooms(typ_hotel),
supports_groups=supports_groups(typ_hotel),
supports_card=supports_card(typ_hotel),
)
def load_rooms(reception: data.Recepcia, params: dict) -> list[data.HotelRoom]:
typ_hotel = int(reception.typ_hotel or 0)
if typ_hotel == 17:
return _hores_rooms(reception, params)
if typ_hotel == 18:
return _previo_rooms(reception)
if typ_hotel == 21:
return _mews_rooms(reception)
return []
def load_guests(
reception: data.Recepcia,
params: dict,
room_id: str = "",
room_code: str = "",
account_id: str = "",
) -> list[data.HotelGuest]:
typ_hotel = int(reception.typ_hotel or 0)
if typ_hotel == 17:
return _hores_guests(reception, params, room_id=room_id, room_code=room_code)
if typ_hotel == 18:
return _previo_guests(reception, room_id=room_id)
if typ_hotel == 21:
return _mews_guests(reception, room_id=room_id, room_code=room_code)
if typ_hotel == 10:
return _protel_guests(reception, room_code=room_code)
if typ_hotel == 6:
raise HotelServiceError("Fidelio/Opera databazovy most zatial nie je napojeny v server_sqlite.")
raise HotelServiceError(f"Nepodporovany typ recepcie {typ_hotel}.")
def check_card(
reception: data.Recepcia,
params: dict,
card_code: str,
) -> data.HotelCardResult:
typ_hotel = int(reception.typ_hotel or 0)
normalized = normalize_card_code(card_code, params)
if typ_hotel == 17:
return _hores_card(reception, normalized)
if typ_hotel == 18:
return _previo_card(reception, normalized)
if typ_hotel == 21:
return _mews_card(reception, normalized)
if typ_hotel == 10:
return _protel_card(reception, normalized)
if typ_hotel == 6:
raise HotelServiceError("Fidelio/Opera karta zatial nie je napojena v server_sqlite.")
raise HotelServiceError(f"Nepodporovany typ recepcie {typ_hotel}.")
def charge_account(
reception: data.Recepcia,
params: dict,
preparation: data.HotelChargePreparation,
) -> dict:
typ_hotel = int(reception.typ_hotel or 0)
if typ_hotel == 17:
return _hores_charge_account(reception, params, preparation)
if typ_hotel == 18:
return _previo_charge_account(reception, params, preparation)
if typ_hotel == 21:
return _mews_charge_account(reception, preparation)
if typ_hotel == 10:
return _protel_charge_account(reception, params, preparation)
raise HotelServiceError(f"Nepodporovane odoslanie uctu pre typ recepcie {typ_hotel}.")
def transfer_cash(
reception: data.Recepcia,
params: dict,
payload: dict,
) -> dict:
typ_hotel = int(reception.typ_hotel or 0)
if typ_hotel == 17:
return _hores_transfer_cash(reception, params, payload)
if typ_hotel == 18:
return _previo_transfer_cash(reception, params, payload)
if typ_hotel == 21:
return _mews_transfer_cash(reception, payload)
raise HotelServiceError(f"Uzavierkovy prenos trzieb nie je podporovany pre typ recepcie {typ_hotel}.")
def _host_with_scheme(host: str, scheme: str = "https") -> str:
host = _s(host).rstrip("/")
if not host:
raise HotelServiceError("Recepcia nema nastavenu adresu servera.")
if not re.match(r"^https?://", host, flags=re.IGNORECASE):
host = f"{scheme}://{host}"
return host
def _append_port(host: str, port) -> str:
port = _s(port)
if not port:
return host
if re.search(r":\d+$", host):
return host
return f"{host}:{port}"
def _request_json(method: str, url: str, *, auth=None, json_body=None, params=None, verify=False):
try:
response = requests.request(
method,
url,
auth=auth,
json=json_body,
params=params,
timeout=15,
verify=verify,
)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
raise HotelServiceError(f"Komunikacia s recepciou zlyhala: {e}") from e
except ValueError as e:
raise HotelServiceError("Recepcia nevratila platny JSON.") from e
def _request_text(method: str, url: str, *, auth=None, data_body=None, headers=None, verify=False):
try:
response = requests.request(
method,
url,
auth=auth,
data=data_body,
headers=headers,
timeout=15,
verify=verify,
)
response.raise_for_status()
return response.text
except requests.RequestException as e:
raise HotelServiceError(f"Komunikacia s recepciou zlyhala: {e}") from e
def _parse_xml(text: str):
try:
return ET.fromstring(text or "")
except ET.ParseError as e:
raise HotelServiceError("Recepcia nevratila platne XML.") from e
def _fmt_amount(value) -> str:
try:
return f"{float(value):.2f}"
except Exception:
return "0.00"
def _fmt_qty(value) -> str:
try:
number = float(value)
except Exception:
return "1"
if number.is_integer():
return str(int(number))
return f"{number:.3f}".rstrip("0").rstrip(".")
def _vat_percent(value) -> str:
try:
rate = float(str(value).replace(",", "."))
except Exception:
return "0"
if 1 <= rate < 3:
rate = (rate - 1) * 100
elif 0 < rate < 1:
rate = rate * 100
if float(rate).is_integer():
return str(int(rate))
return f"{rate:.2f}".rstrip("0").rstrip(".")
def _currency_code(preparation: data.HotelChargePreparation) -> str:
return (_s(getattr(preparation, "currency", "")) or "EUR").upper()
def _currency_record_value(record: dict, names: tuple[str, ...]) -> str:
lowered = {str(k).lower(): v for k, v in record.items()}
for name in names:
if name.lower() in lowered:
return _s(lowered[name.lower()])
return ""
def _currency_id_from_records(records, currency_code: str) -> int:
currency_code = _s(currency_code).upper()
if isinstance(records, dict):
records = records.get("data") or records.get("currencies") or records.get("Currencies") or list(records.values())
if isinstance(records, dict):
records = list(records.values())
if not isinstance(records, list):
records = []
candidates = []
for record in records:
if not isinstance(record, dict):
continue
rec_id = _currency_record_value(record, ("id", "Id", "curId", "currency_id", "CurrencyId"))
rec_code = _currency_record_value(record, ("code", "Code", "name", "Name", "iso_code", "IsoCode", "currency", "Currency"))
if rec_id:
candidates.append((rec_code.upper(), rec_id))
if rec_code.upper() == currency_code and rec_id:
return _int_value(rec_id, 0)
if len(candidates) == 1 and candidates[0][1]:
return _int_value(candidates[0][1], 0)
return 0
def _currency_id_from_xml(root, currency_code: str) -> int:
currency_code = _s(currency_code).upper()
candidates = []
for node in root.iter():
children = list(node)
if not children:
continue
values = {child.tag.lower(): _s(child.text) for child in children}
rec_id = values.get("curid") or values.get("id") or values.get("currencyid")
rec_code = (
values.get("code")
or values.get("curcode")
or values.get("name")
or values.get("currency")
or values.get("shortcut")
)
if rec_id:
candidates.append((_s(rec_code).upper(), rec_id))
if _s(rec_code).upper() == currency_code and rec_id:
return _int_value(rec_id, 0)
if len(candidates) == 1 and candidates[0][1]:
return _int_value(candidates[0][1], 0)
return 0
def _hotel_currency_id(
reception: data.Recepcia,
params: dict,
preparation: data.HotelChargePreparation,
) -> int:
typ_hotel = int(getattr(reception, "typ_hotel", 0) or 0)
currency_code = _currency_code(preparation)
if typ_hotel == 17:
currency_id = _hores_currency_id(reception, currency_code)
elif typ_hotel == 18:
currency_id = _previo_currency_id(reception, currency_code)
else:
currency_id = 0
if currency_id:
return currency_id
fallback = setup_int(params or {}, "hotel_currency_id", 0)
if fallback:
return fallback
raise HotelServiceError(f"Mena {currency_code} sa nenasla v zozname mien recepcie.")
def _charge_items(preparation: data.HotelChargePreparation, currency_id: int = 0) -> list[dict]:
items = []
for line in preparation.lines or []:
qty = line.quantity or 1
unit_price = line.unit_price if line.unit_price else (line.amount / qty if qty else line.amount)
items.append({
"raster_id": _s(line.raster_id),
"price": _fmt_amount(line.amount),
"vat": _vat_percent(line.dph),
"receipt_number": _s(preparation.receipt_number),
"currency_id": currency_id,
"units": _fmt_qty(qty),
"unit_price": _fmt_amount(unit_price),
"description": _s(line.description),
})
return items
def _fmt_date(value: str) -> str:
value = _s(value)
if not value:
return ""
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y%m%d%H%M"):
try:
return dt.datetime.strptime(value, fmt).strftime("%Y-%m-%d")
except Exception:
pass
return value[:10]
def _find_child_text(element, tag: str) -> str:
tag = tag.lower()
for child in list(element):
if child.tag.lower() == tag:
return _s(child.text)
return ""
def _hores_config(reception: data.Recepcia):
host = _append_port(_host_with_scheme(reception.hor_ip, "https"), reception.hor_port)
api_user = _s(reception.api_meno)
api_password = _s(reception.api_heslo)
if api_user and api_password:
return f"{host}/api", HTTPBasicAuth(api_user, api_password), True
return f"{host}/cash_register", HTTPBasicAuth(_s(reception.hor_meno), _s(reception.hor_heslo)), False
def _hores_current_accounts(reception: data.Recepcia):
base, auth, api = _hores_config(reception)
if api:
response = _request_json("GET", f"{base}/current_accounts", auth=auth, verify=False)
return response.get("data", response)
return _request_json("GET", f"{base}/currentAccounts", auth=auth, verify=False)
def _hores_currency_id(reception: data.Recepcia, currency_code: str) -> int:
base, auth, api = _hores_config(reception)
response = _request_json("GET", f"{base}/currencies", auth=auth, verify=False)
if api and isinstance(response, dict):
response = response.get("data", response)
return _currency_id_from_records(response, currency_code)
def _hores_rooms(reception: data.Recepcia, params: dict) -> list[data.HotelRoom]:
response = _hores_current_accounts(reception)
rooms: list[data.HotelRoom] = []
allow_groups = setup_bool(params, "is_horskup", False)
allow_prepaid = setup_bool(params, "is_zapltiz", True)
if isinstance(response, dict) and allow_groups:
for group in response.get("groups") or []:
group_id = _s(group.get("id"))
group_name = _s(group.get("name"))
rooms.append(data.HotelRoom(
type="group",
id=group_id,
account_id=_s(group.get("account_id")),
room_code=group_name,
room_name=group_name,
guest_name=group_name,
checkin_date=_fmt_date(group.get("checkin_date")),
checkout_date=_fmt_date(group.get("checkout_date")),
))
for room in (response.get("rooms") if isinstance(response, dict) else response) or []:
prepaid = bool(room.get("accomodation_prepayed", False))
room_code = _s(room.get("code") or room.get("room_code") or room.get("name") or room.get("id"))
guest_name = " ".join(x for x in (_s(room.get("first_name")), _s(room.get("last_name"))) if x).strip()
rooms.append(data.HotelRoom(
type="room",
id=_s(room.get("id")),
account_id=_s(room.get("account_id")),
room_code=room_code,
room_name=room_code,
guest_name=guest_name,
checkin_date=_fmt_date(room.get("checkin_date")),
checkout_date=_fmt_date(room.get("checkout_date")),
building=_s(room.get("building")),
can_charge=allow_prepaid or not prepaid,
message="Izba je oznacena ako vyplatena." if prepaid and not allow_prepaid else "",
extra={"accomodation_prepayed": prepaid},
))
return rooms
def _hores_guests(reception: data.Recepcia, params: dict, *, room_id: str, room_code: str) -> list[data.HotelGuest]:
base, auth, api = _hores_config(reception)
rid = _s(room_id or room_code)
if not rid:
raise HotelServiceError("Cislo izby je povinne.")
if api:
response = _request_json("GET", f"{base}/current_guests", auth=auth, params={"room_id": rid}, verify=False)
guests = response.get("data", response)
else:
guests = _request_json("POST", f"{base}/currentGuests", auth=auth, json_body={"room_id": rid}, verify=False)
result = []
for guest in guests or []:
name = " ".join(x for x in (_s(guest.get("first_name")), _s(guest.get("last_name"))) if x).strip()
result.append(data.HotelGuest(
id=_s(guest.get("id")),
guest_name=name or _s(guest.get("guest_name")),
room_id=rid,
room_code=_s(guest.get("room_code") or room_code),
account_id=_s(guest.get("account_id")),
checkin_date=_fmt_date(guest.get("checkin_date")),
checkout_date=_fmt_date(guest.get("checkout_date")),
building=_s(guest.get("building")),
))
return result
def _hores_card(reception: data.Recepcia, card_code: str) -> data.HotelCardResult:
base, auth, api = _hores_config(reception)
payload = {"serial_number": card_code}
if api:
response = _request_json("POST", f"{base}/check_card", auth=auth, json_body=payload, verify=False)
else:
response = _request_json("POST", f"{base}/checkCard", auth=auth, json_body=payload, verify=False)
if not isinstance(response, dict) or not _s(response.get("room_id") or response.get("room_code")):
raise HotelServiceError("Nacitanu kartu sa nepodarilo priradit k izbe.")
return data.HotelCardResult(
room_id=_s(response.get("room_id")),
room_code=_s(response.get("room_code")),
account_id=_s(response.get("account_id")),
)
def _hores_charge_account(reception: data.Recepcia, params: dict, preparation: data.HotelChargePreparation) -> dict:
base, auth, api = _hores_config(reception)
target = preparation.target
if not target:
raise HotelServiceError("Hotelovy ucet nema ciel.")
currency_id = _hotel_currency_id(reception, params, preparation)
payload = {
"items": _charge_items(preparation, currency_id),
"account_id": _s(target.account_id or target.group_id),
}
if _s(target.guest_id):
payload["guest_id"] = _s(target.guest_id)
endpoint = "/charge_account" if api else "/chargeAccount"
response = _request_json("POST", f"{base}{endpoint}", auth=auth, json_body=payload, verify=False)
if isinstance(response, dict) and response.get("result") is None and response.get("ok") is not True:
raise HotelServiceError("Recepcia vratila neplatnu odpoved pri natiahnuti uctu.")
return {"ok": True, "message": "OK"}
def _closure_item_currency_id(reception: data.Recepcia, item: dict, params: dict) -> int:
currency_id = _int_value(item.get("currency_id"), 0)
if currency_id:
return currency_id
currency = _s(item.get("currency") or "EUR").upper()
try:
return _hores_currency_id(reception, currency)
except Exception:
fallback = setup_int(params or {}, "hotel_currency_id", 0)
if fallback:
return fallback
raise
def _hores_transfer_cash(reception: data.Recepcia, params: dict, payload: dict) -> dict:
base, auth, api = _hores_config(reception)
items = []
for item in payload.get("items") or []:
amount = float(_fmt_amount(item.get("amount")))
vat_amount = float(_fmt_amount(item.get("vat_amount")))
if abs(amount) < 0.005 and abs(vat_amount) < 0.005:
continue
items.append({
"default_currency_price": _fmt_amount(item.get("amount")),
"price": _fmt_amount(item.get("amount_currency", item.get("amount"))),
"currency_id": _closure_item_currency_id(reception, item, params),
"receipt_number": _s(item.get("receipt_number")),
"credit_card_number": _s(item.get("credit_card_number")),
"credit_card_type_id": _int_value(item.get("credit_card_type_id"), 0),
"vat_price": _fmt_amount(item.get("vat_amount")),
"raster_id": _s(item.get("raster_id")),
"payment_method_id": _int_value(item.get("payment_method_id"), 0),
"note": _s(item.get("note")),
"exchange_rate": _s(item.get("exchange_rate")),
})
if not items:
return {"ok": True, "message": "Nie je co prenasat.", "transferred": 0}
request_payload = [{
"cash_register_code": _s(payload.get("cash_register_code") or payload.get("id_kas")),
"items": items,
}]
endpoint = "/transfer_cash" if api else "/transferCash"
response = _request_json("POST", f"{base}{endpoint}", auth=auth, json_body=request_payload, verify=False)
if isinstance(response, dict) and response.get("result") is None and response.get("ok") is not True:
raise HotelServiceError("Recepcia vratila neplatnu odpoved pri uzavierkovom prenose.")
return {"ok": True, "message": "OK", "transferred": len(items), "response": response}
def _previo_base(reception: data.Recepcia) -> str:
return _host_with_scheme(reception.hor_ip, "https")
def _previo_currency_id(reception: data.Recepcia, currency_code: str) -> int:
xml = (
''
"