from __future__ import annotations from decimal import Decimal from flask import Blueprint, flash, g, redirect, render_template, request, url_for from app.auth import role_required from app.extensions import db from app.models import Account from app.services import ( build_new_household_draft, get_household_or_none, is_head_name_taken, list_enabled_options, list_households, normalize_search_term, parse_entry_form, serialize_admin_edit_snapshot, serialize_entry_edit_snapshot, write_audit_log, ) quick_entry_bp = Blueprint("quick_entry", __name__, url_prefix="/quick-entry") def _include_bride_side_param(source: object) -> bool: if not hasattr(source, "get"): return False value = getattr(source, "get")("include_bride_side", "") if not isinstance(value, str): return False return value.strip() == "1" def _quick_entry_search_url(*, search_term: str = "", include_bride_side: bool = False) -> str: params: dict[str, str] = {} normalized_search_term = search_term.strip() if normalized_search_term: params["q"] = normalized_search_term if include_bride_side: params["include_bride_side"] = "1" return url_for("quick_entry.index", **params) def _option_id_by_code(options: list[object], code: str) -> int | None: for option in options: if getattr(option, "option_code", None) == code: option_id = getattr(option, "id", None) if isinstance(option_id, int): return option_id return None def _quick_entry_index_context() -> dict[str, object]: search_term = request.args.get("q", "").strip() include_bride_side = _include_bride_side_param(request.args) households = list_households(search_term=search_term, limit=20) if normalize_search_term(search_term) else [] if not include_bride_side: households = [household for household in households if household.side != "bride_side"] households = sorted( households, key=lambda household: household.total_gift_amount > Decimal("0.00"), ) return { "search_term": search_term, "include_bride_side": include_bride_side, "households": households, } def _is_partial_request(expected_partial: str) -> bool: requested_partial = request.args.get("partial", "").strip() header_partial = request.headers.get("X-HW-Partial", "").strip() return requested_partial == expected_partial or header_partial == expected_partial @quick_entry_bp.get("/") @role_required("admin", "editor", "entry_only", "quick_editor") def index() -> str: context = _quick_entry_index_context() if _is_partial_request("search-results"): return render_template("quick_entry/_search_results.html", **context) return render_template("quick_entry/index.html", **context) @quick_entry_bp.get("/households/new") @role_required("admin", "editor", "entry_only", "quick_editor") def new_household() -> str: search_term = request.args.get("q", "").strip() include_bride_side = _include_bride_side_param(request.args) gift_method_options = list_enabled_options("gift_method") gift_scene_options = list_enabled_options("gift_scene") return render_template( "quick_entry/new.html", search_term=search_term, include_bride_side=include_bride_side, default_gift_method_id=_option_id_by_code(gift_method_options, "cash"), default_gift_scene_id=_option_id_by_code(gift_scene_options, "wedding_day"), ) @quick_entry_bp.get("/households//edit") @role_required("admin", "editor", "entry_only", "quick_editor") def edit_household(household_id: int): household = get_household_or_none(household_id) if household is None: flash("未找到要修改的户信息。", "error") return redirect(url_for("quick_entry.index")) gift_method_options = list_enabled_options("gift_method") gift_scene_options = list_enabled_options("gift_scene") context = { "household": household, "search_term": request.args.get("q", "").strip(), "include_bride_side": _include_bride_side_param(request.args), "gift_method_options": gift_method_options, "gift_scene_options": gift_scene_options, "default_gift_method_id": _option_id_by_code(gift_method_options, "cash"), "default_gift_scene_id": _option_id_by_code(gift_scene_options, "wedding_day"), } if _is_partial_request("edit-modal"): return render_template("quick_entry/_edit_modal.html", **context) return render_template("quick_entry/edit.html", **context) @quick_entry_bp.post("/households//edit") @role_required("admin", "editor", "entry_only", "quick_editor") def update_household(household_id: int): household = get_household_or_none(household_id) if household is None: flash("未找到要修改的户信息。", "error") return redirect(url_for("quick_entry.index")) search_term = request.form.get("q", "").strip() include_bride_side = _include_bride_side_param(request.form) redirect_to_search = _quick_entry_search_url(search_term=search_term, include_bride_side=include_bride_side) redirect_to_self = url_for( "quick_entry.edit_household", household_id=household_id, q=search_term, **({"include_bride_side": "1"} if include_bride_side else {}), ) submitted_version = request.form.get("version", "").strip() try: version = int(submitted_version) except ValueError: flash("表单版本无效,请重新打开该户信息后再试。", "error") return redirect(redirect_to_self) if version != household.version: flash("该户信息已被其他人更新,请刷新后重新确认再保存。", "warning") return redirect(redirect_to_self) gift_method_options = list_enabled_options("gift_method") gift_scene_options = list_enabled_options("gift_scene") payload, errors = parse_entry_form( dict(request.form), valid_method_ids={option.id for option in gift_method_options}, valid_scene_ids={option.id for option in gift_scene_options}, ) head_name = request.form.get("head_name", "").strip() if not head_name: errors.append("户主姓名不能为空。") elif is_head_name_taken(head_name, excluding_household_id=household_id): errors.append("该户主姓名已存在,请换一个名字或直接录入现有户。") if errors: for error in errors: flash(error, "error") return redirect(redirect_to_self) before_snapshot = serialize_entry_edit_snapshot(household) household.head_name = head_name for field_name, value in payload.items(): setattr(household, field_name, value) current_account = g.current_account if isinstance(current_account, Account): household.updated_by = current_account.id if serialize_entry_edit_snapshot(household) == before_snapshot: flash("没有检测到需要保存的变化。", "warning") db.session.rollback() return redirect(redirect_to_search) household.version += 1 after_snapshot = serialize_entry_edit_snapshot(household) write_audit_log( action_type="update_household_entry", target_type="household", actor=current_account if isinstance(current_account, Account) else None, target_id=household.id, target_display_name=household.head_name, before_data=before_snapshot, after_data=after_snapshot, commit=False, ) db.session.commit() flash(f"已保存 {household.head_name} 的礼金信息。", "success") return redirect(redirect_to_search) @quick_entry_bp.post("/households") @role_required("admin", "editor", "entry_only", "quick_editor") def create_household(): search_term = request.form.get("q", "").strip() or request.form.get("head_name", "").strip() include_bride_side = _include_bride_side_param(request.form) redirect_to_search = _quick_entry_search_url(search_term=search_term, include_bride_side=include_bride_side) redirect_to_self = url_for( "quick_entry.new_household", q=search_term, **({"include_bride_side": "1"} if include_bride_side else {}), ) gift_method_options = list_enabled_options("gift_method") gift_scene_options = list_enabled_options("gift_scene") payload, errors = parse_entry_form( dict(request.form), valid_method_ids={option.id for option in gift_method_options}, valid_scene_ids={option.id for option in gift_scene_options}, ) if errors: for error in errors: flash(error, "error") return redirect(redirect_to_self) head_name = request.form.get("head_name", "").strip() if not head_name: flash("户主姓名不能为空。", "error") return redirect(redirect_to_self) if is_head_name_taken(head_name): flash("该户主姓名已存在,请先搜索现有户后直接录入礼金。", "error") return redirect(redirect_to_self) household = build_new_household_draft() household.head_name = head_name household.note = request.form.get("note", "").strip() or None for field_name, value in payload.items(): setattr(household, field_name, value) if household.actual_attendee_count > 0 and household.attendance_status == "pending": household.attendance_status = "attending" current_account = g.current_account if isinstance(current_account, Account): household.created_by = current_account.id household.updated_by = current_account.id db.session.add(household) db.session.flush() write_audit_log( action_type="create_household_quick_entry", target_type="household", actor=current_account if isinstance(current_account, Account) else None, target_id=household.id, target_display_name=household.head_name, after_data=serialize_admin_edit_snapshot(household), commit=False, ) db.session.commit() flash(f"已快速创建 {household.head_name} 并记录礼金。", "success") return redirect(redirect_to_search)