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 = ( '' "" f"{html.escape(_s(reception.hor_meno))}" f"{html.escape(_s(reception.hor_heslo))}" f"{html.escape(_s(reception.hor_prefix))}" "" ) 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"{html.escape(_s(res_id))}" if _s(res_id) else "" return ( '' "" f"{html.escape(_s(reception.hor_meno))}" f"{html.escape(_s(reception.hor_heslo))}" f"{html.escape(_s(reception.hor_prefix))}" "" f"{today.strftime('%Y-%m-%d')}" f"{tomorrow.strftime('%Y-%m-%d')}" "" "3" f"{res}" "" "" ) 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"{html.escape(_s(target.guest_id))}" items_xml.append( "" f"{html.escape(item.get('description') or ('POS ' + _s(preparation.receipt_number)))}" f"{html.escape(item['units'])}" f"{html.escape(item['unit_price'])}" f"{html.escape(item['vat'])}" f"{guest_xml}" f"{html.escape(_s(item['raster_id']))}" "" ) return ( '' "" f"{html.escape(_s(reception.hor_meno))}" f"{html.escape(_s(reception.hor_heslo))}" f"{html.escape(_s(target.account_id or target.guest_id))}" "" f"{currency_id}" "" "" + "".join(items_xml) + "" "" ) 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"{html.escape(_s(room_code))}" xml = _protel_request(reception, "/FindReservationByRoom", body) else: body = f"{html.escape(_s(track2))}" 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 = [ "", f"{html.escape(_s(preparation.receipt_number))}", f"{html.escape(outlet)}", "1", f"{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([ "", "Revenue", f"{html.escape(_s(line.raster_id))}", f"{html.escape(_fmt_qty(qty))}", f"{html.escape(_fmt_amount(unit_price))}", f"{html.escape(_fmt_amount(amount))}", f"{html.escape(_s(line.description))}", "", ]) parts.extend([ "", "Payment", f"{html.escape(_s(target.guest_id or target.account_id))}", f"{html.escape(_fmt_amount(total))}", "", "", ]) 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"}