Files
shanxia2.0/backend/app/services/pl_vehicle_recharge_summary_service.py
T

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),
)