From efc200b6debaad3948c7717e2148ee13cc4643c1 Mon Sep 17 00:00:00 2001 From: v6ole Date: Sun, 12 Jul 2026 16:21:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E4=BD=9C=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E8=87=AA=E5=8A=A8=E6=B5=81=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽取 auto_complete_work_plans() 可复用函数到 visits service - POST/PUT 拜访均触发自动完成(更新场景解决补录问题) - Excel 导入旧周报时也触发自动完成 - 逾期计划自动取消:plan_date < today 且无匹配拜访→已取消 - edit_log 记录区分三种来源:拜访自动完成/拜访更新自动完成/旧周报导入自动完成/逾期自动取消 Co-Authored-By: Claude --- backend/app/api/visits.py | 23 +++++++++------- backend/app/services/excel_import.py | 7 +++++ backend/app/services/scheduler.py | 24 +++++++++++++++++ backend/app/services/visits.py | 40 ++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 backend/app/services/visits.py diff --git a/backend/app/api/visits.py b/backend/app/api/visits.py index d38ad12..001388c 100644 --- a/backend/app/api/visits.py +++ b/backend/app/api/visits.py @@ -161,17 +161,11 @@ async def create_visit( db.add(customer) # Auto-complete matching work plans for this customer - from app.models.work_plan import WorkPlan - plans_result = await db.execute( - select(WorkPlan).where( - WorkPlan.customer_id == data.customer_id, - WorkPlan.status == "计划中", - WorkPlan.plan_date <= parse_date(data.visit_date), - ) + from app.services.visits import auto_complete_work_plans + await auto_complete_work_plans( + db, data.customer_id, parse_date(data.visit_date), + current_user["name"], "拜访自动完成", ) - for plan in plans_result.scalars().all(): - plan.status = "已完成" - append_entry(plan, current_user["name"], [{"field": "status", "from": "计划中", "to": "已完成", "reason": "拜访自动完成"}]) # Create draft copies for companions for companion_id in data.companions: @@ -237,6 +231,15 @@ async def update_visit( await db.commit() await db.refresh(visit) + + # Auto-complete matching work plans after visit update + from app.services.visits import auto_complete_work_plans + await auto_complete_work_plans( + db, visit.customer_id, visit.visit_date, + current_user["name"], "拜访更新自动完成", + ) + await db.commit() + return await _enrich_visit(visit, db) diff --git a/backend/app/services/excel_import.py b/backend/app/services/excel_import.py index 5cad36b..646c7d5 100644 --- a/backend/app/services/excel_import.py +++ b/backend/app/services/excel_import.py @@ -203,6 +203,13 @@ async def import_from_excel(db: AsyncSession, file_bytes: bytes, manager_id: uui db.add(visit) stats["visits"] += 1 + # Auto-complete matching work plans + from app.services.visits import auto_complete_work_plans + await auto_complete_work_plans( + db, customer_id, visit_date, + mgr_name, "旧周报导入自动完成", + ) + # Update customer's last_visit_date for light board cust = await db.get(Customer, customer_id) if cust and (not cust.last_visit_date or visit_date > cust.last_visit_date): diff --git a/backend/app/services/scheduler.py b/backend/app/services/scheduler.py index 33e192c..98d8374 100644 --- a/backend/app/services/scheduler.py +++ b/backend/app/services/scheduler.py @@ -168,9 +168,33 @@ async def check_overdue_plans(db: AsyncSession) -> dict: await wecom_client.send_text_message([user.wecom_userid], content) + # ── Auto-cancel overdue plans that have no matching visit ── + from app.utils.edit_log import append_entry as append_edit_log + auto_cancelled = 0 + for plan in overdue: + has_visit = await db.execute( + select(Visit).where( + Visit.customer_id == plan.customer_id, + Visit.visit_date >= plan.plan_date, + ) + ) + if not has_visit.scalar(): + plan.status = "已取消" + append_edit_log(plan, "系统", [{ + "field": "status", + "from": "计划中", + "to": "已取消", + "reason": "逾期自动取消", + }]) + auto_cancelled += 1 + + if auto_cancelled: + await db.commit() + return { "status": "ok", "date": str(today), "overdue": len(overdue), + "auto_cancelled": auto_cancelled, "managers_affected": len(by_manager), } diff --git a/backend/app/services/visits.py b/backend/app/services/visits.py new file mode 100644 index 0000000..9808483 --- /dev/null +++ b/backend/app/services/visits.py @@ -0,0 +1,40 @@ +"""Visit service — shared logic for visit creation, update, and import.""" + +from uuid import UUID +from datetime import date +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from app.models.work_plan import WorkPlan +from app.utils.edit_log import append_entry + + +async def auto_complete_work_plans( + db: AsyncSession, + customer_id: UUID, + visit_date: date, + editor_name: str, + reason: str = "拜访自动完成", +) -> int: + """Auto-complete matching work plans when a visit is created/updated/imported. + + Matches by: same customer_id + status=="计划中" + plan_date <= visit_date. + Returns the number of plans completed. + """ + plans_result = await db.execute( + select(WorkPlan).where( + WorkPlan.customer_id == customer_id, + WorkPlan.status == "计划中", + WorkPlan.plan_date <= visit_date, + ) + ) + count = 0 + for plan in plans_result.scalars().all(): + plan.status = "已完成" + append_entry(plan, editor_name, [{ + "field": "status", + "from": "计划中", + "to": "已完成", + "reason": reason, + }]) + count += 1 + return count