05/07 Phase 3: initial codes

This commit is contained in:
2026-05-07 12:17:19 -04:00
parent 6e5518c103
commit ce462c57b2
107 changed files with 6365 additions and 208 deletions
+122 -2
View File
@@ -1,7 +1,127 @@
"""
app/tenant/reconciliation/routes.py
Phase 3+ implementation.
End-of-day reconciliation: close day, cash count, variance calculation.
"""
from flask import Blueprint
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