efc200b6de
- 抽取 auto_complete_work_plans() 可复用函数到 visits service - POST/PUT 拜访均触发自动完成(更新场景解决补录问题) - Excel 导入旧周报时也触发自动完成 - 逾期计划自动取消:plan_date < today 且无匹配拜访→已取消 - edit_log 记录区分三种来源:拜访自动完成/拜访更新自动完成/旧周报导入自动完成/逾期自动取消 Co-Authored-By: Claude <noreply@anthropic.com>
201 lines
7.3 KiB
Python
201 lines
7.3 KiB
Python
from datetime import date, datetime
|
|
from sqlalchemy import select, func, and_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from app.models.visit import Visit
|
|
from app.models.daily_note import DailyNote
|
|
from app.models.user import User
|
|
from app.models.leave import Leave
|
|
from app.models.work_plan import WorkPlan
|
|
from app.models.customer import Customer
|
|
from app.services.holidays import load_holiday_sets
|
|
from app.services.wecom import wecom_client
|
|
from app.utils.timezone import today_cst, is_working_day
|
|
|
|
|
|
async def check_daily_reporting(db: AsyncSession) -> dict:
|
|
"""Check today's reporting progress and send wecom reminders to managers who haven't reported."""
|
|
today = today_cst()
|
|
|
|
# Load holiday data (auto-refreshes from API if not cached)
|
|
holidays, workdays = await load_holiday_sets(db)
|
|
|
|
if not is_working_day(today, holidays, workdays):
|
|
return {"status": "rest_day", "date": str(today)}
|
|
|
|
# Get all users who need to report
|
|
result = await db.execute(
|
|
select(User).where(User.require_report == True)
|
|
)
|
|
managers = result.scalars().all()
|
|
|
|
# ── Exclude managers on leave today ──
|
|
leaves_today = await db.execute(
|
|
select(Leave.manager_id).where(and_(
|
|
Leave.start_date <= today,
|
|
Leave.end_date >= today,
|
|
))
|
|
)
|
|
on_leave_ids = {str(uid) for uid, in leaves_today.all()}
|
|
|
|
# Get managers who have reported today
|
|
reported_visits = await db.execute(
|
|
select(Visit.manager_id).where(Visit.visit_date == today)
|
|
)
|
|
reported_notes = await db.execute(
|
|
select(DailyNote.manager_id).where(DailyNote.note_date == today)
|
|
)
|
|
reported_map = {str(uid): True for uid, in reported_visits.all()}
|
|
for uid, in reported_notes.all():
|
|
reported_map[str(uid)] = True
|
|
|
|
not_reported = []
|
|
reported_names = []
|
|
on_leave_names = []
|
|
for m in managers:
|
|
if str(m.id) in on_leave_ids:
|
|
on_leave_names.append(m.name)
|
|
continue # skip — exempt from reporting
|
|
if str(m.id) in reported_map:
|
|
reported_names.append(m.name)
|
|
else:
|
|
not_reported.append(m)
|
|
|
|
# 1. Send template card to not-reported managers (tap to open app)
|
|
if not_reported:
|
|
user_ids = [m.wecom_userid for m in not_reported if m.wecom_userid]
|
|
if user_ids:
|
|
desc = f"{today} | 已填报 {len(reported_names)}/{len(managers)} 人"
|
|
success = await wecom_client.send_template_card(
|
|
user_ids=user_ids,
|
|
title="📋 今日填报提醒",
|
|
desc=desc,
|
|
url="https://qj.dhdx.fun/m",
|
|
btn_text="去填报",
|
|
)
|
|
# Fallback to text
|
|
if not success:
|
|
names_text = "、".join(m.name for m in not_reported)
|
|
await wecom_client.send_text_message(
|
|
user_ids,
|
|
f"📋 今日填报提醒\n\n{desc}\n未填报:{names_text}\n\n请尽快完成填报 🙏\nhttps://qj.dhdx.fun/m",
|
|
)
|
|
|
|
# 2. Send summary to director
|
|
directors = await db.execute(
|
|
select(User).where(User.role.in_(["director", "leader"]), User.wecom_userid.isnot(None))
|
|
)
|
|
for d in directors.scalars().all():
|
|
if managers:
|
|
effective_total = len(managers) - len(on_leave_names)
|
|
if effective_total > 0:
|
|
pct = len(reported_map) / effective_total * 100
|
|
else:
|
|
pct = 100.0
|
|
leave_note = ""
|
|
if on_leave_names:
|
|
leave_note = f"\n> 请假中(已豁免):{len(on_leave_names)}人\n"
|
|
leave_note += "".join(f"- {n} (请假)\n" for n in on_leave_names)
|
|
summary = (
|
|
f"## 📊 今日填报汇总\n\n"
|
|
f"> 日期:{today}\n"
|
|
f"> 填报率:**{pct:.0f}%** ({len(reported_map)}/{effective_total})\n"
|
|
)
|
|
if leave_note:
|
|
summary += leave_note + "\n"
|
|
if not_reported:
|
|
summary += "**未填报:**\n" + "".join(f"- {m.name}\n" for m in not_reported)
|
|
else:
|
|
summary += "✅ 全体已完成今日填报"
|
|
await wecom_client.send_markdown_message(summary) # broadcast for director visibility
|
|
|
|
return {
|
|
"status": "ok",
|
|
"date": str(today),
|
|
"total_managers": len(managers),
|
|
"reported": len(reported_map),
|
|
"not_reported": len(not_reported),
|
|
}
|
|
|
|
|
|
async def check_overdue_plans(db: AsyncSession) -> dict:
|
|
"""Check for overdue work plans and remind managers (runs at 9:00 AM)."""
|
|
today = today_cst()
|
|
if today.weekday() >= 5:
|
|
return {"status": "weekend", "date": str(today)}
|
|
|
|
result = await db.execute(
|
|
select(WorkPlan).where(
|
|
WorkPlan.status == "计划中",
|
|
WorkPlan.plan_date < today,
|
|
).order_by(WorkPlan.manager_id, WorkPlan.plan_date)
|
|
)
|
|
overdue = result.scalars().all()
|
|
|
|
if not overdue:
|
|
return {"status": "ok", "date": str(today), "overdue": 0}
|
|
|
|
# Group by manager
|
|
by_manager: dict[str, list] = {}
|
|
for p in overdue:
|
|
mid = str(p.manager_id)
|
|
by_manager.setdefault(mid, []).append(p)
|
|
|
|
users_result = await db.execute(
|
|
select(User).where(User.id.in_([uid for uid in by_manager.keys()]))
|
|
)
|
|
user_map = {str(u.id): u for u in users_result.scalars().all()}
|
|
|
|
for mid, plans in by_manager.items():
|
|
user = user_map.get(mid)
|
|
if not user or not user.wecom_userid:
|
|
continue
|
|
names = "、".join(f"{p.customer_id}" for p in plans[:5])
|
|
# Get customer names
|
|
cust_result = await db.execute(
|
|
select(Customer.name).where(Customer.id.in_([p.customer_id for p in plans[:5]]))
|
|
)
|
|
cust_names = [r[0] for r in cust_result.all()]
|
|
|
|
plan_lines = "".join(f"- {n} (计划 {p.plan_date})\n" for p, n in zip(plans[:5], cust_names))
|
|
content = (
|
|
f"📅 拜访计划过期提醒\n\n"
|
|
f"以下 {len(plans)} 个拜访计划已过期,请尽快安排拜访:\n"
|
|
f"{plan_lines}"
|
|
)
|
|
if len(plans) > 5:
|
|
content += f"... 还有 {len(plans) - 5} 个过期计划\n"
|
|
content += f"\n👉 查看详情:https://qj.dhdx.fun/light-board"
|
|
|
|
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),
|
|
}
|