Venus / src /services /closing_service.py
Amir
Venus Hotel Front Desk – initial HF deploy
9d9d2a1
import os
from typing import Dict, List, Tuple
from src.db.connection import get_conn
from src.db.closing import is_closed, save_closing, get_closing
from src.db.audit import recent as recent_audit
from src.utils.time_utils import utc_now_iso, muscat_now, business_day_for_close
from src.reports.daily_closing_pdf import build_daily_closing_pdf
def _compute_closing_payload(conn, business_date: str, closed_by: str) -> dict:
# Occupancy counts (current snapshot)
rooms = conn.execute("SELECT status, COUNT(*) AS c FROM rooms GROUP BY status").fetchall()
occupancy = {r["status"]: int(r["c"]) for r in rooms}
# Room revenue summary for business_date:
# We include:
# - payments recorded on that business_date (based on changed_at_utc -> we do not have local-date indexing)
# Practical approach: summarize by reservations that have check_in_date or check_out_date = business_date, plus all payments.
# For a hotel desk, you mainly want "today's desk movements". We'll do:
# - total payments where reservation check_in_date <= business_date <= check_out_date (active/closed)
# - outstanding balances for those reservations.
res_rows = conn.execute(
"""
SELECT id, room_no, guest_name, total_ro, status
FROM reservations
WHERE check_in_date <= ? AND check_out_date >= ?
""",
(business_date, business_date),
).fetchall()
res_ids = [r["id"] for r in res_rows]
paid_total = 0.0
if res_ids:
q = "SELECT COALESCE(SUM(amount_ro),0) AS s FROM payments WHERE reservation_id IN (%s)" % ",".join(["?"] * len(res_ids))
paid_total = float(conn.execute(q, res_ids).fetchone()["s"])
total_room_value = sum(float(r["total_ro"]) for r in res_rows) if res_rows else 0.0
balance = max(0.0, total_room_value - paid_total)
# Other income / expenses for business_date
other_income = float(conn.execute(
"SELECT COALESCE(SUM(amount_ro),0) AS s FROM income_expense WHERE entry_date=? AND kind='income'",
(business_date,),
).fetchone()["s"])
expenses = float(conn.execute(
"SELECT COALESCE(SUM(amount_ro),0) AS s FROM income_expense WHERE entry_date=? AND kind='expense'",
(business_date,),
).fetchone()["s"])
net = (paid_total + other_income) - expenses
# Active problems list
probs = conn.execute(
"""
SELECT room_no, problem_type, description
FROM room_problems
WHERE is_active=1
ORDER BY room_no
"""
).fetchall()
prob_lines = [f"{p['room_no']} - {p['problem_type']}" for p in probs]
# Audit highlights
audit = recent_audit(conn, limit=12)
audit_lines = [
f"{a['changed_at_utc']} | {a['entity']} {a.get('entity_id','')} | {a['action']} | by {a['changed_by']}"
for a in audit
]
summary_lines = [
f"Occupancy snapshot: free={occupancy.get('free',0)}, occupied={occupancy.get('occupied',0)}, need_cleaning={occupancy.get('need_cleaning',0)}, out_of_service={occupancy.get('out_of_service',0)}",
f"Room revenue (payments): {paid_total:.3f} RO",
f"Outstanding balances (approx.): {balance:.3f} RO",
f"Other income: {other_income:.3f} RO",
f"Expenses: {expenses:.3f} RO",
f"Net: {net:.3f} RO",
]
return {
"business_date": business_date,
"closed_by": closed_by,
"closed_at_utc": utc_now_iso(),
"summary_lines": summary_lines,
"active_problems": prob_lines,
"audit_lines": audit_lines,
}
def close_day(db_path: str, business_date: str, closed_by: str, note: str) -> Tuple[bytes, str]:
"""
Returns (pdf_bytes, saved_path)
"""
conn = get_conn(db_path)
try:
if is_closed(conn, business_date):
existing = get_closing(conn, business_date)
if existing and existing.get("pdf_bytes"):
return bytes(existing["pdf_bytes"]), existing.get("pdf_path") or ""
raise ValueError("Day already closed, but PDF not found.")
payload = _compute_closing_payload(conn, business_date, closed_by)
pdf_bytes = build_daily_closing_pdf(payload)
# Save path
base_dir = "/data/reports" if db_path.startswith("/data/") else "./data/reports"
os.makedirs(base_dir, exist_ok=True)
pdf_path = os.path.join(base_dir, f"closing_{business_date}.pdf")
with open(pdf_path, "wb") as f:
f.write(pdf_bytes)
save_closing(conn, business_date, closed_by, note, pdf_path, pdf_bytes)
return pdf_bytes, pdf_path
finally:
conn.close()
def compute_default_business_date_to_close() -> str:
# after 06:00, close yesterday; we always close yesterday when asked
return business_day_for_close(muscat_now())