diff --git a/skills/paradiz/SKILL.md b/skills/paradiz/SKILL.md index b22b73e..9ee6d75 100644 --- a/skills/paradiz/SKILL.md +++ b/skills/paradiz/SKILL.md @@ -162,14 +162,21 @@ python3 {baseDir}/scripts/save_booking.py \ --room "Двухкомнатный номер" \ --total "54 600 ₽" \ --prepay "7 800 ₽" \ + --booking-status booked \ --notes "трансфер нужен" \ --notify ``` +Для ещё не оплаченной заявки использовать: +- `--booking-status preliminary` → в листе брони ставится запись **«Предварительное»**. + +Для оплаченной/подтверждённой заявки использовать: +- `--booking-status booked` → в листе брони ставится запись **«Забронировано»**. + Файлы бронирований: - текстовый журнал: `{baseDir}/data/bookings.txt` - структурированный журнал: `{baseDir}/data/bookings.jsonl` -- клиентский лист брони Word (.doc): `{baseDir}/data/listbroni/booking_<номер_брони>.doc` из шаблона `{baseDir}/data/shablon_broni.rtf` +- клиентский лист брони Word (.docx): `{baseDir}/data/listbroni/booking_<номер_брони>.docx` из шаблона `{baseDir}/data/shablon_broni.dotx` ### Этап 9 — Во время и после проживания diff --git a/skills/paradiz/scripts/save_booking.py b/skills/paradiz/scripts/save_booking.py index 38b1656..d3aecd8 100755 --- a/skills/paradiz/scripts/save_booking.py +++ b/skills/paradiz/scripts/save_booking.py @@ -6,6 +6,7 @@ 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: @@ -45,6 +46,31 @@ def rtf_escape(value: str) -> str: 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 @@ -60,36 +86,51 @@ def render_booking_rtf(template_path: Path, output_path: Path, data: dict) -> No if txt is None: txt = raw.decode("utf-8", errors="ignore") - # Сохраняем исходный заголовок шаблона RTF (кодировку задаёт сам шаблон) - - total_num = _extract_amount(data.get("total", "")) - prepay_num = _extract_amount(data.get("prepay", "")) - rest_num = max(0.0, total_num - prepay_num) - - repl = { - "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", ""), - } - + 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", + } + + 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 + zout.writestr(info, raw) + + def load_telegram_from_config(): cfg = Path('/home/openclaw/.openclaw/openclaw.json') if not cfg.exists(): @@ -134,8 +175,9 @@ def main(): 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.rtf", help="Путь к шаблону RTF") - p.add_argument("--doc-out", default="", help="Путь сохранения заполненного листа брони в формате Word (.doc)") + 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() @@ -148,9 +190,12 @@ def main(): 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, @@ -169,7 +214,7 @@ def main(): out.parent.mkdir(parents=True, exist_ok=True) human = ( - f"[{now}] БРОНЬ {booking_number}\n" + f"[{now}] БРОНЬ {booking_number} ({booking_status})\n" f"Гость: {args.guest}\n" f"Телефон: {args.phone}\n" f"Email: {args.email}\n" @@ -189,11 +234,20 @@ def main(): with jsonl.open("a", encoding="utf-8") as jf: jf.write(json.dumps(entry, ensure_ascii=False) + "\n") - # Генерируем клиентский лист брони из шаблона RTF и сохраняем как Word (.doc) + # Генерируем клиентский лист брони из шаблона (.dotx/.rtf) default_doc_dir = out.parent / "listbroni" - doc_out = args.doc_out.strip() if args.doc_out else str(default_doc_dir / f"booking_{booking_number}.doc") + 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: - render_booking_rtf(Path(args.template), Path(doc_out), entry) + 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 @@ -211,6 +265,7 @@ def main(): text = ( "📌 Новая бронь Парадиз\n" f"Номер брони: {booking_number}\n" + f"Статус: {booking_status}\n" f"Гость: {args.guest}\n" f"Телефон: {args.phone}\n" f"Email: {args.email}\n"