PayrollManagementSystem/backend/app/schemas/report.py
liuwenxin 2a1939d2e7 feat(report): 报表统计四表查询与 Excel 签字导出
- 四表接口 /api/v1/reports/*:个人明细(全筛选 summary)、部门汇总(团队基数与月结聚合同源、12.5%/5% 分列、零行经理、冻结对照)、业务统计(排除聚合行)、回款统计(8 档区间恒在、回款金额按 receipt_id 去重防分摊拆行翻倍)
- Excel 导出:公司页眉/财务复核栏/领导签字栏/日期/版本、Decimal 原值 + number_format、文本防公式注入、2 万行超限 400、导出行为入操作日志
- manager_share 共享模块:月结聚合与报表部门汇总同源取参与分桶(薄委托保留测试入口)
- 区间元数据落 core/report_meta(repository 需要类型,避免反向依赖 service 层)
- 准入 require_roles(admin/finance/leader),支持自定义 403 文案

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-19 16:33:31 +08:00

270 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""报表统计的请求/响应模型(M4 ⑤)。
金额与比例一律 `Decimal`(pydantic v2 序列化为字符串,零精度损失);
比例原样输出 `rate_text`(如 88/9000),禁止转 float 或截断展示。
"""
from datetime import date
from decimal import Decimal
from pydantic import BaseModel, ConfigDict, Field
from app.core.response import PageQuery
# ==== 查询入参 ====
class PersonalDetailQuery(PageQuery):
"""个人绩效明细查询条件(FR-029:人员/月份/客户类型/产品类别/岗位)。"""
settlement_month: str | None = Field(default=None, max_length=7, description="归属月 YYYY-MM")
personnel_id: int | None = Field(default=None, ge=1, description="人员筛选")
customer_type: str | None = Field(default=None, max_length=32, description="客户类型筛选")
product_category: str | None = Field(default=None, max_length=32, description="产品类别筛选")
role_code: str | None = Field(default=None, max_length=32, description="岗位筛选")
class DepartmentSummaryQuery(BaseModel):
"""部门汇总查询条件(月份必填——部门表按单月聚合,无跨月视角)。"""
settlement_month: str = Field(min_length=7, max_length=7, description="归属月 YYYY-MM")
manager_id: int | None = Field(default=None, ge=1, description="仅看某经理团队")
class BusinessStatsQuery(PageQuery):
"""业务统计查询条件;回款/绩效为该业务累计值,不随业务日期筛选而变。"""
keyword: str | None = Field(default=None, max_length=64, description="业务编号/客户名称")
customer_id: int | None = Field(default=None, ge=1, description="客户筛选")
owner_personnel_id: int | None = Field(default=None, ge=1, description="归属人筛选")
status: int | None = Field(default=None, description="业务状态筛选")
date_from: date | None = Field(default=None, description="业务日期起")
date_to: date | None = Field(default=None, description="业务日期止")
class PaymentStatsQuery(BaseModel):
"""回款统计查询条件(按回款天数区间/客户类型/产品类别统计,FR-031)。"""
settlement_month: str | None = Field(default=None, max_length=7, description="归属月 YYYY-MM")
date_from: date | None = Field(default=None, description="回款日期起")
date_to: date | None = Field(default=None, description="回款日期止")
customer_type: str | None = Field(default=None, max_length=32, description="客户类型筛选")
product_category: str | None = Field(default=None, max_length=32, description="产品类别筛选")
class PaymentDetailQuery(PageQuery):
"""回款区间下钻(桶编码必填,入参由 payment-stats 页面展开行携带)。"""
bucket: str = Field(min_length=1, max_length=16, description="区间桶编码")
settlement_month: str | None = Field(default=None, max_length=7, description="归属月 YYYY-MM")
date_from: date | None = Field(default=None, description="回款日期起")
date_to: date | None = Field(default=None, description="回款日期止")
customer_type: str | None = Field(default=None, max_length=32, description="客户类型筛选")
product_category: str | None = Field(default=None, max_length=32, description="产品类别筛选")
# ==== 响应出参 ====
class ReportSummary(BaseModel):
"""全筛选口径合计(与「当前页合计」区分,前端页脚需注明)。
字段按报表取用:个人明细用绩效/应付/折算毛利;业务统计用发货/回款/
销售额;不涉及的留 None。
"""
row_count: int = Field(default=0, description="筛选结果行数")
performance_total: Decimal | None = Field(default=None, description="绩效金额合计")
payable_total: Decimal | None = Field(default=None, description="应付绩效合计")
converted_profit_total: Decimal | None = Field(default=None, description="折算毛利润合计")
ship_total: Decimal | None = Field(default=None, description="发货额合计")
received_total: Decimal | None = Field(default=None, description="回款额合计")
sale_total: Decimal | None = Field(default=None, description="销售额合计")
class ReportPage[T](BaseModel):
"""报表分页:标准分页四字段 + 全筛选合计。
序列化字段与 PageResult 一致(list/total/pageNum/pageSize),前端
usePagedList 可直接消费;追加的 summary 由各页页脚单独读取。
"""
model_config = ConfigDict(populate_by_name=True)
records: list[T] = Field(
default_factory=list, alias="list", serialization_alias="list", description="当前页数据"
)
total: int = Field(default=0, description="总记录数")
page_num: int = Field(
default=1, alias="pageNum", serialization_alias="pageNum", description="当前页码"
)
page_size: int = Field(
default=10, alias="pageSize", serialization_alias="pageSize", description="每页条数"
)
summary: ReportSummary = Field(default_factory=ReportSummary, description="全筛选合计")
class PersonalDetailRow(BaseModel):
"""个人绩效明细行(需规 2.4.1 列全覆盖)。
二次分摊拆行后同一回款按人多行(负责人余额行 + 参与人份额行),行内
不放「关联回款金额」列——同回款金额重复展示会让合计无法自洽,金额
下沉到快照下钻。
"""
ledger_id: int
business_id: int
business_no: str
personnel_id: int
personnel_name: str
role_code: str
role_name: str
customer_type: str
product_category: str
ship_date: date | None = None
receipt_date: date
payment_days: int | None = None
rate_text: str
weight_r: int | None = None
payment_weight: Decimal | None = None
performance_amount: Decimal | None = None
payable_amount: Decimal | None = None
converted_profit: Decimal | None = None
is_probation: int
missing_cost: int
calc_version: int
settlement_id: int | None = None
class DepartmentMemberRow(BaseModel):
"""部门汇总 · 团队成员明细(内联于团队行)。"""
personnel_id: int
personnel_name: str
role_code: str
is_probation: int
performance_sum: Decimal | None = None
payable_sum: Decimal | None = None
adjust_extra: Decimal = Field(default=Decimal("0"), description="调整单生效补发合计")
adjust_deduct: Decimal = Field(default=Decimal("0"), description="调整单生效扣减合计")
adjust_net: Decimal = Field(default=Decimal("0"), description="调整净额(补发为正)")
net_total: Decimal | None = None
ledger_ids: list[int] = Field(default_factory=list, description="台账行 id 列表(下钻锚点)")
class DepartmentTeamRow(BaseModel):
"""部门汇总 · 经理团队行。"""
manager_id: int
manager_name: str
manager_status: int = Field(description="经理在职状态(1 在职 0 离职)")
member_count: int
team_base: Decimal = Field(description="团队基数(12.5% 的底,等于成员绩效合计)")
team_share_amount: Decimal = Field(description="12.5% 团队分成")
company_share_amount: Decimal = Field(description="5% 全公司分成(同一基数,各团队不互斥)")
company_base_ratio: Decimal = Field(description="团队基数占全公司基数比例(除零为 0)")
adjust_extra: Decimal = Decimal("0")
adjust_deduct: Decimal = Decimal("0")
adjust_net: Decimal = Decimal("0")
net_total: Decimal = Field(description="团队净额 = 分成 + 调整净额")
frozen_amount: Decimal | None = Field(default=None, description="月结聚合行冻结额(对照)")
aggregate_ledger_id: int | None = None
members: list[DepartmentMemberRow] = Field(default_factory=list)
class DepartmentCompanyBlock(BaseModel):
"""部门汇总 · 全公司口径块(FR-030「全公司 5% 部分单列」)。"""
team_base_total: Decimal = Field(description="各团队基数之和(= 全部单笔绩效合计)")
company_base: Decimal = Field(description="全公司基数(5% 的底)")
company_share_pct: str = Field(description="分成比例参数原文")
company_share_amount: Decimal = Field(description="5% 全公司分成")
class DepartmentSummaryOut(BaseModel):
"""部门绩效工资汇总响应(FR-030)。"""
settlement_month: str
params: dict | None = Field(default=None, description="分成参数原文与取值时点(对账用)")
teams: list[DepartmentTeamRow] = Field(default_factory=list)
unassigned: list[DepartmentMemberRow] = Field(
default_factory=list, description="无经理人员的绩效(不参与分成,仅展示)"
)
company: DepartmentCompanyBlock | None = None
totals: dict = Field(default_factory=dict, description="全表合计(成员/团队对账自证)")
class BusinessStatsRow(BaseModel):
"""业务统计行(发货/回款/绩效为该业务累计值)。"""
business_id: int
business_no: str
business_date: date | None = None
status: int
customer_name: str | None = None
owner_personnel_name: str | None = None
total_sale_amount: Decimal | None = None
total_ship_amount: Decimal | None = None
total_received_amount: Decimal | None = None
performance_amount: Decimal | None = None
payable_amount: Decimal | None = None
class BucketRow(BaseModel):
"""回款统计 · 区间桶行(空桶恒在且为 0,防金额静默消失)。"""
code: str
label: str
min_days: int | None = None
max_days: int | None = None
receipt_amount: Decimal | None = None
receipt_count: int = 0
performance_amount: Decimal | None = None
payable_amount: Decimal | None = None
ledger_count: int = 0
class GroupRow(BaseModel):
"""回款统计 · 维度对比行(客户类型 / 产品类别)。"""
code: str
label: str
receipt_amount: Decimal | None = None
performance_amount: Decimal | None = None
payable_amount: Decimal | None = None
ledger_count: int = 0
class PaymentStatsOut(BaseModel):
"""回款情况统计响应(FR-031)。
口径:已计提回款(有台账行)的金额与绩效;进度登记回款无台账行,
不在本表(页面需注明)。
"""
settlement_month: str | None = None
buckets: list[BucketRow] = Field(default_factory=list)
by_customer_type: list[GroupRow] = Field(default_factory=list)
by_product_category: list[GroupRow] = Field(default_factory=list)
totals: dict = Field(default_factory=dict, description="全表合计")
class PaymentDetailRow(BaseModel):
"""回款区间下钻行(行级展示回款金额与批次号)。"""
ledger_id: int
receipt_id: int | None = None
receipt_batch_no: str | None = None
amount: Decimal | None = Field(default=None, description="回款金额(同回款拆行重复展示)")
business_id: int
business_no: str
personnel_id: int
personnel_name: str
customer_type: str
product_category: str
ship_date: date | None = None
receipt_date: date
payment_days: int | None = None
performance_amount: Decimal | None = None
payable_amount: Decimal | None = None