212 lines
10 KiB
Python
212 lines
10 KiB
Python
"""自有车辆 ETC/油卡看板:按有效发放记录聚合,不受列表分页影响。"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, time, timedelta, timezone
|
|
from typing import TYPE_CHECKING, Optional
|
|
|
|
from sqlalchemy import and_, case, func, or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.models import SysUser
|
|
from app.models.pl_models import (
|
|
PlPersonnel,
|
|
PlVehicle,
|
|
PlVehiclePersonnel,
|
|
PlVehicleRechargeApply,
|
|
PlVehicleRechargeApproval,
|
|
PlVehicleRechargeRecord,
|
|
)
|
|
from app.schemas.pl.vehicle_recharge_summary import (
|
|
RechargeAmounts,
|
|
VehicleRechargeNotice,
|
|
VehicleRechargeSummary,
|
|
VehicleRechargeSummaryItem,
|
|
)
|
|
from app.services.pl_data_scope import apply_pl_scope
|
|
|
|
if TYPE_CHECKING:
|
|
from app.core.db_auth import DbUserContext
|
|
|
|
|
|
def _scoped_query(db, model, tenant_no, user):
|
|
query = db.query(model).filter(model.tenant_no == tenant_no, model.del_flag == 0)
|
|
return apply_pl_scope(query, model, user, db, tenant_no)
|
|
|
|
|
|
def _vehicle_driver_names(db, tenant_no, user, vehicle_ids):
|
|
"""从当前车辆人员关联取司机,默认司机优先,不把押运员作为司机。"""
|
|
personnel = _scoped_query(db, PlPersonnel, tenant_no, user).subquery()
|
|
rows = _scoped_query(db, PlVehiclePersonnel, tenant_no, user).join(
|
|
personnel, personnel.c.id == PlVehiclePersonnel.personnel_id
|
|
).filter(
|
|
PlVehiclePersonnel.vehicle_id.in_(vehicle_ids),
|
|
PlVehiclePersonnel.role == "driver",
|
|
).with_entities(
|
|
PlVehiclePersonnel.vehicle_id, personnel.c.personnel_name,
|
|
).order_by(
|
|
PlVehiclePersonnel.is_default.desc(), PlVehiclePersonnel.order_num.asc(),
|
|
PlVehiclePersonnel.create_time.desc(), PlVehiclePersonnel.id.desc(),
|
|
).all()
|
|
names = {}
|
|
for vehicle_id, name in rows:
|
|
names.setdefault(vehicle_id, name)
|
|
return names
|
|
|
|
|
|
def _notices(db, tenant_no, user, applies, records, record_plate, today):
|
|
"""每类最多取三条候选,再合并排序;发放审批与发放记录只展示一次。"""
|
|
apply_model = PlVehicleRechargeApply
|
|
approval_model = PlVehicleRechargeApproval
|
|
record_model = PlVehicleRechargeRecord
|
|
end = datetime.combine(today + timedelta(days=1), time.min)
|
|
visible_applies = applies.subquery()
|
|
valid_records = records.with_entities(record_model.apply_id).subquery()
|
|
approvals = _scoped_query(db, approval_model, tenant_no, user).join(
|
|
visible_applies, visible_applies.c.id == approval_model.apply_id
|
|
).filter(
|
|
approval_model.approval_status.in_(("approved", "rejected", "disbursed")),
|
|
approval_model.approval_time < end,
|
|
or_(
|
|
approval_model.approval_status != "disbursed",
|
|
approval_model.apply_id.in_(db.query(valid_records.c.apply_id)),
|
|
),
|
|
)
|
|
disbursed_approvals = approvals.filter(
|
|
approval_model.approval_status == "disbursed"
|
|
).with_entities(approval_model.apply_id).subquery()
|
|
apply_rows = applies.filter(
|
|
apply_model.status.in_(("in_approval", "pending", "rejected", "disbursed")),
|
|
apply_model.create_time < end,
|
|
).order_by(apply_model.create_time.desc(), apply_model.id.desc()).limit(3).all()
|
|
approval_rows = approvals.with_entities(
|
|
approval_model, visible_applies.c.license_plate, visible_applies.c.recharge_type,
|
|
).order_by(approval_model.approval_time.desc(), approval_model.id.desc()).limit(3).all()
|
|
record_rows = records.filter(
|
|
record_model.create_time < end,
|
|
~record_model.apply_id.in_(db.query(disbursed_approvals.c.apply_id)),
|
|
).with_entities(
|
|
record_model, record_plate
|
|
).order_by(record_model.create_time.desc(), record_model.id.desc()).limit(3).all()
|
|
|
|
# 系统用户与历史业务表的 collation 可能不同;最多九个候选在 Python 中关联姓名。
|
|
actor_ids = {row.applicant_id for row in apply_rows}
|
|
actor_ids.update(row.approver_id for row, _plate, _type in approval_rows)
|
|
actor_ids.update(row.create_by for row, _plate in record_rows)
|
|
actor_ids.discard(None)
|
|
actor_ids.discard("")
|
|
names = {}
|
|
if actor_ids:
|
|
names = {
|
|
user_id: name or account or ""
|
|
for user_id, name, account in db.query(
|
|
SysUser.id, SysUser.nick_name, SysUser.user_no
|
|
).filter(
|
|
SysUser.id.in_(actor_ids),
|
|
SysUser.tenant_no == tenant_no,
|
|
SysUser.del_flag == 0,
|
|
).all()
|
|
}
|
|
candidates = []
|
|
for row in apply_rows:
|
|
# 没有 submit_time 字段,明确展示真实创建动作,不推断提交/重提时间。
|
|
candidates.append(VehicleRechargeNotice(
|
|
id="apply:" + row.id, occurredAt=row.create_time,
|
|
actorName=names.get(row.applicant_id, ""), roleName="申请人", action="申请了",
|
|
licensePlate=row.license_plate, rechargeType=row.recharge_type,
|
|
))
|
|
actions = {"approved": "已审批", "rejected": "已驳回", "disbursed": "已发放"}
|
|
for row, plate, recharge_type in approval_rows:
|
|
candidates.append(VehicleRechargeNotice(
|
|
id="approval:" + row.id, occurredAt=row.approval_time,
|
|
actorName=row.approver_name or names.get(row.approver_id, ""),
|
|
roleName=row.node_name or "", action=actions[row.approval_status],
|
|
licensePlate=plate, rechargeType=recharge_type,
|
|
))
|
|
for row, plate in record_rows:
|
|
candidates.append(VehicleRechargeNotice(
|
|
id="record:" + row.id, occurredAt=row.create_time,
|
|
actorName=names.get(row.create_by, ""), roleName="", action="已发放",
|
|
licensePlate=plate, rechargeType=row.recharge_type,
|
|
))
|
|
return sorted(candidates, key=lambda notice: (notice.occurred_at, notice.id), reverse=True)[:3]
|
|
|
|
|
|
def get_summary(
|
|
db: Session,
|
|
tenant_no: str,
|
|
user: "DbUserContext",
|
|
*,
|
|
today: Optional[date] = None,
|
|
) -> VehicleRechargeSummary:
|
|
today = today or datetime.now(timezone(timedelta(hours=8))).date()
|
|
vehicle_query = _scoped_query(db, PlVehicle, tenant_no, user).filter(
|
|
PlVehicle.vehicle_type == "self"
|
|
)
|
|
# 历史数据中同车牌可能有多份挂车档案,每张卡采用当前可见的最新档案。
|
|
vehicles_by_plate = {}
|
|
for vehicle in vehicle_query.order_by(PlVehicle.create_time.desc(), PlVehicle.id.desc()).all():
|
|
vehicles_by_plate.setdefault(vehicle.license_plate, vehicle)
|
|
vehicles = list(vehicles_by_plate.values())
|
|
if not vehicles:
|
|
return VehicleRechargeSummary(asOfDate=today, vehicles=[], notices=[])
|
|
driver_names = _vehicle_driver_names(db, tenant_no, user, [row.id for row in vehicles])
|
|
# 关联键必须唯一,否则同车牌多档案会将申请数及充值金额成倍放大。
|
|
visible_vehicles = vehicle_query.with_entities(PlVehicle.license_plate).distinct().subquery()
|
|
apply_model = PlVehicleRechargeApply
|
|
record_model = PlVehicleRechargeRecord
|
|
applies = _scoped_query(db, apply_model, tenant_no, user).join(
|
|
visible_vehicles, visible_vehicles.c.license_plate == apply_model.license_plate
|
|
).filter(apply_model.recharge_type.in_(("etc", "fuel")))
|
|
visible_applies = applies.subquery()
|
|
records = _scoped_query(db, record_model, tenant_no, user).join(
|
|
visible_applies,
|
|
and_(visible_applies.c.id == record_model.apply_id,
|
|
visible_applies.c.recharge_type == record_model.recharge_type),
|
|
).filter(
|
|
visible_applies.c.status == "disbursed",
|
|
record_model.recharge_date <= today,
|
|
)
|
|
|
|
month_start = today.replace(day=1)
|
|
previous_month_start = (month_start - timedelta(days=1)).replace(day=1)
|
|
quarter_start = today.replace(month=((today.month - 1) // 3) * 3 + 1, day=1)
|
|
previous_quarter_end = quarter_start - timedelta(days=1)
|
|
previous_quarter_start = previous_quarter_end.replace(
|
|
month=((previous_quarter_end.month - 1) // 3) * 3 + 1, day=1
|
|
)
|
|
recharge_date = record_model.recharge_date
|
|
amount = record_model.amount
|
|
amounts = records.with_entities(
|
|
visible_applies.c.license_plate,
|
|
record_model.recharge_type,
|
|
func.sum(case((recharge_date >= month_start, amount), else_=0)),
|
|
func.sum(case((and_(recharge_date >= previous_month_start, recharge_date < month_start), amount), else_=0)),
|
|
func.sum(case((recharge_date >= quarter_start, amount), else_=0)),
|
|
func.sum(case((and_(recharge_date >= previous_quarter_start, recharge_date < quarter_start), amount), else_=0)),
|
|
func.sum(amount),
|
|
func.sum(case((recharge_date < quarter_start, amount), else_=0)),
|
|
).group_by(visible_applies.c.license_plate, record_model.recharge_type).all()
|
|
summaries = {}
|
|
for plate, recharge_type, month, previous_month, quarter, previous_quarter, total, previous_total in amounts:
|
|
summaries[(plate, recharge_type)] = RechargeAmounts(
|
|
monthAmount=month, previousMonthAmount=previous_month,
|
|
quarterAmount=quarter, previousQuarterAmount=previous_quarter,
|
|
totalAmount=total, previousTotalAmount=previous_total,
|
|
)
|
|
pending = applies.filter(apply_model.status.in_(("in_approval", "pending"))).with_entities(
|
|
apply_model.license_plate, apply_model.recharge_type, func.count(apply_model.id)
|
|
).group_by(apply_model.license_plate, apply_model.recharge_type).all()
|
|
for plate, recharge_type, count in pending:
|
|
summaries.setdefault((plate, recharge_type), RechargeAmounts()).pending_count = count
|
|
return VehicleRechargeSummary(
|
|
asOfDate=today,
|
|
vehicles=[VehicleRechargeSummaryItem(
|
|
id=row.id, licensePlate=row.license_plate, trailerNo=row.trailer_no,
|
|
driverName=driver_names.get(row.id, row.default_driver_name), vehicleType=row.vehicle_type,
|
|
vehicleStatus=row.vehicle_status,
|
|
etc=summaries.get((row.license_plate, "etc"), RechargeAmounts()),
|
|
fuel=summaries.get((row.license_plate, "fuel"), RechargeAmounts()),
|
|
) for row in vehicles],
|
|
notices=_notices(db, tenant_no, user, applies, records, visible_applies.c.license_plate, today),
|
|
)
|