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

1121 lines
42 KiB
Python

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 = (
'<?xml version="1.0"?>'
"<request>"
f"<login>{html.escape(_s(reception.hor_meno))}</login>"
f"<password>{html.escape(_s(reception.hor_heslo))}</password>"
f"<hotId>{html.escape(_s(reception.hor_prefix))}</hotId>"
"</request>"
)
text = _request_text(
"POST",
f"{_previo_base(reception)}/system/getCurrencies",
auth=HTTPBasicAuth(_s(reception.hor_meno), _s(reception.hor_heslo)),
data_body=xml,
verify=False,
)
return _currency_id_from_xml(_parse_xml(text), currency_code)
def _previo_request_xml(reception: data.Recepcia, res_id: str = "") -> str:
today = dt.datetime.today()
tomorrow = today + dt.timedelta(days=1)
res = f"<resId>{html.escape(_s(res_id))}</resId>" if _s(res_id) else ""
return (
'<?xml version="1.0"?>'
"<request>"
f"<login>{html.escape(_s(reception.hor_meno))}</login>"
f"<password>{html.escape(_s(reception.hor_heslo))}</password>"
f"<hotId>{html.escape(_s(reception.hor_prefix))}</hotId>"
"<term>"
f"<from>{today.strftime('%Y-%m-%d')}</from>"
f"<to>{tomorrow.strftime('%Y-%m-%d')}</to>"
"</term>"
"<statuses><cosId>3</cosId>"
f"{res}"
"</statuses>"
"</request>"
)
def _previo_search(reception: data.Recepcia, res_id: str = ""):
xml = _previo_request_xml(reception, res_id=res_id)
text = _request_text(
"POST",
f"{_previo_base(reception)}/hotel/searchReservations/",
auth=HTTPBasicAuth(_s(reception.hor_meno), _s(reception.hor_heslo)),
data_body=xml,
verify=False,
)
return _parse_xml(text)
def _previo_rooms(reception: data.Recepcia) -> list[data.HotelRoom]:
reservations = _previo_search(reception)
response = []
for reservation in reservations:
room_name = ""
room_id = ""
account_id = ""
guest_name = ""
checkin_date = ""
checkout_date = ""
for node in reservation:
tag = node.tag.lower()
if tag == "comid":
account_id = _s(node.text)
elif tag == "object":
room_name = _find_child_text(node, "name")
room_id = _find_child_text(node, "objid")
elif tag == "term":
checkin_date = _fmt_date(_find_child_text(node, "from"))
checkout_date = _fmt_date(_find_child_text(node, "to"))
elif tag == "guest" and not guest_name:
guest_name = _find_child_text(node, "name")
response.append(data.HotelRoom(
type="room",
id=room_id,
account_id=account_id,
room_code=room_name,
room_name=room_name,
guest_name=guest_name,
checkin_date=checkin_date,
checkout_date=checkout_date,
))
return response
def _previo_guests(reception: data.Recepcia, *, room_id: str) -> list[data.HotelGuest]:
rid = _s(room_id)
if not rid:
raise HotelServiceError("ID izby je povinne.")
reservations = _previo_search(reception, res_id=rid)
response = []
for reservation in reservations:
room_id_resp = ""
checkin_date = ""
checkout_date = ""
guests = []
for node in reservation:
tag = node.tag.lower()
if tag == "object":
room_id_resp = _find_child_text(node, "objid")
elif tag == "term":
checkin_date = _fmt_date(_find_child_text(node, "from"))
checkout_date = _fmt_date(_find_child_text(node, "to"))
elif tag == "guest":
guests.append((_find_child_text(node, "gueid"), _find_child_text(node, "name")))
if room_id_resp == rid:
for guest_id, guest_name in guests:
response.append(data.HotelGuest(
id=guest_id,
guest_name=guest_name,
room_id=rid,
checkin_date=checkin_date,
checkout_date=checkout_date,
))
return response
def _previo_card(reception: data.Recepcia, card_code: str) -> data.HotelCardResult:
reservations = _previo_search(reception)
for reservation in reservations:
room_name = ""
room_id = ""
account_id = ""
for node in reservation:
tag = node.tag.lower()
if tag == "comid":
account_id = _s(node.text)
elif tag == "object":
room_name = _find_child_text(node, "name")
room_id = _find_child_text(node, "objid")
elif tag == "carddatalist":
for card_data in node:
if card_data.tag.lower() != "carddata":
continue
key = _find_child_text(card_data, "key")
if key.lower() == card_code.lower():
return data.HotelCardResult(
room_id=room_id,
room_code=room_name,
account_id=account_id,
)
raise HotelServiceError("Nacitana karta sa nenasla.")
def _previo_charge_xml(reception: data.Recepcia, params: dict, preparation: data.HotelChargePreparation) -> str:
target = preparation.target
if not target:
raise HotelServiceError("Hotelovy ucet nema ciel.")
currency_id = _hotel_currency_id(reception, params, preparation)
items_xml = []
for item in _charge_items(preparation, currency_id):
guest_xml = ""
if _s(target.guest_id):
guest_xml = f"<gueId>{html.escape(_s(target.guest_id))}</gueId>"
items_xml.append(
"<item>"
f"<name>{html.escape(item.get('description') or ('POS ' + _s(preparation.receipt_number)))}</name>"
f"<count>{html.escape(item['units'])}</count>"
f"<price>{html.escape(item['unit_price'])}</price>"
f"<taxRate>{html.escape(item['vat'])}</taxRate>"
f"{guest_xml}"
f"<segId>{html.escape(_s(item['raster_id']))}</segId>"
"</item>"
)
return (
'<?xml version="1.0"?>'
"<request>"
f"<login>{html.escape(_s(reception.hor_meno))}</login>"
f"<password>{html.escape(_s(reception.hor_heslo))}</password>"
f"<comId>{html.escape(_s(target.account_id or target.guest_id))}</comId>"
"<currency>"
f"<curId>{currency_id}</curId>"
"</currency>"
"<account>"
+ "".join(items_xml) +
"</account>"
"</request>"
)
def _previo_charge_account(reception: data.Recepcia, params: dict, preparation: data.HotelChargePreparation) -> dict:
xml = _previo_charge_xml(reception, params, preparation)
response = _request_text(
"POST",
f"{_previo_base(reception)}/hotel/addAccountItem",
auth=HTTPBasicAuth(_s(reception.hor_meno), _s(reception.hor_heslo)),
data_body=xml,
verify=False,
)
root = _parse_xml(response)
if root.tag.lower() == "error":
errors = [
_s(child.text)
for child in root
if child.tag.lower() in {"code", "message"} and _s(child.text)
]
raise HotelServiceError("\n".join(errors) or "Previo vratilo chybu.")
return {"ok": True, "message": "OK"}
def _previo_transfer_cash(reception: data.Recepcia, params: dict, payload: dict) -> dict:
rows = payload.get("previo_rows") or []
if not rows:
raise HotelServiceError("Uzavierkovy prenos do Previo nema pripravene previo_rows.")
transferred = 0
for row in rows:
row_payload = row.get("riadok") or row
items_xml = _s(row_payload.get("polozky"))
payment_payload = row_payload.get("platby") or {}
if not items_xml:
continue
response = _request_text(
"POST",
f"{_previo_base(reception)}/hotel/addAccountItem",
auth=HTTPBasicAuth(_s(reception.hor_meno), _s(reception.hor_heslo)),
data_body=items_xml.encode(),
verify=False,
)
root = _parse_xml(response)
if root.tag.lower() == "error":
errors = [
_s(child.text)
for child in root
if child.tag.lower() in {"code", "message"} and _s(child.text)
]
raise HotelServiceError("\n".join(errors) or "Previo vratilo chybu pri prenose poloziek.")
if payment_payload:
base = _previo_base(reception).replace("x1", "rest").replace(".cz", ".app")
response = _request_json(
"POST",
f"{base}/invoice/addPayment",
auth=HTTPBasicAuth(_s(reception.hor_meno), _s(reception.hor_heslo)),
json_body=payment_payload,
params=None,
verify=False,
)
if isinstance(response, dict) and response.get("message"):
raise HotelServiceError(str(response["message"]))
transferred += 1
return {"ok": True, "message": "OK", "transferred": transferred}
def _mews_base(reception: data.Recepcia) -> str:
return f"{_host_with_scheme(reception.hor_ip, 'https')}/api/connector/v1"
def _mews_payload(reception: data.Recepcia, **extra) -> dict:
payload = {
"ClientToken": _s(reception.hor_meno),
"AccessToken": _s(reception.hor_heslo),
"Client": "FoodMan",
}
payload.update(extra)
return payload
def _mews_post(reception: data.Recepcia, endpoint: str, payload: dict) -> dict:
return _request_json("POST", f"{_mews_base(reception)}{endpoint}", json_body=payload, verify=False)
def _mews_currency_code(reception: data.Recepcia, preparation: data.HotelChargePreparation) -> str:
currency_code = _currency_code(preparation)
response = _mews_post(reception, "/configuration/get", _mews_payload(reception))
currencies = response.get("Currencies", []) if isinstance(response, dict) else []
if not currencies:
return currency_code
for record in currencies:
if not isinstance(record, dict):
continue
code = _currency_record_value(record, ("Code", "code", "Currency", "currency"))
if code.upper() == currency_code:
return code.upper()
raise HotelServiceError(f"Mena {currency_code} sa nenasla v konfiguracii Mews.")
def _mews_reservation_window() -> tuple[str, str]:
now = dt.datetime.now(dt.timezone.utc)
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
end = now.replace(hour=23, minute=59, second=59, microsecond=999999)
return start.isoformat().replace("+00:00", "Z"), end.isoformat().replace("+00:00", "Z")
def _mews_reservations(reception: data.Recepcia, resource_id: str = "") -> list[dict]:
start, end = _mews_reservation_window()
extra = {
"States": ["Started"],
"StartUtc": start,
"EndUtc": end,
}
if resource_id:
extra["AssignedResourceIds"] = [resource_id]
response = _mews_post(reception, "/reservations/getAll", _mews_payload(reception, **extra))
return response.get("Reservations", [])
def _mews_resources(reception: data.Recepcia) -> list[dict]:
response = _mews_post(reception, "/resources/getAll", _mews_payload(reception))
return response.get("Resources", [])
def _mews_customers(reception: data.Recepcia, customer_ids: list[str] | None = None) -> list[dict]:
extra = {}
if customer_ids:
extra["CustomerIds"] = list(customer_ids)
response = _mews_post(reception, "/customers/getAll", _mews_payload(reception, **extra))
return response.get("Customers", [])
def _mews_rooms(reception: data.Recepcia) -> list[data.HotelRoom]:
reservations = _mews_reservations(reception)
resources = _mews_resources(reception)
resource_map = {_s(r.get("Id")): _s(r.get("Name")) for r in resources}
customer_ids = list({_s(r.get("CustomerId")) for r in reservations if _s(r.get("CustomerId"))})
customers = {
_s(c.get("Id")): " ".join(x for x in (_s(c.get("FirstName")), _s(c.get("LastName"))) if x).strip()
for c in _mews_customers(reception, customer_ids)
}
rooms = []
for res in reservations:
room_id = _s(res.get("AssignedSpaceId") or res.get("AssignedResourceId") or res.get("ResourceId"))
if not room_id:
continue
customer_id = _s(res.get("CustomerId"))
room_name = resource_map.get(room_id, room_id)
rooms.append(data.HotelRoom(
type="room",
id=room_id,
account_id=_s(res.get("AccountingAccountId") or customer_id),
room_code=room_name,
room_name=room_name,
guest_name=customers.get(customer_id, ""),
checkin_date=_fmt_date(res.get("StartUtc")),
checkout_date=_fmt_date(res.get("EndUtc")),
))
return rooms
def _mews_guests(reception: data.Recepcia, *, room_id: str, room_code: str) -> list[data.HotelGuest]:
rid = _s(room_id)
if not rid:
resources = _mews_resources(reception)
room = next((r for r in resources if _s(r.get("Name")).lower() == _s(room_code).lower()), None)
rid = _s(room.get("Id")) if room else ""
if not rid:
raise HotelServiceError("Neznama izba.")
reservations = _mews_reservations(reception, resource_id=rid)
response = []
for res in reservations:
ids = []
if _s(res.get("CustomerId")):
ids.append(_s(res.get("CustomerId")))
ids.extend(_s(x) for x in (res.get("CompanionIds") or []) if _s(x))
customers = _mews_customers(reception, ids)
for customer in customers:
name = " ".join(x for x in (_s(customer.get("FirstName")), _s(customer.get("LastName"))) if x).strip()
response.append(data.HotelGuest(
id=_s(customer.get("Id")),
guest_name=name,
room_id=rid,
room_code=room_code,
account_id=_s(res.get("AccountingAccountId") or customer.get("Id")),
checkin_date=_fmt_date(res.get("StartUtc")),
checkout_date=_fmt_date(res.get("EndUtc")),
))
return response
def _mews_card(reception: data.Recepcia, card_code: str) -> data.HotelCardResult:
response = _mews_post(reception, "/keyLockCards/getAll/", _mews_payload(reception))
cards = response.get("KeyLockCards", [])
now = dt.datetime.utcnow().isoformat() + "Z"
card = next((
c for c in cards
if _s(c.get("KeyCode")).lower() == card_code.lower()
and (not _s(c.get("ValidFromUtc")) or _s(c.get("ValidFromUtc")) <= now)
and (not _s(c.get("ValidToUtc")) or now <= _s(c.get("ValidToUtc")))
), None)
if not card:
raise HotelServiceError("Karta nie je platna alebo neexistuje.")
customer_id = _s(card.get("CustomerId"))
resource_id = _s(card.get("ResourceId"))
rooms = _mews_resources(reception)
room = next((r for r in rooms if _s(r.get("Id")) == resource_id), None)
reservations = _mews_reservations(reception, resource_id=resource_id)
reservation = next((r for r in reservations if _s(r.get("CustomerId")) == customer_id), None)
customers = _mews_customers(reception, [customer_id])
customer = customers[0] if customers else {}
guest_name = " ".join(x for x in (_s(customer.get("FirstName")), _s(customer.get("LastName"))) if x).strip()
return data.HotelCardResult(
room_id=resource_id,
room_code=_s(room.get("Name")) if room else resource_id,
account_id=_s((reservation or {}).get("AccountingAccountId") or customer_id),
guest_id=customer_id,
guest_name=guest_name,
)
def _mews_charge_account(reception: data.Recepcia, preparation: data.HotelChargePreparation) -> dict:
target = preparation.target
if not target:
raise HotelServiceError("Hotelovy ucet nema ciel.")
account_id = _s(target.account_id or target.guest_id)
if not account_id:
raise HotelServiceError("Mews ciel nema AccountId.")
currency = _mews_currency_code(reception, preparation)
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)
unit_amount = {
"Currency": currency,
"GrossValue": float(_fmt_amount(unit_price)),
}
if _s(line.tax_code):
unit_amount["TaxCodes"] = [_s(line.tax_code)]
payload = _mews_payload(
reception,
AccountId=account_id,
ServiceId=_s(line.raster_id),
Notes=f"Z POS systemu: {_s(preparation.receipt_number)}",
Items=[{
"Name": _s(line.description) or f"Z POS systemu: {_s(preparation.receipt_number)}",
"UnitCount": abs(float(qty)),
"UnitAmount": unit_amount,
}],
)
response = _mews_post(reception, "/orders/add", payload)
if isinstance(response, dict) and response.get("message"):
raise HotelServiceError(str(response["message"]))
return {"ok": True, "message": "OK"}
def _mews_transfer_cash(reception: data.Recepcia, payload: dict) -> dict:
request_payload = payload.get("mews_payload") or payload.get("payload")
if not request_payload:
raise HotelServiceError("Uzavierkovy prenos do Mews nema pripravene mews_payload.")
response = _mews_post(reception, "/outletBills/add", request_payload)
if isinstance(response, dict) and response.get("message"):
raise HotelServiceError(str(response["message"]))
return {"ok": True, "message": "OK", "response": response}
def _protel_base(reception: data.Recepcia) -> str:
return _append_port(_host_with_scheme(reception.hor_ip, "http"), reception.hor_port)
def _protel_request(reception: data.Recepcia, endpoint: str, xml_body: str) -> str:
transaction = str(int(time.time() * 1000))
return _request_text(
"POST",
f"{_protel_base(reception)}{endpoint}",
auth=HTTPBasicAuth(_s(reception.hor_meno), _s(reception.hor_heslo)),
data_body=xml_body,
headers={"Transaction": transaction},
verify=False,
)
def _protel_check_room(reception: data.Recepcia, *, room_code: str = "", track2: str = "") -> list[data.HotelGuest]:
if room_code:
body = f"<Body><Room>{html.escape(_s(room_code))}</Room></Body>"
xml = _protel_request(reception, "/FindReservationByRoom", body)
else:
body = f"<Body><Key>{html.escape(_s(track2))}</Key></Body>"
xml = _protel_request(reception, "/FindReservationByKey", body)
reservations = _parse_xml(xml)
guests = []
for reservation in reservations:
values = {child.tag.lower(): _s(child.text) for child in reservation}
if values.get("errortext"):
raise HotelServiceError(values["errortext"])
name = " ".join(x for x in (values.get("firstname", ""), values.get("lastname", "")) if x).strip()
guest_id = values.get("resno", "")
room = values.get("room", room_code)
if guest_id or name:
guests.append(data.HotelGuest(
id=guest_id,
guest_name=name,
room_id=room,
room_code=room,
checkin_date=_fmt_date(values.get("arrival", "")),
checkout_date=_fmt_date(values.get("departure", "")),
))
if not guests:
raise HotelServiceError("Na izbe sa nenasiel host.")
return guests
def _protel_guests(reception: data.Recepcia, *, room_code: str) -> list[data.HotelGuest]:
if not _s(room_code):
raise HotelServiceError("Cislo izby je povinne.")
return _protel_check_room(reception, room_code=room_code)
def _protel_card(reception: data.Recepcia, card_code: str) -> data.HotelCardResult:
guests = _protel_check_room(reception, track2=card_code)
guest = guests[0]
return data.HotelCardResult(
room_id=guest.room_id,
room_code=guest.room_code,
account_id=guest.account_id,
guest_id=guest.id,
guest_name=guest.guest_name,
)
def _protel_charge_xml(params: dict, preparation: data.HotelChargePreparation) -> str:
target = preparation.target
if not target:
raise HotelServiceError("Hotelovy ucet nema ciel.")
creation = dt.datetime.now().strftime("%Y%m%d%H%M%S")
outlet = _s(params.get("protel_outlet") if params else "") or _s(preparation.id_kas) or "1"
parts = [
"<Body>",
f"<Invoice>{html.escape(_s(preparation.receipt_number))}</Invoice>",
f"<Outlet>{html.escape(outlet)}</Outlet>",
"<User>1</User>",
f"<Creation>{creation}</Creation>",
]
total = 0.0
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)
amount = float(_fmt_amount(float(unit_price) * float(qty)))
total += amount
parts.extend([
"<Item>",
"<Type>Revenue</Type>",
f"<Productgroup>{html.escape(_s(line.raster_id))}</Productgroup>",
f"<Quantity>{html.escape(_fmt_qty(qty))}</Quantity>",
f"<SinglePrice>{html.escape(_fmt_amount(unit_price))}</SinglePrice>",
f"<TotalAmount>{html.escape(_fmt_amount(amount))}</TotalAmount>",
f"<Text>{html.escape(_s(line.description))}</Text>",
"</Item>",
])
parts.extend([
"<Item>",
"<Type>Payment</Type>",
f"<ResNo>{html.escape(_s(target.guest_id or target.account_id))}</ResNo>",
f"<TotalAmount>{html.escape(_fmt_amount(total))}</TotalAmount>",
"</Item>",
"</Body>",
])
return "".join(parts)
def _protel_charge_account(reception: data.Recepcia, params: dict, preparation: data.HotelChargePreparation) -> dict:
xml = _protel_charge_xml(params or {}, preparation)
response = _protel_request(reception, "/CloseInvoice", xml)
root = _parse_xml(response)
if root.tag.lower() == "errortext":
raise HotelServiceError(_s(root.text) or "Protel vratil chybu.")
return {"ok": True, "message": "OK"}