You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
742 lines
29 KiB
742 lines
29 KiB
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
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 (
|
|
ATTENDANCE_STATUS_OPTIONS,
|
|
DEFAULT_HOMEPAGE_PER_PAGE,
|
|
HOMEPAGE_PER_PAGE_OPTIONS,
|
|
INVITE_STATUS_OPTIONS,
|
|
SIDE_OPTIONS,
|
|
AGE_GROUP_LABELS,
|
|
AGE_GROUP_OPTIONS,
|
|
GIFT_RECORD_TYPE_LABELS,
|
|
GIFT_RECORD_TYPE_OPTIONS,
|
|
GENDER_LABELS,
|
|
GENDER_OPTIONS,
|
|
RELATION_TO_HEAD_OPTIONS,
|
|
SORT_FIELD_LABELS,
|
|
SORT_FIELD_OPTIONS,
|
|
SORT_ORDER_OPTIONS,
|
|
build_new_gift_record_draft,
|
|
build_new_household_draft,
|
|
build_new_member_draft,
|
|
ensure_relation_category_option,
|
|
ensure_tag_options,
|
|
get_gift_record_or_none,
|
|
get_filtered_homepage_stats,
|
|
get_homepage_stats,
|
|
get_household_or_none,
|
|
get_member_or_none,
|
|
gift_record_type_label,
|
|
household_has_active_gift_records,
|
|
is_head_name_taken,
|
|
is_household_code_taken,
|
|
list_enabled_options,
|
|
list_gift_records,
|
|
list_members,
|
|
paginate_households_for_homepage,
|
|
parse_admin_form,
|
|
parse_new_relation_category_label,
|
|
parse_new_tag_labels,
|
|
parse_gift_record_form,
|
|
parse_member_form,
|
|
recalculate_household_gift_summary,
|
|
serialize_admin_edit_snapshot,
|
|
serialize_gift_record_snapshot,
|
|
serialize_member_snapshot,
|
|
write_audit_log,
|
|
)
|
|
|
|
main_bp = Blueprint("main", __name__)
|
|
|
|
|
|
def _admin_index_context() -> dict[str, object]:
|
|
search_term = request.args.get("q", "").strip()
|
|
side = request.args.get("side", "").strip()
|
|
invite_status = request.args.get("invite_status", "").strip()
|
|
attendance_status = request.args.get("attendance_status", "").strip()
|
|
tag_option_id = request.args.get("tag_option_id", type=int)
|
|
sort_by = request.args.get("sort_by", "updated_at").strip()
|
|
sort_order = request.args.get("sort_order", "desc").strip()
|
|
page = request.args.get("page", type=int) or 1
|
|
per_page = request.args.get("per_page", type=int) or DEFAULT_HOMEPAGE_PER_PAGE
|
|
|
|
if sort_by not in SORT_FIELD_OPTIONS:
|
|
sort_by = "updated_at"
|
|
if sort_order not in SORT_ORDER_OPTIONS:
|
|
sort_order = "desc"
|
|
if per_page not in HOMEPAGE_PER_PAGE_OPTIONS:
|
|
per_page = DEFAULT_HOMEPAGE_PER_PAGE
|
|
tag_options = list_enabled_options("tag")
|
|
if tag_option_id not in {option.id for option in tag_options}:
|
|
tag_option_id = None
|
|
|
|
paginated_households = paginate_households_for_homepage(
|
|
search_term=search_term,
|
|
side=side,
|
|
invite_status=invite_status,
|
|
attendance_status=attendance_status,
|
|
tag_option_id=tag_option_id,
|
|
sort_by=sort_by,
|
|
sort_order=sort_order,
|
|
page=page,
|
|
per_page=per_page,
|
|
)
|
|
stats = get_homepage_stats()
|
|
filtered_stats = get_filtered_homepage_stats(paginated_households.filtered_households)
|
|
return {
|
|
"households": paginated_households.items,
|
|
"search_term": search_term,
|
|
"side": side,
|
|
"invite_status": invite_status,
|
|
"attendance_status": attendance_status,
|
|
"tag_option_id": tag_option_id,
|
|
"tag_options": tag_options,
|
|
"sort_by": sort_by,
|
|
"sort_order": sort_order,
|
|
"page": paginated_households.page,
|
|
"per_page": paginated_households.per_page,
|
|
"per_page_options": HOMEPAGE_PER_PAGE_OPTIONS,
|
|
"total_pages": paginated_households.total_pages,
|
|
"has_prev": paginated_households.has_prev,
|
|
"has_next": paginated_households.has_next,
|
|
"page_start": paginated_households.page_start,
|
|
"page_end": paginated_households.page_end,
|
|
"side_options": SIDE_OPTIONS,
|
|
"invite_status_options": INVITE_STATUS_OPTIONS,
|
|
"attendance_status_options": ATTENDANCE_STATUS_OPTIONS,
|
|
"sort_field_options": SORT_FIELD_OPTIONS,
|
|
"sort_field_labels": SORT_FIELD_LABELS,
|
|
"stats": stats,
|
|
"filtered_stats": filtered_stats,
|
|
"filtered_count": paginated_households.filtered_count,
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
def _admin_index_redirect_from_values(values) -> str:
|
|
params: dict[str, object] = {}
|
|
for key in ("q", "side", "invite_status", "attendance_status", "sort_by", "sort_order"):
|
|
raw_value = values.get(key, "")
|
|
normalized = raw_value.strip() if isinstance(raw_value, str) else raw_value
|
|
if normalized:
|
|
params[key] = normalized
|
|
|
|
for key in ("tag_option_id", "page", "per_page"):
|
|
raw_value = values.get(key, "")
|
|
normalized = raw_value.strip() if isinstance(raw_value, str) else raw_value
|
|
if normalized not in ("", None):
|
|
params[key] = normalized
|
|
|
|
return url_for("main.index", **params)
|
|
|
|
|
|
@main_bp.get("/")
|
|
@role_required("admin", "editor")
|
|
def index() -> str:
|
|
context = _admin_index_context()
|
|
if _is_partial_request("results"):
|
|
return render_template("main/_household_results.html", **context)
|
|
return render_template("main/index.html", **context)
|
|
|
|
|
|
@main_bp.get("/households/new")
|
|
@role_required("admin", "editor")
|
|
def new_household() -> str:
|
|
return render_template(
|
|
"main/household_edit.html",
|
|
household=build_new_household_draft(),
|
|
gift_records=[],
|
|
relation_category_options=list_enabled_options("relation_category"),
|
|
relation_detail_options=list_enabled_options("relation_detail"),
|
|
tag_options=list_enabled_options("tag"),
|
|
gift_method_options=list_enabled_options("gift_method"),
|
|
gift_scene_options=list_enabled_options("gift_scene"),
|
|
is_create_mode=True,
|
|
form_title="新增一户",
|
|
form_intro="只需填写户主姓名即可创建,户编码会自动生成;创建成功后会自动进入编辑页继续补充。",
|
|
submit_label="创建户信息",
|
|
)
|
|
|
|
|
|
@main_bp.post("/households")
|
|
@role_required("admin", "editor")
|
|
def create_household():
|
|
redirect_target = url_for("main.new_household")
|
|
relation_category_options = list_enabled_options("relation_category")
|
|
tag_options = list_enabled_options("tag")
|
|
gift_method_options = list_enabled_options("gift_method")
|
|
gift_scene_options = list_enabled_options("gift_scene")
|
|
payload, errors = parse_admin_form(
|
|
dict(request.form),
|
|
raw_tag_option_ids=request.form.getlist("tag_option_ids_json"),
|
|
valid_relation_category_ids={option.id for option in relation_category_options},
|
|
relation_detail_parent_map={},
|
|
valid_tag_ids={option.id for option in tag_options},
|
|
valid_method_ids={option.id for option in gift_method_options},
|
|
valid_scene_ids={option.id for option in gift_scene_options},
|
|
require_household_code=False,
|
|
)
|
|
if errors:
|
|
for error in errors:
|
|
flash(error, "error")
|
|
return redirect(redirect_target)
|
|
|
|
new_relation_category_label, relation_category_errors = parse_new_relation_category_label(
|
|
request.form.get("new_relation_category_label", ""),
|
|
)
|
|
if relation_category_errors:
|
|
for error in relation_category_errors:
|
|
flash(error, "error")
|
|
return redirect(redirect_target)
|
|
|
|
new_tag_labels, tag_label_errors = parse_new_tag_labels(request.form.get("new_tag_labels", ""))
|
|
if tag_label_errors:
|
|
for error in tag_label_errors:
|
|
flash(error, "error")
|
|
return redirect(redirect_target)
|
|
|
|
head_name = str(payload["head_name"])
|
|
if is_head_name_taken(head_name):
|
|
flash("户主姓名已存在,请修改后重试。", "error")
|
|
return redirect(redirect_target)
|
|
|
|
household_code = payload.get("household_code")
|
|
normalized_household_code = str(household_code) if household_code else None
|
|
if normalized_household_code and is_household_code_taken(normalized_household_code):
|
|
flash("户编码已存在,请换一个新的户编码。", "error")
|
|
return redirect(redirect_target)
|
|
|
|
current_account = g.current_account
|
|
created_relation_category = ensure_relation_category_option(
|
|
new_relation_category_label,
|
|
actor_id=current_account.id if isinstance(current_account, Account) else None,
|
|
)
|
|
if created_relation_category is not None:
|
|
payload["relation_category_option_id"] = created_relation_category.id
|
|
payload["relation_detail_option_id"] = None
|
|
|
|
created_tag_options = ensure_tag_options(
|
|
new_tag_labels,
|
|
actor_id=current_account.id if isinstance(current_account, Account) else None,
|
|
)
|
|
if created_tag_options:
|
|
existing_ids = set(payload["tag_option_ids_json"]) if isinstance(payload["tag_option_ids_json"], list) else set()
|
|
payload["tag_option_ids_json"] = sorted(existing_ids | {option.id for option in created_tag_options})
|
|
|
|
household = build_new_household_draft(household_code=normalized_household_code)
|
|
for field_name, value in payload.items():
|
|
if field_name == "household_code" and value is None:
|
|
continue
|
|
setattr(household, field_name, value)
|
|
|
|
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",
|
|
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}({household.household_code})。", "success")
|
|
return redirect(url_for("main.edit_household", household_id=household.id))
|
|
|
|
|
|
@main_bp.get("/households/<int:household_id>/edit")
|
|
@role_required("admin", "editor")
|
|
def edit_household(household_id: int):
|
|
household = get_household_or_none(household_id)
|
|
if household is None:
|
|
flash("未找到要编辑的户信息。", "error")
|
|
return redirect(url_for("main.index"))
|
|
|
|
members = list_members(household_id)
|
|
gift_records = list_gift_records(household_id)
|
|
new_gift_record = build_new_gift_record_draft(household_id)
|
|
|
|
return render_template(
|
|
"main/household_edit.html",
|
|
household=household,
|
|
members=members,
|
|
gift_records=gift_records,
|
|
new_gift_record=new_gift_record,
|
|
new_member=build_new_member_draft(household_id),
|
|
relation_category_options=list_enabled_options("relation_category"),
|
|
relation_detail_options=list_enabled_options("relation_detail"),
|
|
tag_options=list_enabled_options("tag"),
|
|
gift_method_options=list_enabled_options("gift_method"),
|
|
gift_scene_options=list_enabled_options("gift_scene"),
|
|
gift_record_type_options=GIFT_RECORD_TYPE_OPTIONS,
|
|
gift_record_type_labels=GIFT_RECORD_TYPE_LABELS,
|
|
gift_record_type_label=gift_record_type_label,
|
|
gender_options=GENDER_OPTIONS,
|
|
gender_labels=GENDER_LABELS,
|
|
age_group_options=AGE_GROUP_OPTIONS,
|
|
age_group_labels=AGE_GROUP_LABELS,
|
|
relation_to_head_options=RELATION_TO_HEAD_OPTIONS,
|
|
)
|
|
|
|
|
|
@main_bp.post("/households/<int:household_id>")
|
|
@role_required("admin", "editor")
|
|
def update_household(household_id: int):
|
|
household = get_household_or_none(household_id)
|
|
if household is None:
|
|
flash("未找到要编辑的户信息。", "error")
|
|
return redirect(url_for("main.index"))
|
|
|
|
redirect_target = url_for("main.edit_household", household_id=household_id)
|
|
submitted_version = request.form.get("version", "").strip()
|
|
try:
|
|
version = int(submitted_version)
|
|
except ValueError:
|
|
flash("表单版本无效,请重新打开该户信息后再试。", "error")
|
|
return redirect(redirect_target)
|
|
|
|
if version != household.version:
|
|
flash("该户信息已被其他人更新,请刷新后重新确认再保存。", "warning")
|
|
return redirect(redirect_target)
|
|
|
|
relation_category_options = list_enabled_options("relation_category")
|
|
tag_options = list_enabled_options("tag")
|
|
gift_method_options = list_enabled_options("gift_method")
|
|
gift_scene_options = list_enabled_options("gift_scene")
|
|
payload, errors = parse_admin_form(
|
|
dict(request.form),
|
|
raw_tag_option_ids=request.form.getlist("tag_option_ids_json"),
|
|
valid_relation_category_ids={option.id for option in relation_category_options},
|
|
relation_detail_parent_map={},
|
|
valid_tag_ids={option.id for option in tag_options},
|
|
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_target)
|
|
|
|
new_relation_category_label, relation_category_errors = parse_new_relation_category_label(
|
|
request.form.get("new_relation_category_label", ""),
|
|
)
|
|
if relation_category_errors:
|
|
for error in relation_category_errors:
|
|
flash(error, "error")
|
|
return redirect(redirect_target)
|
|
|
|
new_tag_labels, tag_label_errors = parse_new_tag_labels(request.form.get("new_tag_labels", ""))
|
|
if tag_label_errors:
|
|
for error in tag_label_errors:
|
|
flash(error, "error")
|
|
return redirect(redirect_target)
|
|
|
|
if household_has_active_gift_records(household_id):
|
|
payload.pop("total_gift_amount", None)
|
|
payload.pop("gift_method_option_id", None)
|
|
payload.pop("gift_scene_option_id", None)
|
|
|
|
head_name = str(payload["head_name"])
|
|
if is_head_name_taken(head_name, excluding_household_id=household_id):
|
|
flash("户主姓名已存在,请修改后重试。", "error")
|
|
return redirect(redirect_target)
|
|
|
|
household_code = payload.get("household_code")
|
|
normalized_household_code = str(household_code) if household_code else None
|
|
if normalized_household_code and is_household_code_taken(normalized_household_code, excluding_household_id=household_id):
|
|
flash("户编码已存在,请换一个新的户编码。", "error")
|
|
return redirect(redirect_target)
|
|
|
|
before_snapshot = serialize_admin_edit_snapshot(household)
|
|
current_account = g.current_account
|
|
created_relation_category = ensure_relation_category_option(
|
|
new_relation_category_label,
|
|
actor_id=current_account.id if isinstance(current_account, Account) else None,
|
|
)
|
|
if created_relation_category is not None:
|
|
payload["relation_category_option_id"] = created_relation_category.id
|
|
payload["relation_detail_option_id"] = None
|
|
|
|
created_tag_options = ensure_tag_options(
|
|
new_tag_labels,
|
|
actor_id=current_account.id if isinstance(current_account, Account) else None,
|
|
)
|
|
if created_tag_options:
|
|
existing_ids = set(payload["tag_option_ids_json"]) if isinstance(payload["tag_option_ids_json"], list) else set()
|
|
payload["tag_option_ids_json"] = sorted(existing_ids | {option.id for option in created_tag_options})
|
|
|
|
for field_name, value in payload.items():
|
|
setattr(household, field_name, value)
|
|
|
|
if isinstance(current_account, Account):
|
|
household.updated_by = current_account.id
|
|
|
|
if household_has_active_gift_records(household_id):
|
|
recalculate_household_gift_summary(household)
|
|
|
|
if serialize_admin_edit_snapshot(household) == before_snapshot:
|
|
flash("没有检测到需要保存的变化。", "warning")
|
|
db.session.rollback()
|
|
return redirect(redirect_target)
|
|
|
|
household.version += 1
|
|
after_snapshot = serialize_admin_edit_snapshot(household)
|
|
write_audit_log(
|
|
action_type="update_household",
|
|
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_target)
|
|
|
|
|
|
@main_bp.post("/households/<int:household_id>/delete")
|
|
@role_required("admin", "editor")
|
|
def delete_household(household_id: int):
|
|
household = get_household_or_none(household_id)
|
|
if household is None:
|
|
flash("未找到要删除的户信息。", "error")
|
|
return redirect(url_for("main.index"))
|
|
|
|
redirect_target = _admin_index_redirect_from_values(request.form)
|
|
confirm = request.form.get("confirm", "").strip()
|
|
if confirm != "yes":
|
|
flash("请确认删除户操作。", "warning")
|
|
return redirect(redirect_target)
|
|
|
|
before_snapshot = serialize_admin_edit_snapshot(household)
|
|
current_account = g.current_account
|
|
if isinstance(current_account, Account):
|
|
household.updated_by = current_account.id
|
|
|
|
household.deleted_at = datetime.now(UTC).replace(tzinfo=None)
|
|
household.version += 1
|
|
|
|
write_audit_log(
|
|
action_type="delete_household",
|
|
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=serialize_admin_edit_snapshot(household),
|
|
commit=False,
|
|
)
|
|
db.session.commit()
|
|
flash(f"已删除户 {household.head_name}。", "success")
|
|
return redirect(redirect_target)
|
|
|
|
|
|
@main_bp.post("/households/<int:household_id>/gift-records")
|
|
@role_required("admin", "editor")
|
|
def create_gift_record(household_id: int):
|
|
household = get_household_or_none(household_id)
|
|
if household is None:
|
|
flash("未找到要补录礼金明细的户信息。", "error")
|
|
return redirect(url_for("main.index"))
|
|
|
|
redirect_target = url_for("main.edit_household", household_id=household_id)
|
|
gift_method_options = list_enabled_options("gift_method")
|
|
gift_scene_options = list_enabled_options("gift_scene")
|
|
payload, errors = parse_gift_record_form(
|
|
dict(request.form),
|
|
household_id=household_id,
|
|
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_target)
|
|
|
|
record = build_new_gift_record_draft(household_id)
|
|
for field_name, value in payload.items():
|
|
setattr(record, field_name, value)
|
|
|
|
current_account = g.current_account
|
|
if isinstance(current_account, Account):
|
|
record.created_by = current_account.id
|
|
record.updated_by = current_account.id
|
|
household.updated_by = current_account.id
|
|
|
|
db.session.add(record)
|
|
db.session.flush()
|
|
recalculate_household_gift_summary(household)
|
|
household.version += 1
|
|
write_audit_log(
|
|
action_type="create_gift_record",
|
|
target_type="gift_record",
|
|
actor=current_account if isinstance(current_account, Account) else None,
|
|
target_id=record.id,
|
|
target_display_name=f"{household.head_name} - {gift_record_type_label(record.record_type)}",
|
|
after_data=serialize_gift_record_snapshot(record),
|
|
commit=False,
|
|
)
|
|
db.session.commit()
|
|
flash("已新增礼金明细。", "success")
|
|
return redirect(redirect_target)
|
|
|
|
|
|
@main_bp.post("/households/<int:household_id>/gift-records/<int:record_id>")
|
|
@role_required("admin", "editor")
|
|
def update_gift_record(household_id: int, record_id: int):
|
|
household = get_household_or_none(household_id)
|
|
if household is None:
|
|
flash("未找到要编辑礼金明细的户信息。", "error")
|
|
return redirect(url_for("main.index"))
|
|
|
|
record = get_gift_record_or_none(record_id)
|
|
if record is None or record.household_id != household_id:
|
|
flash("未找到要编辑的礼金明细。", "error")
|
|
return redirect(url_for("main.edit_household", household_id=household_id))
|
|
|
|
redirect_target = url_for("main.edit_household", household_id=household_id)
|
|
gift_method_options = list_enabled_options("gift_method")
|
|
gift_scene_options = list_enabled_options("gift_scene")
|
|
payload, errors = parse_gift_record_form(
|
|
dict(request.form),
|
|
household_id=household_id,
|
|
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_target)
|
|
|
|
before_snapshot = serialize_gift_record_snapshot(record)
|
|
for field_name, value in payload.items():
|
|
setattr(record, field_name, value)
|
|
|
|
current_account = g.current_account
|
|
if isinstance(current_account, Account):
|
|
record.updated_by = current_account.id
|
|
household.updated_by = current_account.id
|
|
|
|
after_snapshot = serialize_gift_record_snapshot(record)
|
|
if after_snapshot == before_snapshot:
|
|
flash("没有检测到需要保存的礼金明细变更。", "warning")
|
|
db.session.rollback()
|
|
return redirect(redirect_target)
|
|
|
|
recalculate_household_gift_summary(household)
|
|
household.version += 1
|
|
write_audit_log(
|
|
action_type="update_gift_record",
|
|
target_type="gift_record",
|
|
actor=current_account if isinstance(current_account, Account) else None,
|
|
target_id=record.id,
|
|
target_display_name=f"{household.head_name} - {gift_record_type_label(record.record_type)}",
|
|
before_data=before_snapshot,
|
|
after_data=after_snapshot,
|
|
commit=False,
|
|
)
|
|
db.session.commit()
|
|
flash("已更新礼金明细。", "success")
|
|
return redirect(redirect_target)
|
|
|
|
|
|
@main_bp.post("/households/<int:household_id>/gift-records/<int:record_id>/delete")
|
|
@role_required("admin", "editor")
|
|
def delete_gift_record(household_id: int, record_id: int):
|
|
household = get_household_or_none(household_id)
|
|
if household is None:
|
|
flash("未找到要删除礼金明细的户信息。", "error")
|
|
return redirect(url_for("main.index"))
|
|
|
|
record = get_gift_record_or_none(record_id)
|
|
if record is None or record.household_id != household_id:
|
|
flash("未找到要删除的礼金明细。", "error")
|
|
return redirect(url_for("main.edit_household", household_id=household_id))
|
|
|
|
redirect_target = url_for("main.edit_household", household_id=household_id)
|
|
confirm = request.form.get("confirm", "").strip()
|
|
if confirm != "yes":
|
|
flash("请确认删除礼金明细操作。", "warning")
|
|
return redirect(redirect_target)
|
|
|
|
before_snapshot = serialize_gift_record_snapshot(record)
|
|
current_account = g.current_account
|
|
if isinstance(current_account, Account):
|
|
record.updated_by = current_account.id
|
|
household.updated_by = current_account.id
|
|
|
|
record.deleted_at = datetime.now(UTC).replace(tzinfo=None)
|
|
recalculate_household_gift_summary(household)
|
|
household.version += 1
|
|
write_audit_log(
|
|
action_type="delete_gift_record",
|
|
target_type="gift_record",
|
|
actor=current_account if isinstance(current_account, Account) else None,
|
|
target_id=record.id,
|
|
target_display_name=f"{household.head_name} - {gift_record_type_label(record.record_type)}",
|
|
before_data=before_snapshot,
|
|
after_data=serialize_gift_record_snapshot(record),
|
|
commit=False,
|
|
)
|
|
db.session.commit()
|
|
flash("已删除礼金明细。", "success")
|
|
return redirect(redirect_target)
|
|
|
|
|
|
@main_bp.post("/households/<int:household_id>/members")
|
|
@role_required("admin", "editor")
|
|
def create_member(household_id: int):
|
|
"""创建新成员。"""
|
|
household = get_household_or_none(household_id)
|
|
if household is None:
|
|
flash("未找到要添加成员的户信息。", "error")
|
|
return redirect(url_for("main.index"))
|
|
|
|
payload, errors = parse_member_form(dict(request.form), household_id=household_id)
|
|
if errors:
|
|
for error in errors:
|
|
flash(error, "error")
|
|
return redirect(url_for("main.edit_household", household_id=household_id))
|
|
|
|
member = build_new_member_draft(household_id)
|
|
for field_name, value in payload.items():
|
|
setattr(member, field_name, value)
|
|
|
|
current_account = g.current_account
|
|
if isinstance(current_account, Account):
|
|
member.created_by = current_account.id
|
|
member.updated_by = current_account.id
|
|
|
|
db.session.add(member)
|
|
db.session.flush()
|
|
|
|
write_audit_log(
|
|
action_type="create_household_member",
|
|
target_type="household_member",
|
|
actor=current_account if isinstance(current_account, Account) else None,
|
|
target_id=member.id,
|
|
target_display_name=f"{household.head_name} - {member.name}",
|
|
after_data=serialize_member_snapshot(member),
|
|
commit=False,
|
|
)
|
|
db.session.commit()
|
|
|
|
flash(f"已添加成员 {member.name}。", "success")
|
|
return redirect(url_for("main.edit_household", household_id=household_id))
|
|
|
|
|
|
@main_bp.post("/households/<int:household_id>/members/<int:member_id>")
|
|
@role_required("admin", "editor")
|
|
def update_member(household_id: int, member_id: int):
|
|
"""更新成员信息。"""
|
|
household = get_household_or_none(household_id)
|
|
if household is None:
|
|
flash("未找到要编辑成员的户信息。", "error")
|
|
return redirect(url_for("main.index"))
|
|
|
|
member = get_member_or_none(member_id)
|
|
if member is None or member.household_id != household_id:
|
|
flash("未找到要编辑的成员信息。", "error")
|
|
return redirect(url_for("main.edit_household", household_id=household_id))
|
|
|
|
redirect_target = url_for("main.edit_household", household_id=household_id)
|
|
payload, errors = parse_member_form(dict(request.form), household_id=household_id)
|
|
if errors:
|
|
for error in errors:
|
|
flash(error, "error")
|
|
return redirect(redirect_target)
|
|
|
|
before_snapshot = serialize_member_snapshot(member)
|
|
for field_name, value in payload.items():
|
|
setattr(member, field_name, value)
|
|
|
|
current_account = g.current_account
|
|
if isinstance(current_account, Account):
|
|
member.updated_by = current_account.id
|
|
|
|
after_snapshot = serialize_member_snapshot(member)
|
|
if after_snapshot == before_snapshot:
|
|
flash("没有检测到需要保存的成员变化。", "warning")
|
|
db.session.rollback()
|
|
return redirect(redirect_target)
|
|
|
|
write_audit_log(
|
|
action_type="update_household_member",
|
|
target_type="household_member",
|
|
actor=current_account if isinstance(current_account, Account) else None,
|
|
target_id=member.id,
|
|
target_display_name=f"{household.head_name} - {member.name}",
|
|
before_data=before_snapshot,
|
|
after_data=after_snapshot,
|
|
commit=False,
|
|
)
|
|
db.session.commit()
|
|
|
|
flash(f"已更新成员 {member.name} 的信息。", "success")
|
|
return redirect(redirect_target)
|
|
|
|
|
|
@main_bp.post("/households/<int:household_id>/members/<int:member_id>/delete")
|
|
@role_required("admin", "editor")
|
|
def delete_member(household_id: int, member_id: int):
|
|
"""删除成员。"""
|
|
household = get_household_or_none(household_id)
|
|
if household is None:
|
|
flash("未找到要删除成员的户信息。", "error")
|
|
return redirect(url_for("main.index"))
|
|
|
|
member = get_member_or_none(member_id)
|
|
if member is None or member.household_id != household_id:
|
|
flash("未找到要删除的成员信息。", "error")
|
|
return redirect(url_for("main.edit_household", household_id=household_id))
|
|
|
|
redirect_target = url_for("main.edit_household", household_id=household_id)
|
|
|
|
# 确认删除
|
|
confirm = request.form.get("confirm", "").strip()
|
|
if confirm != "yes":
|
|
flash("请确认删除操作。", "warning")
|
|
return redirect(redirect_target)
|
|
|
|
before_snapshot = serialize_member_snapshot(member)
|
|
member_name = member.name
|
|
|
|
current_account = g.current_account
|
|
if isinstance(current_account, Account):
|
|
member.updated_by = current_account.id
|
|
|
|
db.session.delete(member)
|
|
|
|
write_audit_log(
|
|
action_type="delete_household_member",
|
|
target_type="household_member",
|
|
actor=current_account if isinstance(current_account, Account) else None,
|
|
target_id=member_id,
|
|
target_display_name=f"{household.head_name} - {member_name}",
|
|
before_data=before_snapshot,
|
|
after_data={"deleted": True},
|
|
commit=False,
|
|
)
|
|
db.session.commit()
|
|
|
|
flash(f"已删除成员 {member_name}。", "success")
|
|
return redirect(redirect_target)
|
|
|