feat: 工作计划状态自动流转

- 抽取 auto_complete_work_plans() 可复用函数到 visits service
- POST/PUT 拜访均触发自动完成(更新场景解决补录问题)
- Excel 导入旧周报时也触发自动完成
- 逾期计划自动取消:plan_date < today 且无匹配拜访→已取消
- edit_log 记录区分三种来源:拜访自动完成/拜访更新自动完成/旧周报导入自动完成/逾期自动取消

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-12 16:21:38 +08:00
parent c38a9e74ad
commit efc200b6de
4 changed files with 84 additions and 10 deletions
+13 -10
View File
@@ -161,17 +161,11 @@ async def create_visit(
db.add(customer) db.add(customer)
# Auto-complete matching work plans for this customer # Auto-complete matching work plans for this customer
from app.models.work_plan import WorkPlan from app.services.visits import auto_complete_work_plans
plans_result = await db.execute( await auto_complete_work_plans(
select(WorkPlan).where( db, data.customer_id, parse_date(data.visit_date),
WorkPlan.customer_id == data.customer_id, current_user["name"], "拜访自动完成",
WorkPlan.status == "计划中",
WorkPlan.plan_date <= parse_date(data.visit_date),
)
) )
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 # Create draft copies for companions
for companion_id in data.companions: for companion_id in data.companions:
@@ -237,6 +231,15 @@ async def update_visit(
await db.commit() await db.commit()
await db.refresh(visit) 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) return await _enrich_visit(visit, db)
+7
View File
@@ -203,6 +203,13 @@ async def import_from_excel(db: AsyncSession, file_bytes: bytes, manager_id: uui
db.add(visit) db.add(visit)
stats["visits"] += 1 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 # Update customer's last_visit_date for light board
cust = await db.get(Customer, customer_id) cust = await db.get(Customer, customer_id)
if cust and (not cust.last_visit_date or visit_date > cust.last_visit_date): if cust and (not cust.last_visit_date or visit_date > cust.last_visit_date):
+24
View File
@@ -168,9 +168,33 @@ async def check_overdue_plans(db: AsyncSession) -> dict:
await wecom_client.send_text_message([user.wecom_userid], content) 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 { return {
"status": "ok", "status": "ok",
"date": str(today), "date": str(today),
"overdue": len(overdue), "overdue": len(overdue),
"auto_cancelled": auto_cancelled,
"managers_affected": len(by_manager), "managers_affected": len(by_manager),
} }
+40
View File
@@ -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