307 lines
11 KiB
Python
Executable File
307 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
import os
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from urllib import parse, request
|
|
import re
|
|
import zipfile
|
|
|
|
|
|
def send_telegram(bot_token: str, chat_id: str, text: str) -> None:
|
|
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
|
payload = parse.urlencode({
|
|
"chat_id": chat_id,
|
|
"text": text,
|
|
"disable_web_page_preview": "true",
|
|
}).encode("utf-8")
|
|
req = request.Request(url, data=payload, method="POST")
|
|
with request.urlopen(req, timeout=15) as resp:
|
|
if resp.status != 200:
|
|
raise RuntimeError(f"Telegram HTTP {resp.status}")
|
|
|
|
|
|
def _extract_amount(s: str) -> float:
|
|
cleaned = re.sub(r"[^0-9,\.]", "", s or "").replace(",", ".")
|
|
if not cleaned:
|
|
return 0.0
|
|
try:
|
|
return float(cleaned)
|
|
except Exception:
|
|
return 0.0
|
|
|
|
|
|
def rtf_escape(value: str) -> str:
|
|
out = []
|
|
for ch in value:
|
|
code = ord(ch)
|
|
if ch in ['\\', '{', '}']:
|
|
out.append('\\' + ch)
|
|
elif code > 127:
|
|
signed = code if code < 32768 else code - 65536
|
|
out.append(f"\\u{signed}?")
|
|
else:
|
|
out.append(ch)
|
|
return ''.join(out)
|
|
|
|
|
|
def _build_repl(data: dict) -> dict:
|
|
total_num = _extract_amount(data.get("total", ""))
|
|
prepay_num = _extract_amount(data.get("prepay", ""))
|
|
rest_num = max(0.0, total_num - prepay_num)
|
|
status = data.get("booking_status", "Предварительное")
|
|
|
|
return {
|
|
"BKGNFIO": data.get("guest", ""),
|
|
"BKGNNUMBER": data.get("booking_number", ""),
|
|
"BKGNDATE": data.get("created_at", ""),
|
|
"BKGNBEGINDATE": data.get("checkin", ""),
|
|
"BKGNENDDATE": data.get("checkout", ""),
|
|
"BKGNCATEGORY": data.get("room", ""),
|
|
"BKGNNPEOPLE": str(data.get("guests", "")),
|
|
"BKGNCOSTFULL": data.get("total", ""),
|
|
"BKGNCOSTPAYFULL": data.get("prepay", ""),
|
|
"BKGNCOSTRESTFULL": f"{rest_num:,.0f} ₽".replace(",", " "),
|
|
"BKGNNUMDAYS": str(data.get("nights", "")),
|
|
"CICLSERVICENAME": data.get("room", ""),
|
|
"CICLNUMDAYS": str(data.get("nights", "")),
|
|
"CICLDAYDICOST": data.get("day_price", ""),
|
|
"BKGNSTATUS": status,
|
|
}
|
|
|
|
|
|
def render_booking_rtf(template_path: Path, output_path: Path, data: dict) -> None:
|
|
if not template_path.exists():
|
|
return
|
|
|
|
raw = template_path.read_bytes()
|
|
txt = None
|
|
for enc in ("utf-8", "cp1251", "utf-8-sig"):
|
|
try:
|
|
txt = raw.decode(enc)
|
|
break
|
|
except Exception:
|
|
continue
|
|
if txt is None:
|
|
txt = raw.decode("utf-8", errors="ignore")
|
|
|
|
repl = _build_repl(data)
|
|
for k, v in repl.items():
|
|
txt = txt.replace(k, rtf_escape(str(v)))
|
|
|
|
# Явная пометка статуса в теле документа
|
|
status = repl["BKGNSTATUS"]
|
|
txt = txt.replace("подтверждаем бронирование", f"оформляем {status} бронирование")
|
|
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text(txt, encoding="utf-8")
|
|
|
|
|
|
def render_booking_dotx(template_path: Path, output_path: Path, data: dict) -> None:
|
|
if not template_path.exists():
|
|
return
|
|
|
|
repl = _build_repl(data)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
xml_targets = {
|
|
"word/document.xml",
|
|
"word/header1.xml",
|
|
"word/header2.xml",
|
|
"word/footer1.xml",
|
|
"word/footer2.xml",
|
|
}
|
|
|
|
to_docx = output_path.suffix.lower() == '.docx'
|
|
|
|
with zipfile.ZipFile(template_path, 'r') as zin, zipfile.ZipFile(output_path, 'w', compression=zipfile.ZIP_DEFLATED) as zout:
|
|
for info in zin.infolist():
|
|
raw = zin.read(info.filename)
|
|
|
|
if info.filename in xml_targets:
|
|
try:
|
|
txt = raw.decode('utf-8')
|
|
for k, v in repl.items():
|
|
txt = txt.replace(k, str(v))
|
|
|
|
status = repl["BKGNSTATUS"]
|
|
txt = txt.replace("подтверждаем бронирование", f"оформляем {status} бронирование")
|
|
txt = txt.replace("подтверждаем бронирование,", f"оформляем {status} бронирование,")
|
|
raw = txt.encode('utf-8')
|
|
except Exception:
|
|
pass
|
|
|
|
if to_docx and info.filename == '[Content_Types].xml':
|
|
try:
|
|
txt = raw.decode('utf-8')
|
|
txt = txt.replace(
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'
|
|
)
|
|
raw = txt.encode('utf-8')
|
|
except Exception:
|
|
pass
|
|
|
|
zout.writestr(info, raw)
|
|
|
|
|
|
def load_telegram_from_config():
|
|
cfg = Path('/home/openclaw/.openclaw/openclaw.json')
|
|
if not cfg.exists():
|
|
return "", ""
|
|
try:
|
|
j = json.loads(cfg.read_text(encoding='utf-8'))
|
|
except Exception:
|
|
return "", ""
|
|
|
|
bot = ""
|
|
chat = ""
|
|
|
|
# 1) dedicated env for paradiz skill
|
|
try:
|
|
env = j.get('skills', {}).get('entries', {}).get('paradiz', {}).get('env', {})
|
|
bot = (env.get('PARADIZ_TG_BOT_TOKEN') or "").strip()
|
|
chat = str(env.get('PARADIZ_TG_CHAT_ID') or "").strip()
|
|
except Exception:
|
|
pass
|
|
|
|
# 2) fallback to channel token format
|
|
if not bot:
|
|
bt = str(j.get('channels', {}).get('telegram', {}).get('botToken', '')).strip()
|
|
if bt.startswith('https://api.telegram.org/bot'):
|
|
bt = bt.replace('https://api.telegram.org/bot', '', 1)
|
|
bot = bt
|
|
|
|
return bot, chat
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description="Сохранить бронь и отправить уведомление в Telegram")
|
|
p.add_argument("--guest", required=True, help="ФИО гостя")
|
|
p.add_argument("--phone", required=True, help="Телефон")
|
|
p.add_argument("--email", required=True, help="E-mail")
|
|
p.add_argument("--checkin", required=True, help="Дата заезда YYYY-MM-DD")
|
|
p.add_argument("--checkout", required=True, help="Дата выезда YYYY-MM-DD")
|
|
p.add_argument("--guests", required=True, type=int, help="Количество гостей")
|
|
p.add_argument("--room", required=True, help="Категория номера")
|
|
p.add_argument("--total", required=True, help="Итоговая сумма")
|
|
p.add_argument("--prepay", required=True, help="Сумма предоплаты")
|
|
p.add_argument("--notes", default="", help="Комментарий")
|
|
p.add_argument("--file", default="/home/openclaw/.openclaw/workspace/skills/paradiz/data/bookings.txt")
|
|
p.add_argument("--notify", action="store_true", help="Отправить Telegram-уведомление")
|
|
p.add_argument("--template", default="/home/openclaw/.openclaw/workspace/skills/paradiz/data/shablon_broni.dotx", help="Путь к шаблону брони (.dotx/.rtf)")
|
|
p.add_argument("--doc-out", default="", help="Путь сохранения заполненного листа брони (.docx/.doc)")
|
|
p.add_argument("--booking-status", choices=["preliminary", "booked"], default="preliminary", help="Статус: preliminary=Предварительное, booked=Забронировано")
|
|
args = p.parse_args()
|
|
|
|
dt_now = datetime.now()
|
|
now = dt_now.strftime("%Y-%m-%d %H:%M:%S")
|
|
booking_number = dt_now.strftime("PDZ-%Y%m%d-%H%M%S")
|
|
|
|
d1 = datetime.strptime(args.checkin, "%Y-%m-%d")
|
|
d2 = datetime.strptime(args.checkout, "%Y-%m-%d")
|
|
nights = max(0, (d2 - d1).days)
|
|
total_num = _extract_amount(args.total)
|
|
day_price = f"{(total_num / nights):,.0f} ₽".replace(",", " ") if nights else ""
|
|
|
|
booking_status = "Забронировано" if args.booking_status == "booked" else "Предварительное"
|
|
|
|
entry = {
|
|
"created_at": now,
|
|
"booking_number": booking_number,
|
|
"booking_status": booking_status,
|
|
"guest": args.guest,
|
|
"phone": args.phone,
|
|
"email": args.email,
|
|
"checkin": args.checkin,
|
|
"checkout": args.checkout,
|
|
"guests": args.guests,
|
|
"room": args.room,
|
|
"total": args.total,
|
|
"prepay": args.prepay,
|
|
"notes": args.notes,
|
|
"nights": nights,
|
|
"day_price": day_price,
|
|
}
|
|
|
|
out = Path(args.file)
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
human = (
|
|
f"[{now}] БРОНЬ {booking_number} ({booking_status})\n"
|
|
f"Гость: {args.guest}\n"
|
|
f"Телефон: {args.phone}\n"
|
|
f"Email: {args.email}\n"
|
|
f"Период: {args.checkin} → {args.checkout}\n"
|
|
f"Гостей: {args.guests}\n"
|
|
f"Номер: {args.room}\n"
|
|
f"Итого: {args.total}\n"
|
|
f"Предоплата: {args.prepay}\n"
|
|
f"Комментарий: {args.notes or '-'}\n"
|
|
f"---\n"
|
|
)
|
|
|
|
with out.open("a", encoding="utf-8") as f:
|
|
f.write(human)
|
|
|
|
jsonl = out.with_suffix(".jsonl")
|
|
with jsonl.open("a", encoding="utf-8") as jf:
|
|
jf.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
|
|
# Генерируем клиентский лист брони из шаблона (.dotx/.rtf)
|
|
default_doc_dir = out.parent / "listbroni"
|
|
template_path = Path(args.template)
|
|
if args.doc_out:
|
|
doc_out = args.doc_out.strip()
|
|
else:
|
|
ext = ".docx" if template_path.suffix.lower() == ".dotx" else ".doc"
|
|
doc_out = str(default_doc_dir / f"booking_{booking_number}{ext}")
|
|
|
|
try:
|
|
if template_path.suffix.lower() == ".dotx":
|
|
render_booking_dotx(template_path, Path(doc_out), entry)
|
|
else:
|
|
render_booking_rtf(template_path, Path(doc_out), entry)
|
|
except Exception:
|
|
pass
|
|
|
|
sent = False
|
|
err = None
|
|
if args.notify:
|
|
bot_token = os.getenv("PARADIZ_TG_BOT_TOKEN", "").strip()
|
|
chat_id = os.getenv("PARADIZ_TG_CHAT_ID", "").strip()
|
|
if not (bot_token and chat_id):
|
|
cfg_bot, cfg_chat = load_telegram_from_config()
|
|
bot_token = bot_token or cfg_bot
|
|
chat_id = chat_id or cfg_chat
|
|
|
|
if bot_token and chat_id:
|
|
text = (
|
|
"📌 Новая бронь Парадиз\n"
|
|
f"Номер брони: {booking_number}\n"
|
|
f"Статус: {booking_status}\n"
|
|
f"Гость: {args.guest}\n"
|
|
f"Телефон: {args.phone}\n"
|
|
f"Email: {args.email}\n"
|
|
f"Период: {args.checkin} → {args.checkout}\n"
|
|
f"Гостей: {args.guests}\n"
|
|
f"Номер: {args.room}\n"
|
|
f"Итого: {args.total}\n"
|
|
f"Предоплата: {args.prepay}\n"
|
|
f"Комментарий: {args.notes or '-'}"
|
|
)
|
|
try:
|
|
send_telegram(bot_token, chat_id, text)
|
|
sent = True
|
|
except Exception as e:
|
|
err = str(e)
|
|
else:
|
|
err = "PARADIZ_TG_BOT_TOKEN / PARADIZ_TG_CHAT_ID не заданы"
|
|
|
|
print(json.dumps({"ok": True, "booking_number": booking_number, "saved": str(out), "jsonl": str(jsonl), "doc": str(doc_out), "telegram_sent": sent, "telegram_error": err}, ensure_ascii=False))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|