1121 lines
42 KiB
Python
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"}
|