05/07 Phase 3: initial codes
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user