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:
+13
-10
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user