""" app/tenant/reconciliation/routes.py End-of-day reconciliation: close day, cash count, variance calculation. """ import logging from datetime import datetime, timezone, timedelta, date from decimal import Decimal from flask import Blueprint, render_template, redirect, url_for, flash, request, g from flask_login import login_required from app.extensions import db from app.models.salon import DailyReconciliation, Transaction from app.decorators import require_role, demo_readonly from app.tenant.utils import log_tenant_action logger = logging.getLogger(__name__) reconciliation_bp = Blueprint("reconciliation", __name__, url_prefix="/reconciliation") @reconciliation_bp.route("/") @login_required @require_role("tenant_admin", "tenant_manager") def index(): records = DailyReconciliation.query.filter_by( tenant_id=g.tenant.id, location_id=g.location.id ).order_by(DailyReconciliation.date.desc()).limit(30).all() return render_template("tenant/reconciliation/index.html", records=records) @reconciliation_bp.route("/close", methods=["GET", "POST"]) @login_required @require_role("tenant_admin", "tenant_manager") @demo_readonly def close_day(): today = date.today() # Check if already closed for today existing = DailyReconciliation.query.filter_by( tenant_id=g.tenant.id, location_id=g.location.id, date=today ).first() if existing and existing.closed_at: flash("Today has already been reconciled.", "info") return redirect(url_for("reconciliation.index")) # Compute expected totals from today's transactions day_start = datetime.combine(today, datetime.min.time()).replace(tzinfo=timezone.utc) day_end = day_start + timedelta(days=1) txns = Transaction.query.filter_by( tenant_id=g.tenant.id, location_id=g.location.id ).filter( Transaction.created_at >= day_start, Transaction.created_at < day_end, Transaction.voided_at.is_(None), ).all() total_cash = sum( Decimal(str(t.total)) for t in txns if t.payment_method == "cash" ) total_app = sum( Decimal(str(t.total)) for t in txns if t.payment_method in ("zelle", "venmo", "cashapp", "other") ) total_tips = sum(Decimal(str(t.tip_amount)) for t in txns) total_gc = sum(Decimal(str(t.gift_card_amount)) for t in txns) expected_cash = total_cash # Starting float not tracked in MVP if request.method == "POST": try: actual_cash = Decimal(request.form.get("actual_cash", "0")) except Exception: return render_template("tenant/reconciliation/close.html", today=today, total_cash=float(total_cash), total_app=float(total_app), total_tips=float(total_tips), total_gc=float(total_gc), expected_cash=float(expected_cash), error="Invalid cash amount.") variance = actual_cash - expected_cash notes = request.form.get("notes", "").strip() or None if existing: existing.actual_cash_counted = float(actual_cash) existing.variance = float(variance) existing.closed_by = _user_db_id() existing.closed_at = datetime.now(timezone.utc) existing.notes = notes rec = existing else: rec = DailyReconciliation( tenant_id=g.tenant.id, location_id=g.location.id, date=today, total_cash=float(total_cash), total_app_payments=float(total_app), total_tips=float(total_tips), total_gift_card_redemptions=float(total_gc), expected_cash_in_drawer=float(expected_cash), actual_cash_counted=float(actual_cash), variance=float(variance), closed_by=_user_db_id(), closed_at=datetime.now(timezone.utc), notes=notes, ) db.session.add(rec) log_tenant_action("reconciliation.close", "reconciliation", None, {"date": str(today), "variance": float(variance)}) db.session.commit() flash(f"Day closed. Variance: ${float(variance):.2f}", "success") return redirect(url_for("reconciliation.index")) return render_template("tenant/reconciliation/close.html", today=today, total_cash=float(total_cash), total_app=float(total_app), total_tips=float(total_tips), total_gc=float(total_gc), expected_cash=float(expected_cash)) def _user_db_id(): from flask_login import current_user try: uid_str = current_user.get_id() return int(uid_str.split(":")[1]) if uid_str and ":" in uid_str else None except Exception: return None