"""报表统计仓储:只读聚合查询;不 commit、不抛业务异常。 报表与台账分页查询刻意分开而非复用 ``performance_ledger_repository``: - 台账查询允许聚合行(calc_type=2),报表明细必须显式排除——经理聚合 行 receipt_id 为 NULL,混入明细会让合同编号/天数列失真、业务统计的 绩效被整团队聚合额污染; - 月结的 ``_confirm_scope`` 带 ``settlement_id IS NULL``(可入结口径), 报表要看的是同月**全量**生效单笔行,口径不同; - 聚合需求(SUM/GROUP BY)台账仓储完全没有,报表在此统一实现。 发货额/回款额直接读 businesses 的汇总冗余列(服务层事务内单点维护), 不逐业务子查询发货/回款表;绩效额 businesses 无冗余列,用子查询联台账。 """ from sqlalchemy import func, select from sqlalchemy.orm import Session from app.core.report_meta import DayBucket from app.models import ( AdjustmentOrder, Business, Customer, MonthlySettlement, PerformanceLedger, Personnel, ) from app.schemas.report import ( BusinessStatsQuery, PaymentDetailQuery, PaymentStatsQuery, PersonalDetailQuery, ) # 单笔生效行基础条件:报表四表共用(聚合行与已替代版本一律排除) _ACTIVE_SINGLE = (PerformanceLedger.calc_type == 1, PerformanceLedger.is_active == 1) # ==== 个人绩效明细(FR-029)==== def _personal_filters(query: PersonalDetailQuery) -> tuple: filters = [*_ACTIVE_SINGLE] if query.settlement_month: filters.append(PerformanceLedger.settlement_month == query.settlement_month) if query.personnel_id is not None: filters.append(PerformanceLedger.personnel_id == query.personnel_id) if query.customer_type: filters.append(PerformanceLedger.customer_type == query.customer_type) if query.product_category: filters.append(PerformanceLedger.product_category == query.product_category) if query.role_code: filters.append(PerformanceLedger.role_code == query.role_code) return tuple(filters) def list_personal_detail( session: Session, query: PersonalDetailQuery, *, offset: int, limit: int ) -> list[PerformanceLedger]: """个人绩效明细分页(回款日倒序,同人按版本可追溯)。""" stmt = select(PerformanceLedger).where(*_personal_filters(query)) stmt = stmt.order_by(PerformanceLedger.receipt_date.desc(), PerformanceLedger.id.desc()) stmt = stmt.offset(offset).limit(limit) return list(session.execute(stmt).scalars()) def count_personal_detail(session: Session, query: PersonalDetailQuery) -> int: """个人绩效明细计数(与 list_personal_detail 同筛选)。""" return int( session.execute( select(func.count()).select_from(PerformanceLedger).where(*_personal_filters(query)) ).scalar_one() ) def sum_personal_detail(session: Session, query: PersonalDetailQuery) -> dict: """全筛选口径合计(分页与合计同条件,避免「页脚合计≠全部结果」)。""" return ( session.execute( select( func.count().label("row_count"), func.coalesce(func.sum(PerformanceLedger.performance_amount), 0).label( "performance_total" ), func.coalesce(func.sum(PerformanceLedger.payable_amount), 0).label("payable_total"), func.coalesce(func.sum(PerformanceLedger.converted_profit), 0).label( "converted_profit_total" ), ).where(*_personal_filters(query)) ) .mappings() .one() ) # ==== 部门汇总取数(FR-030)==== def list_month_receipt_rows(session: Session, month: str) -> list[PerformanceLedger]: """同月全量生效单笔台账行(部门汇总基数与成员明细的取数)。""" return list( session.execute( select(PerformanceLedger).where( PerformanceLedger.settlement_month == month, *_ACTIVE_SINGLE ) ).scalars() ) def list_month_manager_aggregates(session: Session, month: str) -> list[PerformanceLedger]: """同月经理聚合行(calc_type=2):已生成月结时的冻结对照值。""" return list( session.execute( select(PerformanceLedger).where( PerformanceLedger.settlement_month == month, PerformanceLedger.calc_type == 2, PerformanceLedger.is_active == 1, ) ).scalars() ) def list_month_adjustments(session: Session, month: str) -> list[AdjustmentOrder]: """同月已生效调整单(status=2)——报表汇总并入的口径。 与 list_adjustments(调整单管理页)不同:必须按月筛选且只取生效行, 待审批/已驳回/已撤回的金额绝不能混进报表。 """ return list( session.execute( select(AdjustmentOrder).where( AdjustmentOrder.settlement_month == month, AdjustmentOrder.status == 2, ) ).scalars() ) def list_month_settlement(session: Session, month: str) -> MonthlySettlement | None: """同月月结头(部门汇总展示锁定状态用;无则 None)。""" return session.execute( select(MonthlySettlement).where(MonthlySettlement.settlement_month == month) ).scalar_one_or_none() # ==== 业务统计(FR-031 之一)==== def _business_perf_subquery(): """业务维度绩效子查询(排除聚合行,防 12.5%/5% 聚合额污染单业务绩效)。""" return ( select( PerformanceLedger.business_id.label("business_id"), func.coalesce(func.sum(PerformanceLedger.performance_amount), 0).label("perf"), func.coalesce(func.sum(PerformanceLedger.payable_amount), 0).label("payable"), ) .where(*_ACTIVE_SINGLE) .group_by(PerformanceLedger.business_id) .subquery() ) def _business_filters(query: BusinessStatsQuery) -> tuple: filters = [] if query.keyword: # 业务编号/公司名称双列 OR 模糊;contains(autoescape) 转义通配符, # 与 business_repository._apply_filters 同口径(双列 OR 用法为该处既定) filters.append( Business.business_no.contains(query.keyword, autoescape=True) | Customer.company_name.contains(query.keyword, autoescape=True) ) if query.customer_id is not None: filters.append(Business.customer_id == query.customer_id) if query.owner_personnel_id is not None: filters.append(Business.owner_personnel_id == query.owner_personnel_id) if query.status is not None: filters.append(Business.status == query.status) if query.date_from is not None: filters.append(Business.business_date >= query.date_from) if query.date_to is not None: filters.append(Business.business_date <= query.date_to) return tuple(filters) def _business_select(query: BusinessStatsQuery): perf_sq = _business_perf_subquery() return ( select( Business, Customer.company_name.label("customer_name"), Personnel.personnel_name.label("owner_personnel_name"), perf_sq.c.perf.label("performance_amount"), perf_sq.c.payable.label("payable_amount"), ) .outerjoin(Customer, Customer.id == Business.customer_id) .outerjoin(Personnel, Personnel.id == Business.owner_personnel_id) .outerjoin(perf_sq, perf_sq.c.business_id == Business.id) .where(*_business_filters(query)) ) def list_business_stats( session: Session, query: BusinessStatsQuery, *, offset: int, limit: int ) -> list: """业务统计分页(业务日期倒序)。""" stmt = _business_select(query).order_by(Business.id.desc()).offset(offset).limit(limit) return list(session.execute(stmt).all()) def count_business_stats(session: Session, query: BusinessStatsQuery) -> int: """业务统计计数(联客户表因关键字筛选涉及客户名)。""" stmt = ( select(func.count()) .select_from(Business) .outerjoin(Customer, Customer.id == Business.customer_id) .where(*_business_filters(query)) ) return int(session.execute(stmt).scalar_one()) def list_source_business_sales(session: Session, query: BusinessStatsQuery, *, after_id: int, limit: int): """键集分页读取筛选范围内的来源业务,不加载完整业务或用户数据。""" stmt = (select(Business.id, Business.total_sale_amount).select_from(Business) .outerjoin(Customer, Customer.id == Business.customer_id) .where(*_business_filters(query), Business.integration_json["source_database"].as_string().is_not(None), Business.id > after_id).order_by(Business.id).limit(limit)) return list(session.execute(stmt)) def sum_business_stats(session: Session, query: BusinessStatsQuery) -> dict: """业务统计全筛选合计(业务数/发货/回款/销售额/绩效/应付)。""" perf_sq = _business_perf_subquery() return ( session.execute( select( func.count().label("row_count"), func.coalesce(func.sum(Business.total_ship_amount), 0).label("ship_total"), func.coalesce(func.sum(Business.total_received_amount), 0).label("received_total"), func.coalesce(func.sum(Business.total_sale_amount), 0).label("sale_total"), func.coalesce(func.sum(perf_sq.c.perf), 0).label("performance_total"), func.coalesce(func.sum(perf_sq.c.payable), 0).label("payable_total"), ) .outerjoin(Customer, Customer.id == Business.customer_id) .outerjoin(perf_sq, perf_sq.c.business_id == Business.id) .where(*_business_filters(query)) ) .mappings() .one() ) # ==== 回款统计(FR-031 之二)==== def _payment_filters(query: PaymentStatsQuery | PaymentDetailQuery) -> tuple: filters = [*_ACTIVE_SINGLE] if query.settlement_month: filters.append(PerformanceLedger.settlement_month == query.settlement_month) if query.date_from is not None: filters.append(PerformanceLedger.receipt_date >= query.date_from) if query.date_to is not None: filters.append(PerformanceLedger.receipt_date <= query.date_to) if query.customer_type: filters.append(PerformanceLedger.customer_type == query.customer_type) if query.product_category: filters.append(PerformanceLedger.product_category == query.product_category) return tuple(filters) def group_payment_by_days(session: Session, query: PaymentStatsQuery) -> list: """按(回款天数精确值, 客户类型, 产品类别)聚合绩效与应付。""" return list( session.execute( select( PerformanceLedger.payment_days, PerformanceLedger.customer_type, PerformanceLedger.product_category, func.coalesce(func.sum(PerformanceLedger.performance_amount), 0).label( "performance_total" ), func.coalesce(func.sum(PerformanceLedger.payable_amount), 0).label("payable_total"), func.count().label("ledger_count"), ) .where(*_payment_filters(query)) .group_by( PerformanceLedger.payment_days, PerformanceLedger.customer_type, PerformanceLedger.product_category, ) ).all() ) def group_payment_receipt_by_days(session: Session, query: PaymentStatsQuery) -> list: """按维度聚合回款金额与笔数——金额侧必须按 receipt_id 去重。 二次分摊拆行使同一回款在台账出现多行,直接 SUM(receipts.amount) 会 按拆行数翻倍;台账拆行只改归属人,不改维度值(payment_days/客户类型/ 产品类别),因此先对台账行 DISTINCT 出回款维度,再联回款表求和。 """ distinct_receipts = ( select( PerformanceLedger.receipt_id.label("receipt_id"), PerformanceLedger.payment_days.label("payment_days"), PerformanceLedger.customer_type.label("customer_type"), PerformanceLedger.product_category.label("product_category"), ) .where(*_payment_filters(query), PerformanceLedger.receipt_id.is_not(None)) .distinct() .subquery() ) from app.models import Receipt return list( session.execute( select( distinct_receipts.c.payment_days, distinct_receipts.c.customer_type, distinct_receipts.c.product_category, func.coalesce(func.sum(Receipt.amount), 0).label("receipt_amount"), func.count(distinct_receipts.c.receipt_id).label("receipt_count"), ) .outerjoin(Receipt, Receipt.id == distinct_receipts.c.receipt_id) .group_by( distinct_receipts.c.payment_days, distinct_receipts.c.customer_type, distinct_receipts.c.product_category, ) ).all() ) def group_payment_by_customer_type(session: Session, query: PaymentStatsQuery) -> list: """按客户类型聚合(绩效 + 回款金额)。""" return _group_by_dimension(session, query, PerformanceLedger.customer_type) def group_payment_by_product_category(session: Session, query: PaymentStatsQuery) -> list: """按产品类别聚合(绩效 + 回款金额)。""" return _group_by_dimension(session, query, PerformanceLedger.product_category) def _group_by_dimension(session: Session, query: PaymentStatsQuery, dim_column) -> list: """维度聚合共用实现(绩效侧 GROUP BY + 金额侧 DISTINCT 去重联回款)。""" from app.models import Receipt dim_perf = ( select( dim_column.label("dim"), func.coalesce(func.sum(PerformanceLedger.performance_amount), 0).label( "performance_total" ), func.coalesce(func.sum(PerformanceLedger.payable_amount), 0).label("payable_total"), func.count().label("ledger_count"), ) .where(*_payment_filters(query)) .group_by(dim_column) ) dim_receipts = ( select( dim_column.label("dim"), PerformanceLedger.receipt_id.label("receipt_id"), ) .where(*_payment_filters(query), PerformanceLedger.receipt_id.is_not(None)) .distinct() .subquery() ) perf_rows = {row.dim: row for row in session.execute(dim_perf).all()} receipt_rows = session.execute( select( dim_receipts.c.dim, func.coalesce(func.sum(Receipt.amount), 0).label("receipt_amount"), ) .outerjoin(Receipt, Receipt.id == dim_receipts.c.receipt_id) .group_by(dim_receipts.c.dim) ).all() receipt_amounts = {row.dim: row.receipt_amount for row in receipt_rows} return [ ( row.dim, receipt_amounts.get(row.dim), row.performance_total, row.payable_total, row.ledger_count, ) for row in perf_rows.values() ] def _bucket_condition(bucket: DayBucket) -> tuple: """区间桶的 days 过滤条件:none 桶 IS NULL,其余按 min/max 闭区间。""" if bucket.code == "none": return (PerformanceLedger.payment_days.is_(None),) conds = [] if bucket.min_days is not None: conds.append(PerformanceLedger.payment_days >= bucket.min_days) if bucket.max_days is not None: conds.append(PerformanceLedger.payment_days <= bucket.max_days) return tuple(conds) def list_payment_detail(session: Session, query: PaymentDetailQuery, bucket: DayBucket) -> list: """区间下钻:单笔生效行联回款表(行级展示回款金额与批次号)。 bucket 由服务层解析(未注册编码 400),仓储只按区间拼条件。 """ from app.models import Receipt stmt = ( select( PerformanceLedger, Receipt.amount.label("receipt_amount"), Receipt.receipt_batch_no.label("receipt_batch_no"), ) .outerjoin(Receipt, Receipt.id == PerformanceLedger.receipt_id) .where(*_payment_filters(query), *_bucket_condition(bucket)) ) stmt = stmt.order_by(PerformanceLedger.receipt_date.desc(), PerformanceLedger.id.desc()) stmt = stmt.offset(query.offset).limit(query.page_size) return list(session.execute(stmt).all()) def count_payment_detail(session: Session, query: PaymentDetailQuery, bucket: DayBucket) -> int: """区间下钻计数(与 list_payment_detail 同筛选)。""" return int( session.execute( select(func.count()) .select_from(PerformanceLedger) .where(*_payment_filters(query), *_bucket_condition(bucket)) ).scalar_one() )