feat/m3-increments #1
@ -0,0 +1,632 @@
|
||||
"""V7 核算台账/月结/调整单四表与调整单开关种子
|
||||
|
||||
Revision ID: 3bca400421c6
|
||||
Revises: 79bcc5aa98d4
|
||||
Create Date: 2026-10-16 15:54:53.000890
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from datetime import date
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "3bca400421c6"
|
||||
down_revision: str | Sequence[str] | None = "79bcc5aa98d4"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""应用迁移。"""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"monthly_settlements",
|
||||
sa.Column(
|
||||
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||
),
|
||||
sa.Column(
|
||||
"settlement_month",
|
||||
sa.String(length=7),
|
||||
nullable=False,
|
||||
comment="结算月份(YYYY-MM;全表唯一)",
|
||||
),
|
||||
sa.Column(
|
||||
"status",
|
||||
mysql.TINYINT(),
|
||||
server_default="1",
|
||||
nullable=False,
|
||||
comment="状态:1草稿 2待审批 3已审定(终态=已锁定)4已驳回(回草稿)",
|
||||
),
|
||||
sa.Column(
|
||||
"total_performance",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=True,
|
||||
comment="绩效金额合计(可负)",
|
||||
),
|
||||
sa.Column(
|
||||
"total_payable",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=True,
|
||||
comment="应付绩效合计(可负)",
|
||||
),
|
||||
sa.Column(
|
||||
"person_count", mysql.TINYINT(), server_default="0", nullable=False, comment="结算人数"
|
||||
),
|
||||
sa.Column(
|
||||
"snapshot_json",
|
||||
sa.JSON(),
|
||||
nullable=True,
|
||||
comment="月度总览快照(生成时刻的确认回款口径说明与汇总参数)",
|
||||
),
|
||||
sa.Column(
|
||||
"approval_order_no",
|
||||
sa.String(length=32),
|
||||
nullable=True,
|
||||
comment="关联审批单号(冗余固化;开关关闭自动通过时也有单)",
|
||||
),
|
||||
sa.Column(
|
||||
"submitter_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=True,
|
||||
comment="提交人ID(账号删除后置空)",
|
||||
),
|
||||
sa.Column(
|
||||
"submitter_name",
|
||||
sa.String(length=64),
|
||||
nullable=False,
|
||||
comment="提交人用户名(冗余固化)",
|
||||
),
|
||||
sa.Column("locked_at", sa.DateTime(), nullable=True, comment="锁定时间(status=3 时回填)"),
|
||||
sa.Column(
|
||||
"locked_by", sa.String(length=64), nullable=True, comment="锁定操作人(冗余固化)"
|
||||
),
|
||||
sa.Column(
|
||||
"create_time",
|
||||
sa.DateTime(),
|
||||
server_default=sa.text("CURRENT_TIMESTAMP"),
|
||||
nullable=False,
|
||||
comment="创建时间",
|
||||
),
|
||||
sa.Column(
|
||||
"update_time",
|
||||
sa.DateTime(),
|
||||
server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
|
||||
nullable=False,
|
||||
comment="更新时间",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["submitter_id"],
|
||||
["users.id"],
|
||||
name=op.f("fk_monthly_settlements_submitter_id"),
|
||||
ondelete="SET NULL",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_monthly_settlements")),
|
||||
sa.UniqueConstraint(
|
||||
"settlement_month", name=op.f("uk_monthly_settlements_settlement_month")
|
||||
),
|
||||
comment="月度结算表(按月一行;通过即锁定,锁定后走调整单)",
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_collate="utf8mb4_general_ci",
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_table(
|
||||
"adjustment_orders",
|
||||
sa.Column(
|
||||
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||
),
|
||||
sa.Column(
|
||||
"order_no",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
comment="调整单号,自动生成 TZ{YYYYMMDD}-{4位当日序号}",
|
||||
),
|
||||
sa.Column(
|
||||
"settlement_month",
|
||||
sa.String(length=7),
|
||||
nullable=False,
|
||||
comment="调整月份(YYYY-MM;跨月规则待澄清 #9 前不限制)",
|
||||
),
|
||||
sa.Column(
|
||||
"personnel_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
comment="调整人员ID(personnel.id)",
|
||||
),
|
||||
sa.Column(
|
||||
"personnel_name",
|
||||
sa.String(length=64),
|
||||
nullable=False,
|
||||
comment="调整人员姓名(冗余固化)",
|
||||
),
|
||||
sa.Column("direction", mysql.TINYINT(), nullable=False, comment="方向:1补发 2扣减"),
|
||||
sa.Column(
|
||||
"amount",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=False,
|
||||
comment="调整金额(恒正,方向表达语义)",
|
||||
),
|
||||
sa.Column(
|
||||
"reason",
|
||||
sa.String(length=255),
|
||||
nullable=False,
|
||||
comment="扣补说明(必填,审批与追溯锚点)",
|
||||
),
|
||||
sa.Column(
|
||||
"status",
|
||||
mysql.TINYINT(),
|
||||
server_default="1",
|
||||
nullable=False,
|
||||
comment="状态:1待审批 2已生效 3已驳回 4已撤回",
|
||||
),
|
||||
sa.Column(
|
||||
"submitter_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=True,
|
||||
comment="提交人ID(账号删除后置空)",
|
||||
),
|
||||
sa.Column(
|
||||
"submitter_name",
|
||||
sa.String(length=64),
|
||||
nullable=False,
|
||||
comment="提交人用户名(冗余固化)",
|
||||
),
|
||||
sa.Column(
|
||||
"create_time",
|
||||
sa.DateTime(),
|
||||
server_default=sa.text("CURRENT_TIMESTAMP"),
|
||||
nullable=False,
|
||||
comment="创建时间",
|
||||
),
|
||||
sa.Column(
|
||||
"update_time",
|
||||
sa.DateTime(),
|
||||
server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
|
||||
nullable=False,
|
||||
comment="更新时间",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["personnel_id"],
|
||||
["personnel.id"],
|
||||
name=op.f("fk_adjustment_orders_personnel_id"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["submitter_id"],
|
||||
["users.id"],
|
||||
name=op.f("fk_adjustment_orders_submitter_id"),
|
||||
ondelete="SET NULL",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_adjustment_orders")),
|
||||
sa.UniqueConstraint("order_no", name=op.f("uk_adjustment_orders_order_no")),
|
||||
comment="调整单表(已锁定月份的补发/扣减;走审批;生效后不回写月结快照)",
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_collate="utf8mb4_general_ci",
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_table(
|
||||
"settlement_items",
|
||||
sa.Column(
|
||||
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||
),
|
||||
sa.Column(
|
||||
"settlement_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
comment="月结ID(monthly_settlements.id;头删明细随删)",
|
||||
),
|
||||
sa.Column(
|
||||
"personnel_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
comment="结算人员ID(personnel.id)",
|
||||
),
|
||||
sa.Column(
|
||||
"personnel_name",
|
||||
sa.String(length=64),
|
||||
nullable=False,
|
||||
comment="结算人员姓名(冗余固化)",
|
||||
),
|
||||
sa.Column(
|
||||
"role_code", sa.String(length=32), nullable=False, comment="岗位编码(主岗,聚合锚点)"
|
||||
),
|
||||
sa.Column(
|
||||
"calc_type",
|
||||
mysql.TINYINT(),
|
||||
nullable=False,
|
||||
comment="核算类型:1单笔回款 2经理团队聚合",
|
||||
),
|
||||
sa.Column(
|
||||
"performance_sum",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=False,
|
||||
comment="绩效金额合计(可负;聚合行排除经理个人户业绩,#12 暂口径)",
|
||||
),
|
||||
sa.Column(
|
||||
"payable_sum",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=False,
|
||||
comment="应付绩效合计(可负)",
|
||||
),
|
||||
sa.Column(
|
||||
"basic_amount",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=True,
|
||||
comment="月工资标准(personnel.salary_standard 当月快照)",
|
||||
),
|
||||
sa.Column(
|
||||
"salary_mode",
|
||||
sa.String(length=16),
|
||||
nullable=True,
|
||||
comment="月工资口径快照:INCLUDE/DEDUCT/EXCLUDE",
|
||||
),
|
||||
sa.Column(
|
||||
"net_amount",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=True,
|
||||
comment="净工资 = 绩效±基本工资(按口径;试用期 0)",
|
||||
),
|
||||
sa.Column(
|
||||
"is_probation",
|
||||
mysql.TINYINT(),
|
||||
server_default="0",
|
||||
nullable=False,
|
||||
comment="试用期:1绩效照算不发放(net=0,超年累计记 note)0否",
|
||||
),
|
||||
sa.Column(
|
||||
"probation_note",
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
comment="试用期说明(年累计超阈值部分年底一次性结算)",
|
||||
),
|
||||
sa.Column(
|
||||
"snapshot_json", sa.JSON(), nullable=True, comment="个人明细快照(台账行引用列表)"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["personnel_id"],
|
||||
["personnel.id"],
|
||||
name=op.f("fk_settlement_items_personnel_id"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["settlement_id"],
|
||||
["monthly_settlements.id"],
|
||||
name=op.f("fk_settlement_items_settlement_id"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_settlement_items")),
|
||||
comment="月结明细表(按人固化;支持负数工资;锁定后只读)",
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_collate="utf8mb4_general_ci",
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_table(
|
||||
"performance_ledgers",
|
||||
sa.Column(
|
||||
"id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID"
|
||||
),
|
||||
sa.Column(
|
||||
"receipt_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=True,
|
||||
comment="回款ID(receipts.id;聚合行可为 NULL)",
|
||||
),
|
||||
sa.Column(
|
||||
"calc_version",
|
||||
mysql.TINYINT(),
|
||||
server_default="1",
|
||||
nullable=False,
|
||||
comment="核算版本(重算+1,旧行 is_active=0)",
|
||||
),
|
||||
sa.Column(
|
||||
"is_active",
|
||||
mysql.TINYINT(),
|
||||
server_default="1",
|
||||
nullable=False,
|
||||
comment="生效标记:1当前生效 0已被重算替代(重算受控 UPDATE 例外)",
|
||||
),
|
||||
sa.Column(
|
||||
"calc_type",
|
||||
mysql.TINYINT(),
|
||||
server_default="1",
|
||||
nullable=False,
|
||||
comment="核算类型:1单笔回款 2经理团队聚合(月度)",
|
||||
),
|
||||
sa.Column(
|
||||
"business_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
comment="业务ID(固化无外键;聚合行为该经理名下首行业务ID,报表下钻锚点)",
|
||||
),
|
||||
sa.Column(
|
||||
"business_no",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
comment="业务编号(冗余固化,列表免联查)",
|
||||
),
|
||||
sa.Column(
|
||||
"personnel_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
comment="归属营销人员ID(personnel.id;核算归属锚点)",
|
||||
),
|
||||
sa.Column(
|
||||
"personnel_name",
|
||||
sa.String(length=64),
|
||||
nullable=False,
|
||||
comment="归属营销人员姓名(冗余固化)",
|
||||
),
|
||||
sa.Column(
|
||||
"role_code",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
comment="岗位编码(business_roles.role_code,计提公式锚点)",
|
||||
),
|
||||
sa.Column(
|
||||
"customer_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
comment="客户分类:personal/manager_personal/enterprise",
|
||||
),
|
||||
sa.Column(
|
||||
"product_category",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
comment="产品类别:municipal_water/rail_transit/mixed(混合业务,行级明细入 snapshot_json)",
|
||||
),
|
||||
sa.Column("ship_date", sa.Date(), nullable=True, comment="发货日期(回款天数基准)"),
|
||||
sa.Column(
|
||||
"receipt_date",
|
||||
sa.Date(),
|
||||
nullable=False,
|
||||
comment="回款日期(参数版本匹配日,裁决 #11)",
|
||||
),
|
||||
sa.Column(
|
||||
"payment_days",
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
comment="回款天数(发货日首日计第 1 天;未指定批次为 NULL;先款后货照实负值)",
|
||||
),
|
||||
sa.Column(
|
||||
"rate_text",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
comment="计提比例原文(如 4/360,防 Fraction 截断;聚合行存 12.5%/5% 结构说明)",
|
||||
),
|
||||
sa.Column(
|
||||
"converted_profit",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=True,
|
||||
comment="折算毛利润额(可负;混合业务为各行汇总)",
|
||||
),
|
||||
sa.Column(
|
||||
"performance_amount",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=True,
|
||||
comment="绩效金额(可负,裁决:负绩效照算不归零)",
|
||||
),
|
||||
sa.Column(
|
||||
"barter_applied",
|
||||
mysql.TINYINT(),
|
||||
server_default="0",
|
||||
nullable=False,
|
||||
comment="以物抵顶折算:1已按 80% 折算 0否",
|
||||
),
|
||||
sa.Column(
|
||||
"weight_r",
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
comment="支付权重 R(回款占销售额百分比向下取整)",
|
||||
),
|
||||
sa.Column(
|
||||
"payment_weight",
|
||||
sa.Numeric(precision=7, scale=6),
|
||||
nullable=True,
|
||||
comment="支付权重系数 R×R(展示用)",
|
||||
),
|
||||
sa.Column(
|
||||
"payable_amount",
|
||||
sa.Numeric(precision=14, scale=2),
|
||||
nullable=True,
|
||||
comment="当期应付绩效(绩效 × 支付权重;聚合行为当月汇总)",
|
||||
),
|
||||
sa.Column(
|
||||
"settlement_month",
|
||||
sa.String(length=7),
|
||||
nullable=False,
|
||||
comment="归属月(回款日期所在月,YYYY-MM;月结与报表维度)",
|
||||
),
|
||||
sa.Column(
|
||||
"is_probation",
|
||||
mysql.TINYINT(),
|
||||
server_default="0",
|
||||
nullable=False,
|
||||
comment="试用期:1照算不发放 0否",
|
||||
),
|
||||
sa.Column(
|
||||
"missing_cost",
|
||||
mysql.TINYINT(),
|
||||
server_default="0",
|
||||
nullable=False,
|
||||
comment="成本缺失:1有明细行缺成本(该行毛利按 0 计)0否",
|
||||
),
|
||||
sa.Column(
|
||||
"snapshot_json",
|
||||
sa.JSON(),
|
||||
nullable=True,
|
||||
comment="计算快照(所用参数版本与行级折算明细,重算可回溯)",
|
||||
),
|
||||
sa.Column(
|
||||
"settlement_id",
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=True,
|
||||
comment="所属月结ID(月结生成时回填;NULL=未入结)",
|
||||
),
|
||||
sa.Column(
|
||||
"calc_reason",
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
comment="核算/重算原因(重算必填留痕)",
|
||||
),
|
||||
sa.Column("remark", sa.String(length=255), nullable=True, comment="备注"),
|
||||
sa.Column(
|
||||
"create_time",
|
||||
sa.DateTime(),
|
||||
server_default=sa.text("CURRENT_TIMESTAMP"),
|
||||
nullable=False,
|
||||
comment="创建时间",
|
||||
),
|
||||
sa.Column(
|
||||
"update_time",
|
||||
sa.DateTime(),
|
||||
server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
|
||||
nullable=False,
|
||||
comment="更新时间",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["personnel_id"],
|
||||
["personnel.id"],
|
||||
name=op.f("fk_performance_ledgers_personnel_id"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["receipt_id"],
|
||||
["receipts.id"],
|
||||
name=op.f("fk_performance_ledgers_receipt_id"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["settlement_id"],
|
||||
["monthly_settlements.id"],
|
||||
name=op.f("fk_performance_ledgers_settlement_id"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_performance_ledgers")),
|
||||
sa.UniqueConstraint(
|
||||
"receipt_id", "calc_version", name="uk_performance_ledgers_receipt_id_calc_version"
|
||||
),
|
||||
comment="绩效核算台账表(append-only 版本行;重算插新版本旧行仅翻 is_active)",
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_collate="utf8mb4_general_ci",
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_index(
|
||||
"idx_performance_ledgers_business_id", "performance_ledgers", ["business_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
"idx_performance_ledgers_month_active",
|
||||
"performance_ledgers",
|
||||
["settlement_month", "is_active"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"idx_performance_ledgers_month_personnel_type",
|
||||
"performance_ledgers",
|
||||
["settlement_month", "personnel_id", "calc_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"idx_performance_ledgers_personnel_id",
|
||||
"performance_ledgers",
|
||||
["personnel_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.alter_column(
|
||||
"approval_orders",
|
||||
"order_type",
|
||||
existing_type=mysql.TINYINT(),
|
||||
comment="审批对象类型:1二次分配方案 2月度结算 3调整单",
|
||||
existing_comment="审批对象类型:1二次分配方案 2月度结算(预留)",
|
||||
existing_nullable=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
# ---- 种子:调整单审批开关(V6 写法先例;definitions id 19 / values id 22)----
|
||||
op.bulk_insert(
|
||||
sa.table(
|
||||
"param_definitions",
|
||||
sa.column("id", sa.BigInteger),
|
||||
sa.column("param_code", sa.String),
|
||||
sa.column("param_name", sa.String),
|
||||
sa.column("param_group", sa.String),
|
||||
sa.column("value_type", sa.String),
|
||||
sa.column("unit", sa.String),
|
||||
sa.column("dimension_type", sa.String),
|
||||
sa.column("options_text", sa.String),
|
||||
sa.column("default_value", sa.String),
|
||||
sa.column("value_min", sa.String),
|
||||
sa.column("value_max", sa.String),
|
||||
sa.column("description", sa.String),
|
||||
sa.column("sort_order", sa.Integer),
|
||||
sa.column("status", sa.Integer),
|
||||
),
|
||||
[
|
||||
{
|
||||
"id": 19,
|
||||
"param_code": "approval.adjustment.enabled",
|
||||
"param_name": "调整单审批开关",
|
||||
"param_group": "审批开关",
|
||||
"value_type": "BOOL",
|
||||
"unit": None,
|
||||
"dimension_type": "NONE",
|
||||
"options_text": None,
|
||||
"default_value": "0",
|
||||
"value_min": None,
|
||||
"value_max": None,
|
||||
"description": (
|
||||
"0=调整单提交即生效(系统自动通过留痕);1=走财务复核→领导审核审批流。"
|
||||
"调整单是已锁定月份补发/扣减的唯一通道"
|
||||
),
|
||||
"sort_order": 19,
|
||||
"status": 1,
|
||||
}
|
||||
],
|
||||
)
|
||||
op.bulk_insert(
|
||||
sa.table(
|
||||
"param_values",
|
||||
sa.column("id", sa.BigInteger),
|
||||
sa.column("param_id", sa.BigInteger),
|
||||
sa.column("dimension_value", sa.String),
|
||||
sa.column("effective_date", sa.Date),
|
||||
sa.column("value_text", sa.String),
|
||||
sa.column("batch_id", sa.BigInteger),
|
||||
),
|
||||
[
|
||||
{
|
||||
"id": 22,
|
||||
"param_id": 19,
|
||||
"dimension_value": "",
|
||||
"effective_date": date(2026, 1, 1),
|
||||
"value_text": "0",
|
||||
"batch_id": 1,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""回滚迁移。
|
||||
|
||||
整表回滚只保留 `op.drop_table`(V1/V3/V6 先例:外键唯一支撑索引单独
|
||||
drop 会报 MySQL 1553)。行级种子(param_definitions 19 / param_values 22)
|
||||
的宿主表由 V3 建、本迁移只插行,须显式 DELETE 否则重放 upgrade 撞唯一键。
|
||||
删除顺序:先删引用方(ledger/items),再删被引用方(头表/调整单)。
|
||||
"""
|
||||
op.drop_table("performance_ledgers")
|
||||
op.drop_table("settlement_items")
|
||||
op.drop_table("adjustment_orders")
|
||||
op.drop_table("monthly_settlements")
|
||||
op.alter_column(
|
||||
"approval_orders",
|
||||
"order_type",
|
||||
existing_type=mysql.TINYINT(),
|
||||
comment="审批对象类型:1二次分配方案 2月度结算(预留)",
|
||||
existing_comment="审批对象类型:1二次分配方案 2月度结算 3调整单",
|
||||
existing_nullable=False,
|
||||
)
|
||||
op.execute("DELETE FROM param_values WHERE id = 22")
|
||||
op.execute("DELETE FROM param_definitions WHERE id = 19")
|
||||
@ -85,6 +85,8 @@ MODULE_RECEIPT = "回款管理"
|
||||
MODULE_RATING = "客户评级"
|
||||
MODULE_SYSTEM_SETTINGS = "系统设置"
|
||||
MODULE_APPROVAL = "审批管理"
|
||||
MODULE_CALC = "绩效核算"
|
||||
MODULE_SETTLEMENT = "月度结算"
|
||||
|
||||
# ===== 单据编号前缀({前缀}{YYYYMMDD}-{当日4位序号},序号按自然日重置)=====
|
||||
# 一期为代码常量(业务未提出配置诉求);如需可配置,走 M1 参数体系新增参数组
|
||||
@ -93,6 +95,7 @@ NO_PREFIX_SHIPMENT = "SH"
|
||||
NO_PREFIX_RECEIPT = "HK"
|
||||
NO_PREFIX_PARAM = "P"
|
||||
NO_PREFIX_APPROVAL = "AP"
|
||||
NO_PREFIX_ADJUSTMENT = "TZ"
|
||||
|
||||
ACTION_CREATE = "新增"
|
||||
ACTION_UPDATE = "修改"
|
||||
|
||||
@ -17,16 +17,19 @@ from app.models.customer import Customer, OwnershipChangeLog
|
||||
from app.models.login_log import LoginLog
|
||||
from app.models.operation_log import OperationLog
|
||||
from app.models.param import ParamChangeBatch, ParamDefinition, ParamValue
|
||||
from app.models.performance_ledger import PerformanceLedger
|
||||
from app.models.personnel import Personnel, PersonnelPlan, PersonnelRegion
|
||||
from app.models.product import Product
|
||||
from app.models.rating import CustomerRatingGrade
|
||||
from app.models.receipt import Receipt
|
||||
from app.models.role import Permission, Role, RolePermission
|
||||
from app.models.settlement import AdjustmentOrder, MonthlySettlement, SettlementItem
|
||||
from app.models.shipment import Shipment, ShipmentItem
|
||||
from app.models.system_setting import SystemSetting
|
||||
from app.models.user import User, UserRole
|
||||
|
||||
__all__ = [
|
||||
"AdjustmentOrder",
|
||||
"ApprovalOrder",
|
||||
"ApprovalStep",
|
||||
"Base",
|
||||
@ -37,11 +40,13 @@ __all__ = [
|
||||
"Customer",
|
||||
"CustomerRatingGrade",
|
||||
"LoginLog",
|
||||
"MonthlySettlement",
|
||||
"OperationLog",
|
||||
"OwnershipChangeLog",
|
||||
"ParamChangeBatch",
|
||||
"ParamDefinition",
|
||||
"ParamValue",
|
||||
"PerformanceLedger",
|
||||
"Permission",
|
||||
"Personnel",
|
||||
"PersonnelPlan",
|
||||
@ -51,6 +56,7 @@ __all__ = [
|
||||
"Role",
|
||||
"RolePermission",
|
||||
"SecondaryAllocation",
|
||||
"SettlementItem",
|
||||
"Shipment",
|
||||
"ShipmentItem",
|
||||
"SystemSetting",
|
||||
|
||||
@ -42,9 +42,10 @@ APPROVAL_ACTION_REJECT = 2 # 人工驳回
|
||||
APPROVAL_ACTION_AUTO_APPROVE = 3 # 系统自动通过(开关关闭时逐节点留痕)
|
||||
APPROVAL_ACTION_WITHDRAW = 4 # 撤回(业务重新提交时旧在途单注销)
|
||||
|
||||
# 审批对象类型(approval_orders.order_type;2 为 ④ 月结预留)
|
||||
# 审批对象类型(approval_orders.order_type)
|
||||
APPROVAL_TYPE_SECONDARY_ALLOCATION = 1
|
||||
APPROVAL_TYPE_MONTHLY_SETTLEMENT = 2
|
||||
APPROVAL_TYPE_ADJUSTMENT = 3
|
||||
|
||||
|
||||
class ApprovalOrder(Base, TimestampMixin):
|
||||
@ -66,7 +67,7 @@ class ApprovalOrder(Base, TimestampMixin):
|
||||
comment="审批单号,自动生成 AP{YYYYMMDD}-{4位当日序号}",
|
||||
)
|
||||
order_type: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE, nullable=False, comment="审批对象类型:1二次分配方案 2月度结算(预留)"
|
||||
TINYINT_TYPE, nullable=False, comment="审批对象类型:1二次分配方案 2月度结算 3调整单"
|
||||
)
|
||||
ref_id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
|
||||
173
backend/app/models/performance_ledger.py
Normal file
173
backend/app/models/performance_ledger.py
Normal file
@ -0,0 +1,173 @@
|
||||
"""绩效核算台账模型(M3 ④:每笔回款仅核算一次的固化点)。
|
||||
|
||||
**append-only 版本行**:重算 = 插入 calc_version+1 的新行,旧行仅把
|
||||
is_active 翻 0 —— 这是全表唯一的受控 UPDATE(模型注释与 test_models
|
||||
均已声明例外),行内容永不改写;历史版本可回溯(重算授权留痕的需求口径)。
|
||||
|
||||
防重三层:服务层 CAS(UPDATE ... WHERE settlement_status=0,rowcount=0
|
||||
即 400)+ FOR UPDATE 行锁 + 库级 uk(receipt_id, calc_version) 兜底。
|
||||
|
||||
经理聚合行(calc_type=2,receipt_id 为 NULL):月度批处理时按「本团队
|
||||
业务员绩效 ×12.5% + 全公司 ×5%」落一行;其唯一性由月结生成的
|
||||
settlement_month 串行化 + 服务层保证(MySQL 无部分唯一索引,docstring
|
||||
声明即可)。
|
||||
|
||||
#12 策略点(待业务答复):经理个人户业绩暂不计入聚合基数——聚合集中
|
||||
在 settlement_service 的聚合函数,答复后只改该函数。
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Date,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Numeric,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, mysql_table_args
|
||||
|
||||
# 核算类型(performance_ledgers.calc_type)
|
||||
CALC_TYPE_RECEIPT = 1 # 单笔回款核算
|
||||
CALC_TYPE_MANAGER_AGGREGATE = 2 # 经理团队聚合(月度批处理)
|
||||
|
||||
|
||||
class PerformanceLedger(Base, TimestampMixin):
|
||||
"""绩效核算台账表:核算事实的固化点,报表 2.4.1 的数据源。"""
|
||||
|
||||
__tablename__ = "performance_ledgers"
|
||||
__table_args__ = mysql_table_args(
|
||||
"绩效核算台账表(append-only 版本行;重算插新版本旧行仅翻 is_active)",
|
||||
# 库级防重兜底:同一回款同版本仅一行(服务层 CAS+行锁之外的第二道
|
||||
# 闸);聚合行 receipt_id 为 NULL,MySQL 唯一索引对 NULL 不去重,
|
||||
# 天然不约束聚合行
|
||||
UniqueConstraint(
|
||||
"receipt_id", "calc_version", name="uk_performance_ledgers_receipt_id_calc_version"
|
||||
),
|
||||
Index("idx_performance_ledgers_month_active", "settlement_month", "is_active"),
|
||||
Index(
|
||||
"idx_performance_ledgers_month_personnel_type",
|
||||
"settlement_month",
|
||||
"personnel_id",
|
||||
"calc_type",
|
||||
),
|
||||
Index("idx_performance_ledgers_personnel_id", "personnel_id"),
|
||||
Index("idx_performance_ledgers_business_id", "business_id"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||
)
|
||||
receipt_id: Mapped[int | None] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("receipts.id", ondelete="RESTRICT"),
|
||||
comment="回款ID(receipts.id;聚合行可为 NULL)",
|
||||
)
|
||||
calc_version: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="1",
|
||||
comment="核算版本(重算+1,旧行 is_active=0)",
|
||||
)
|
||||
is_active: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="1",
|
||||
comment="生效标记:1当前生效 0已被重算替代(重算受控 UPDATE 例外)",
|
||||
)
|
||||
calc_type: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="1",
|
||||
comment="核算类型:1单笔回款 2经理团队聚合(月度)",
|
||||
)
|
||||
business_id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
comment="业务ID(固化无外键;聚合行为该经理名下首行业务ID,报表下钻锚点)",
|
||||
)
|
||||
business_no: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, comment="业务编号(冗余固化,列表免联查)"
|
||||
)
|
||||
personnel_id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("personnel.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
comment="归属营销人员ID(personnel.id;核算归属锚点)",
|
||||
)
|
||||
personnel_name: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="归属营销人员姓名(冗余固化)"
|
||||
)
|
||||
role_code: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, comment="岗位编码(business_roles.role_code,计提公式锚点)"
|
||||
)
|
||||
customer_type: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, comment="客户分类:personal/manager_personal/enterprise"
|
||||
)
|
||||
product_category: Mapped[str] = mapped_column(
|
||||
String(32),
|
||||
nullable=False,
|
||||
comment="产品类别:municipal_water/rail_transit/mixed(混合业务,行级明细入 snapshot_json)",
|
||||
)
|
||||
ship_date: Mapped[date | None] = mapped_column(Date, comment="发货日期(回款天数基准)")
|
||||
receipt_date: Mapped[date] = mapped_column(
|
||||
Date, nullable=False, comment="回款日期(参数版本匹配日,裁决 #11)"
|
||||
)
|
||||
payment_days: Mapped[int | None] = mapped_column(
|
||||
comment="回款天数(发货日首日计第 1 天;未指定批次为 NULL;先款后货照实负值)"
|
||||
)
|
||||
rate_text: Mapped[str] = mapped_column(
|
||||
String(32),
|
||||
nullable=False,
|
||||
comment="计提比例原文(如 4/360,防 Fraction 截断;聚合行存 12.5%/5% 结构说明)",
|
||||
)
|
||||
converted_profit: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(14, 2), comment="折算毛利润额(可负;混合业务为各行汇总)"
|
||||
)
|
||||
performance_amount: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(14, 2), comment="绩效金额(可负,裁决:负绩效照算不归零)"
|
||||
)
|
||||
barter_applied: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="0",
|
||||
comment="以物抵顶折算:1已按 80% 折算 0否",
|
||||
)
|
||||
weight_r: Mapped[int | None] = mapped_column(comment="支付权重 R(回款占销售额百分比向下取整)")
|
||||
payment_weight: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(7, 6), comment="支付权重系数 R×R(展示用)"
|
||||
)
|
||||
payable_amount: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(14, 2), comment="当期应付绩效(绩效 × 支付权重;聚合行为当月汇总)"
|
||||
)
|
||||
settlement_month: Mapped[str] = mapped_column(
|
||||
String(7), nullable=False, comment="归属月(回款日期所在月,YYYY-MM;月结与报表维度)"
|
||||
)
|
||||
is_probation: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="0",
|
||||
comment="试用期:1照算不发放 0否",
|
||||
)
|
||||
missing_cost: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="0",
|
||||
comment="成本缺失:1有明细行缺成本(该行毛利按 0 计)0否",
|
||||
)
|
||||
snapshot_json: Mapped[dict | None] = mapped_column(
|
||||
JSON, comment="计算快照(所用参数版本与行级折算明细,重算可回溯)"
|
||||
)
|
||||
settlement_id: Mapped[int | None] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("monthly_settlements.id", ondelete="RESTRICT"),
|
||||
comment="所属月结ID(月结生成时回填;NULL=未入结)",
|
||||
)
|
||||
calc_reason: Mapped[str | None] = mapped_column(
|
||||
String(255), comment="核算/重算原因(重算必填留痕)"
|
||||
)
|
||||
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")
|
||||
197
backend/app/models/settlement.py
Normal file
197
backend/app/models/settlement.py
Normal file
@ -0,0 +1,197 @@
|
||||
"""月度结算与调整单模型(M3 ④:月结审批报表补充需求)。
|
||||
|
||||
- 月度结算按「确认回款」和计算快照生成;**通过即锁定**(status=3 已审定
|
||||
即终态,锁定后源数据改动只产生提示与后续调整)
|
||||
- settlement_items 明细按人固化(可负=负数工资),锁定后无 UPDATE 路径
|
||||
(重生成 = 草稿/驳回态删旧插新)
|
||||
- 调整单是已锁定月份的唯一调整通道(补发/扣减,走审批;生效后不回写
|
||||
已锁定月结——append-only,报表汇总并入)
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import JSON, DateTime, ForeignKey, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, mysql_table_args
|
||||
|
||||
# 月结状态(monthly_settlements.status;通过即锁定——两态合一)
|
||||
SETTLEMENT_STATUS_DRAFT = 1 # 草稿(可重新生成/提交)
|
||||
SETTLEMENT_STATUS_PENDING = 2 # 待审批(提交后)
|
||||
SETTLEMENT_STATUS_APPROVED = 3 # 已审定(终态=已锁定)
|
||||
SETTLEMENT_STATUS_REJECTED = 4 # 已驳回(回草稿可重提)
|
||||
|
||||
# 调整单状态(adjustment_orders.status)
|
||||
ADJUSTMENT_STATUS_PENDING = 1 # 待审批
|
||||
ADJUSTMENT_STATUS_EFFECTIVE = 2 # 已生效
|
||||
ADJUSTMENT_STATUS_REJECTED = 3 # 已驳回
|
||||
ADJUSTMENT_STATUS_WITHDRAWN = 4 # 已撤回
|
||||
|
||||
# 调整方向(adjustment_orders.direction)
|
||||
ADJUSTMENT_DIRECTION_EXTRA = 1 # 补发
|
||||
ADJUSTMENT_DIRECTION_DEDUCT = 2 # 扣减
|
||||
|
||||
|
||||
class MonthlySettlement(Base, TimestampMixin):
|
||||
"""月度结算表:按月一行(settlement_month 唯一),快照 + 审批 + 锁定。"""
|
||||
|
||||
__tablename__ = "monthly_settlements"
|
||||
__table_args__ = mysql_table_args("月度结算表(按月一行;通过即锁定,锁定后走调整单)")
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||
)
|
||||
settlement_month: Mapped[str] = mapped_column(
|
||||
String(7), nullable=False, unique=True, comment="结算月份(YYYY-MM;全表唯一)"
|
||||
)
|
||||
status: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="1",
|
||||
comment="状态:1草稿 2待审批 3已审定(终态=已锁定)4已驳回(回草稿)",
|
||||
)
|
||||
total_performance: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(14, 2), comment="绩效金额合计(可负)"
|
||||
)
|
||||
total_payable: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(14, 2), comment="应付绩效合计(可负)"
|
||||
)
|
||||
person_count: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE, nullable=False, server_default="0", comment="结算人数"
|
||||
)
|
||||
snapshot_json: Mapped[dict | None] = mapped_column(
|
||||
JSON, comment="月度总览快照(生成时刻的确认回款口径说明与汇总参数)"
|
||||
)
|
||||
approval_order_no: Mapped[str | None] = mapped_column(
|
||||
String(32), comment="关联审批单号(冗余固化;开关关闭自动通过时也有单)"
|
||||
)
|
||||
submitter_id: Mapped[int | None] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
comment="提交人ID(账号删除后置空)",
|
||||
)
|
||||
submitter_name: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="提交人用户名(冗余固化)"
|
||||
)
|
||||
locked_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, comment="锁定时间(status=3 时回填)"
|
||||
)
|
||||
locked_by: Mapped[str | None] = mapped_column(String(64), comment="锁定操作人(冗余固化)")
|
||||
|
||||
|
||||
class SettlementItem(Base):
|
||||
"""月结明细表:按人按核算类型固化金额(锁定后只读;重生成=草稿态删旧插新)。"""
|
||||
|
||||
__tablename__ = "settlement_items"
|
||||
__table_args__ = mysql_table_args(
|
||||
"月结明细表(按人固化;支持负数工资;锁定后只读)",
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||
)
|
||||
settlement_id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("monthly_settlements.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
comment="月结ID(monthly_settlements.id;头删明细随删)",
|
||||
)
|
||||
personnel_id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("personnel.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
comment="结算人员ID(personnel.id)",
|
||||
)
|
||||
personnel_name: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="结算人员姓名(冗余固化)"
|
||||
)
|
||||
role_code: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, comment="岗位编码(主岗,聚合锚点)"
|
||||
)
|
||||
calc_type: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE, nullable=False, comment="核算类型:1单笔回款 2经理团队聚合"
|
||||
)
|
||||
performance_sum: Mapped[Decimal] = mapped_column(
|
||||
Numeric(14, 2),
|
||||
nullable=False,
|
||||
comment="绩效金额合计(可负;聚合行排除经理个人户业绩,#12 暂口径)",
|
||||
)
|
||||
payable_sum: Mapped[Decimal] = mapped_column(
|
||||
Numeric(14, 2), nullable=False, comment="应付绩效合计(可负)"
|
||||
)
|
||||
basic_amount: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(14, 2), comment="月工资标准(personnel.salary_standard 当月快照)"
|
||||
)
|
||||
salary_mode: Mapped[str | None] = mapped_column(
|
||||
String(16), comment="月工资口径快照:INCLUDE/DEDUCT/EXCLUDE"
|
||||
)
|
||||
net_amount: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(14, 2), comment="净工资 = 绩效±基本工资(按口径;试用期 0)"
|
||||
)
|
||||
is_probation: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="0",
|
||||
comment="试用期:1绩效照算不发放(net=0,超年累计记 note)0否",
|
||||
)
|
||||
probation_note: Mapped[str | None] = mapped_column(
|
||||
String(255), comment="试用期说明(年累计超阈值部分年底一次性结算)"
|
||||
)
|
||||
snapshot_json: Mapped[dict | None] = mapped_column(
|
||||
JSON, comment="个人明细快照(台账行引用列表)"
|
||||
)
|
||||
|
||||
|
||||
class AdjustmentOrder(Base, TimestampMixin):
|
||||
"""调整单:已锁定月份的唯一调整通道(补发/扣减,走审批;不回写月结)。"""
|
||||
|
||||
__tablename__ = "adjustment_orders"
|
||||
__table_args__ = mysql_table_args(
|
||||
"调整单表(已锁定月份的补发/扣减;走审批;生效后不回写月结快照)",
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||||
)
|
||||
order_no: Mapped[str] = mapped_column(
|
||||
String(32),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
comment="调整单号,自动生成 TZ{YYYYMMDD}-{4位当日序号}",
|
||||
)
|
||||
settlement_month: Mapped[str] = mapped_column(
|
||||
String(7), nullable=False, comment="调整月份(YYYY-MM;跨月规则待澄清 #9 前不限制)"
|
||||
)
|
||||
personnel_id: Mapped[int] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("personnel.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
comment="调整人员ID(personnel.id)",
|
||||
)
|
||||
personnel_name: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="调整人员姓名(冗余固化)"
|
||||
)
|
||||
direction: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE, nullable=False, comment="方向:1补发 2扣减"
|
||||
)
|
||||
amount: Mapped[Decimal] = mapped_column(
|
||||
Numeric(14, 2), nullable=False, comment="调整金额(恒正,方向表达语义)"
|
||||
)
|
||||
reason: Mapped[str] = mapped_column(
|
||||
String(255), nullable=False, comment="扣补说明(必填,审批与追溯锚点)"
|
||||
)
|
||||
status: Mapped[int] = mapped_column(
|
||||
TINYINT_TYPE,
|
||||
nullable=False,
|
||||
server_default="1",
|
||||
comment="状态:1待审批 2已生效 3已驳回 4已撤回",
|
||||
)
|
||||
submitter_id: Mapped[int | None] = mapped_column(
|
||||
BIGINT_UNSIGNED,
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
comment="提交人ID(账号删除后置空)",
|
||||
)
|
||||
submitter_name: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="提交人用户名(冗余固化)"
|
||||
)
|
||||
@ -95,3 +95,63 @@ def start_payment_ok(received: Decimal, revenue: Decimal, ratio: Fraction) -> bo
|
||||
if revenue == 0:
|
||||
return False
|
||||
return Fraction(received) / Fraction(revenue) >= ratio / 100
|
||||
|
||||
|
||||
def clamp_weight_r(received: Decimal, revenue: Decimal) -> int:
|
||||
"""支付权重 R:回款占销售额百分比,封顶封底 [0, 100](对照表 #22)。
|
||||
|
||||
received 非负时 int() 即向下取整;min/max 同时是红字冲销(负数回款)
|
||||
接入时的方向防线。从计算预览的内联实现提取 —— ④ 核算与预览共用。
|
||||
"""
|
||||
if revenue <= 0:
|
||||
return 0
|
||||
return max(0, min(100, int(Fraction(received) / Fraction(revenue) * 100)))
|
||||
|
||||
|
||||
def monthly_salary(performance: Decimal, basic: Decimal | None, mode: str) -> Decimal | None:
|
||||
"""月工资分岗口径(对照表 #8,salary.basic.mode 按角色维度取值)。
|
||||
|
||||
- INCLUDE:绩效 + 基本工资(业务员默认——绩效之外照发基本工资)
|
||||
- DEDUCT:绩效 − 基本工资(经理默认——基本工资从绩效分成中扣除)
|
||||
- EXCLUDE:仅绩效,基本工资不参与
|
||||
basic 缺失时按 0 参与(INCLUDE/EXCLUDE 等价于纯绩效)。
|
||||
"""
|
||||
basic_value = basic if basic is not None else Decimal("0")
|
||||
if mode == "INCLUDE":
|
||||
return performance + basic_value
|
||||
if mode == "DEDUCT":
|
||||
return performance - basic_value
|
||||
if mode == "EXCLUDE":
|
||||
return performance
|
||||
raise ValueError(f"未知的月工资口径:{mode}")
|
||||
|
||||
|
||||
def probation_annual_note(year_total: Decimal, threshold: Decimal) -> str | None:
|
||||
"""试用期年累计说明:超阈值部分年底一次性结算(对照表 #17)。
|
||||
|
||||
未超阈值返回 None(无需说明);阈值按元展示(Decimal 直出)。
|
||||
"""
|
||||
if year_total <= threshold:
|
||||
return None
|
||||
excess = (year_total - threshold).quantize(Decimal("0.01"), rounding="ROUND_HALF_UP")
|
||||
return f"试用期绩效照算不发放;年累计超 {threshold} 元部分年底一次性结算(当前超 {excess} 元)"
|
||||
|
||||
|
||||
def manager_team_share(
|
||||
team_performance: Decimal,
|
||||
company_performance: Decimal,
|
||||
*,
|
||||
team_pct: Fraction,
|
||||
company_pct: Fraction,
|
||||
) -> Decimal:
|
||||
"""经理绩效 = 本团队业务员绩效 × 12.5% + 全公司业务员绩效 × 5%(裁决 #6/#15/#16)。
|
||||
|
||||
**#12 策略点(待业务答复,2026-10-16 暂定口径)**:基数**不含**经理本人
|
||||
名下经理个人户业务的业绩——该部分按经理个人户规则另行处理(单笔落台账
|
||||
rate=0/绩效 0,待答复后若需并入,只改调用方传入的基数集合)。
|
||||
团队口径:manager_id=该经理且岗位非「营销经理」的业务员绩效行之和;
|
||||
全公司口径:全部业务员(非经理岗)绩效行之和。
|
||||
"""
|
||||
return _to_decimal(Fraction(team_performance) * team_pct / 100) + _to_decimal(
|
||||
Fraction(company_performance) * company_pct / 100
|
||||
)
|
||||
|
||||
@ -290,6 +290,23 @@ PARAM_REGISTRY: dict[str, ParamSpec] = {
|
||||
),
|
||||
sort_order=18,
|
||||
),
|
||||
ParamSpec(
|
||||
code="approval.adjustment.enabled",
|
||||
name="调整单审批开关",
|
||||
group="审批开关",
|
||||
value_type="BOOL",
|
||||
unit=None,
|
||||
dimension_type="NONE",
|
||||
options=None,
|
||||
default_value="0",
|
||||
value_min=None,
|
||||
value_max=None,
|
||||
description=(
|
||||
"0=调整单提交即生效(系统自动通过留痕);1=走财务复核→领导审核审批流。"
|
||||
"调整单是已锁定月份补发/扣减的唯一通道"
|
||||
),
|
||||
sort_order=19,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -614,11 +614,8 @@ def preview(session: Session, payload: ParamPreviewIn) -> ParamPreviewOut:
|
||||
|
||||
start_ratio = take("payment.start.ratio")
|
||||
start_ok = formula.start_payment_ok(received, revenue, start_ratio)
|
||||
# R 按「回款占销售额百分比向下取整」口径封顶封底:received 非负时 int()
|
||||
# 即向下取整;min/max 同时是红字冲销(负数回款)接入时的方向防线
|
||||
weight_r = (
|
||||
max(0, min(100, int(Fraction(received) / Fraction(revenue) * 100))) if revenue > 0 else 0
|
||||
)
|
||||
# R 钳制已提取为纯函数 clamp_weight_r(④ 核算与预览共用同一实现)
|
||||
weight_r = formula.clamp_weight_r(received, revenue)
|
||||
weight = formula.payment_weight(weight_r)
|
||||
base_amount = after_barter if barter_applied else performance
|
||||
payable = to_decimal(Fraction(base_amount) * weight)
|
||||
|
||||
@ -30,6 +30,7 @@ from app.core.constants import ROLE_ADMIN, ROLE_USER
|
||||
from app.core.db import get_session_factory
|
||||
from app.core.security import create_access_token, hash_password
|
||||
from app.models import (
|
||||
AdjustmentOrder,
|
||||
ApprovalOrder,
|
||||
Business,
|
||||
BusinessItem,
|
||||
@ -37,16 +38,19 @@ from app.models import (
|
||||
BusinessStatusLog,
|
||||
Customer,
|
||||
CustomerRatingGrade,
|
||||
MonthlySettlement,
|
||||
OperationLog,
|
||||
OwnershipChangeLog,
|
||||
ParamChangeBatch,
|
||||
ParamValue,
|
||||
PerformanceLedger,
|
||||
Personnel,
|
||||
PersonnelPlan,
|
||||
PersonnelRegion,
|
||||
Product,
|
||||
Receipt,
|
||||
Role,
|
||||
SettlementItem,
|
||||
Shipment,
|
||||
ShipmentItem,
|
||||
SystemSetting,
|
||||
@ -178,12 +182,19 @@ def _purge_test_data() -> None:
|
||||
# 审批单无 t_ 前缀可用,按 AP 编号前缀全量清(steps 外键 CASCADE 随删;
|
||||
# ref_id 固化无外键,业务已在上一步删除,无悬挂影响)
|
||||
session.execute(delete(ApprovalOrder).where(ApprovalOrder.order_no.like("AP%")))
|
||||
# 开关参数恢复种子基线:测试提交的变更批次条目 id 均大于种子终值 21
|
||||
# (V3 十九条 + V6 两条),删新条目与新批次后解析回退到种子值(开关=0)
|
||||
stray_values = list(session.execute(select(ParamValue).where(ParamValue.id > 21)).scalars())
|
||||
# ---- M3-core:核算台账 / 月结 / 调整单(仅测试写入,全量清;
|
||||
# 删除顺序:台账→明细→月结头→调整单,TZ 前缀全量清)----
|
||||
session.execute(delete(PerformanceLedger))
|
||||
session.execute(delete(SettlementItem))
|
||||
session.execute(delete(MonthlySettlement))
|
||||
session.execute(delete(AdjustmentOrder).where(AdjustmentOrder.order_no.like("TZ%")))
|
||||
# 开关参数恢复种子基线:测试提交的变更批次条目 id 均大于种子终值 22
|
||||
# (V3 十九条 + V6 两条 + V7 调整单开关一条),删新条目与新批次后解析
|
||||
# 回退到种子值(开关=0)
|
||||
stray_values = list(session.execute(select(ParamValue).where(ParamValue.id > 22)).scalars())
|
||||
if stray_values:
|
||||
stray_batches = {v.batch_id for v in stray_values}
|
||||
session.execute(delete(ParamValue).where(ParamValue.id > 21))
|
||||
session.execute(delete(ParamValue).where(ParamValue.id > 22))
|
||||
session.execute(delete(ParamChangeBatch).where(ParamChangeBatch.id.in_(stray_batches)))
|
||||
# 系统设置恢复种子单行(test_settings 会改公司名/传 logo——残留会让
|
||||
# 后续 e2e 的品牌断言在污染状态下运行);uploads 下的测试 logo 一并删
|
||||
|
||||
@ -11,10 +11,14 @@ from fractions import Fraction
|
||||
|
||||
from app.services.formula import (
|
||||
apply_barter,
|
||||
clamp_weight_r,
|
||||
converted_profit,
|
||||
manager_team_share,
|
||||
monthly_salary,
|
||||
payment_days,
|
||||
payment_weight,
|
||||
performance_amount,
|
||||
probation_annual_note,
|
||||
profit_rate,
|
||||
salesperson_rate,
|
||||
start_payment_ok,
|
||||
@ -137,3 +141,68 @@ class TestModifiers:
|
||||
|
||||
def test_起付判定_未达到60点(self) -> None:
|
||||
assert start_payment_ok(Decimal("59999"), Decimal("100000"), Fraction(60)) is False
|
||||
|
||||
|
||||
class TestClampWeightR:
|
||||
"""R 钳制:向下取整 + [0,100] 封顶封底(从预览内联提取的共用实现)。"""
|
||||
|
||||
def test_向下取整(self) -> None:
|
||||
assert clamp_weight_r(Decimal("5999.99"), Decimal("10000")) == 59
|
||||
|
||||
def test_封顶100_封底0(self) -> None:
|
||||
assert clamp_weight_r(Decimal("20000"), Decimal("10000")) == 100
|
||||
assert clamp_weight_r(Decimal("0"), Decimal("10000")) == 0
|
||||
|
||||
def test_销售额0_防御0(self) -> None:
|
||||
assert clamp_weight_r(Decimal("100"), Decimal("0")) == 0
|
||||
|
||||
|
||||
class TestMonthlySalary:
|
||||
"""月工资分岗口径三档(对照表 #8)。"""
|
||||
|
||||
def test_INCLUDE_绩效加基本(self) -> None:
|
||||
assert monthly_salary(Decimal("1000"), Decimal("3000"), "INCLUDE") == Decimal("4000")
|
||||
|
||||
def test_DEDUCT_绩效减基本_可为负(self) -> None:
|
||||
assert monthly_salary(Decimal("1000"), Decimal("3000"), "DEDUCT") == Decimal("-2000")
|
||||
|
||||
def test_EXCLUDE_仅绩效(self) -> None:
|
||||
assert monthly_salary(Decimal("1000"), Decimal("3000"), "EXCLUDE") == Decimal("1000")
|
||||
|
||||
def test_基本缺失按0(self) -> None:
|
||||
assert monthly_salary(Decimal("1000"), None, "INCLUDE") == Decimal("1000")
|
||||
|
||||
def test_未知口径报错(self) -> None:
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
monthly_salary(Decimal("1"), Decimal("1"), "UNKNOWN")
|
||||
|
||||
|
||||
class TestProbationAnnualNote:
|
||||
"""试用期年累计说明:超阈值才有 note,金额精确到分。"""
|
||||
|
||||
def test_未超阈值_无说明(self) -> None:
|
||||
assert probation_annual_note(Decimal("18000"), Decimal("18000")) is None
|
||||
assert probation_annual_note(Decimal("100"), Decimal("18000")) is None
|
||||
|
||||
def test_超额_给出精确差额(self) -> None:
|
||||
note = probation_annual_note(Decimal("20000.5"), Decimal("18000"))
|
||||
assert note is not None and "2000.50" in note
|
||||
|
||||
|
||||
class TestManagerTeamShare:
|
||||
"""经理 12.5%+5% 聚合(#12 暂口径:基数不含经理个人户业绩——由调用方
|
||||
传入的基数集合保证,本函数只做乘法与求和)。"""
|
||||
|
||||
def test_两段乘加_精确分数(self) -> None:
|
||||
# 入参为百分数(与种子一致:12.5 表示 12.5%):10000×12.5%=1250、50000×5%=2500
|
||||
got = manager_team_share(
|
||||
Decimal("10000"), Decimal("50000"), team_pct=Fraction(25, 2), company_pct=Fraction(5)
|
||||
)
|
||||
assert got == Decimal("3750")
|
||||
|
||||
def test_零基数_零结果(self) -> None:
|
||||
assert manager_team_share(
|
||||
Decimal("0"), Decimal("0"), team_pct=Fraction(0), company_pct=Fraction(0)
|
||||
) == Decimal("0")
|
||||
|
||||
@ -40,6 +40,10 @@ EXPECTED_TABLES = {
|
||||
"shipment_items",
|
||||
"receipts",
|
||||
"customer_rating_grades",
|
||||
"performance_ledgers",
|
||||
"monthly_settlements",
|
||||
"settlement_items",
|
||||
"adjustment_orders",
|
||||
}
|
||||
|
||||
# 单复数同形的集合名词:本身即复数语义,不受「以 s 结尾」检查约束
|
||||
|
||||
@ -18,7 +18,7 @@ from app.services.param_registry import (
|
||||
validate_and_normalize,
|
||||
)
|
||||
|
||||
# 与 db/migration 迁移种子保持一致的 18 项编码(V3 十六项 + V6 审批开关两项)
|
||||
# 与 db/migration 迁移种子保持一致的 19 项编码(V3 十六项 + V6 审批开关两项 + V7 调整单开关)
|
||||
EXPECTED_CODES = {
|
||||
"salesperson.rate.start",
|
||||
"salesperson.rate.slope",
|
||||
@ -38,14 +38,15 @@ EXPECTED_CODES = {
|
||||
"probation.salary.basic",
|
||||
"approval.secondary_allocation.enabled",
|
||||
"approval.monthly_settlement.enabled",
|
||||
"approval.adjustment.enabled",
|
||||
}
|
||||
|
||||
|
||||
class TestRegistry:
|
||||
"""注册表元数据完整性(与迁移种子同源)。"""
|
||||
|
||||
def test_注册表共18项(self) -> None:
|
||||
assert len(PARAM_REGISTRY) == 18
|
||||
def test_注册表共19项(self) -> None:
|
||||
assert len(PARAM_REGISTRY) == 19
|
||||
|
||||
def test_编码集合与迁移种子一致(self) -> None:
|
||||
assert set(PARAM_REGISTRY) == EXPECTED_CODES
|
||||
@ -70,10 +71,11 @@ class TestRegistry:
|
||||
assert spec.default_value in spec.options
|
||||
|
||||
def test_BOOL开关_仅接受0与1(self) -> None:
|
||||
"""V6 首例 BOOL 注册的两个审批开关:合法值与非法值边界。"""
|
||||
"""审批开关 BOOL 组(V6 两个 + V7 调整单):合法值与非法值边界。"""
|
||||
for code in (
|
||||
"approval.secondary_allocation.enabled",
|
||||
"approval.monthly_settlement.enabled",
|
||||
"approval.adjustment.enabled",
|
||||
):
|
||||
spec = PARAM_REGISTRY[code]
|
||||
assert spec.value_type == "BOOL"
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 删除顺序遵循外键依赖:先删子表
|
||||
DROP TABLE IF EXISTS `performance_ledgers`;
|
||||
DROP TABLE IF EXISTS `settlement_items`;
|
||||
DROP TABLE IF EXISTS `adjustment_orders`;
|
||||
DROP TABLE IF EXISTS `monthly_settlements`;
|
||||
DROP TABLE IF EXISTS `approval_steps`;
|
||||
DROP TABLE IF EXISTS `approval_orders`;
|
||||
DROP TABLE IF EXISTS `business_secondary_allocations`;
|
||||
@ -450,7 +454,8 @@ INSERT INTO param_definitions (id, param_code, param_name, param_group, value_ty
|
||||
(15, 'probation.annual.threshold', '试用期年累计结算阈值', '月工资口径', 'DECIMAL', '元', 'NONE', NULL, '18000', '0', NULL, '试用期年累计绩效超过此值部分年底一次性结算', 15),
|
||||
(16, 'probation.salary.basic', '试用期基本工资标准', '月工资口径', 'DECIMAL', '元', 'NONE', NULL, '3000', '0', NULL, '试用期仅发基本工资、不发放绩效(归属待 M2 复核)', 16),
|
||||
(17, 'approval.secondary_allocation.enabled', '二次分配审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '0=二次分配方案提交即生效(系统自动通过全节点留痕,留痕不断链);1=提交后进入财务复核→领导审核审批流(2026-10-15 复核审批补充需求)', 17),
|
||||
(18, 'approval.monthly_settlement.enabled', '月度结算审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '预留参数:月度结算审批链开关(M3 ④ 月结子块接入时消费);0=结算生成即生效,1=走财务复核→领导审核审批流', 18);
|
||||
(18, 'approval.monthly_settlement.enabled', '月度结算审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '预留参数:月度结算审批链开关(M3 ④ 月结子块接入时消费);0=结算生成即生效,1=走财务复核→领导审核审批流', 18),
|
||||
(19, 'approval.adjustment.enabled', '调整单审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '0=调整单提交即生效(系统自动通过留痕);1=走财务复核→领导审核审批流。调整单是已锁定月份补发/扣减的唯一通道', 19);
|
||||
|
||||
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES
|
||||
(1, 1, 'salesperson', '2026-02-17', '10', 1),
|
||||
@ -473,7 +478,8 @@ INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_t
|
||||
(18, 15, '', '2026-01-01', '18000', 1),
|
||||
(19, 16, '', '2026-01-01', '3000', 1),
|
||||
(20, 17, '', '2026-01-01', '0', 1),
|
||||
(21, 18, '', '2026-01-01', '0', 1);
|
||||
(21, 18, '', '2026-01-01', '0', 1),
|
||||
(22, 19, '', '2026-01-01', '0', 1);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 业务表(V5 迁移;业务编号自动生成,五项汇总冗余列由服务层事务内维护)
|
||||
@ -743,6 +749,118 @@ CREATE INDEX idx_business_secondary_allocations_business_id ON business_secondar
|
||||
|
||||
CREATE INDEX idx_business_secondary_allocations_personnel_id ON business_secondary_allocations (personnel_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 绩效核算台账/月结/调整单(V7 迁移;append-only 版本行;通过即锁定走调整单)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE monthly_settlements (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
settlement_month VARCHAR(7) NOT NULL COMMENT '结算月份(YYYY-MM;全表唯一)',
|
||||
status TINYINT NOT NULL COMMENT '状态:1草稿 2待审批 3已审定(终态=已锁定)4已驳回(回草稿)' DEFAULT '1',
|
||||
total_performance NUMERIC(14, 2) COMMENT '绩效金额合计(可负)',
|
||||
total_payable NUMERIC(14, 2) COMMENT '应付绩效合计(可负)',
|
||||
person_count TINYINT NOT NULL COMMENT '结算人数' DEFAULT '0',
|
||||
snapshot_json JSON COMMENT '月度总览快照(生成时刻的确认回款口径说明与汇总参数)',
|
||||
approval_order_no VARCHAR(32) COMMENT '关联审批单号(冗余固化;开关关闭自动通过时也有单)',
|
||||
submitter_id BIGINT UNSIGNED COMMENT '提交人ID(账号删除后置空)',
|
||||
submitter_name VARCHAR(64) NOT NULL COMMENT '提交人用户名(冗余固化)',
|
||||
locked_at DATETIME COMMENT '锁定时间(status=3 时回填)',
|
||||
locked_by VARCHAR(64) COMMENT '锁定操作人(冗余固化)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_monthly_settlements PRIMARY KEY (id),
|
||||
CONSTRAINT uk_monthly_settlements_settlement_month UNIQUE (settlement_month),
|
||||
CONSTRAINT fk_monthly_settlements_submitter_id FOREIGN KEY(submitter_id) REFERENCES users (id) ON DELETE SET NULL
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='月度结算表(按月一行;通过即锁定,锁定后走调整单)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE settlement_items (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
settlement_id BIGINT UNSIGNED NOT NULL COMMENT '月结ID(monthly_settlements.id;头删明细随删)',
|
||||
personnel_id BIGINT UNSIGNED NOT NULL COMMENT '结算人员ID(personnel.id)',
|
||||
personnel_name VARCHAR(64) NOT NULL COMMENT '结算人员姓名(冗余固化)',
|
||||
role_code VARCHAR(32) NOT NULL COMMENT '岗位编码(主岗,聚合锚点)',
|
||||
calc_type TINYINT NOT NULL COMMENT '核算类型:1单笔回款 2经理团队聚合',
|
||||
performance_sum NUMERIC(14, 2) NOT NULL COMMENT '绩效金额合计(可负;聚合行排除经理个人户业绩,#12 暂口径)',
|
||||
payable_sum NUMERIC(14, 2) NOT NULL COMMENT '应付绩效合计(可负)',
|
||||
basic_amount NUMERIC(14, 2) COMMENT '月工资标准(personnel.salary_standard 当月快照)',
|
||||
salary_mode VARCHAR(16) COMMENT '月工资口径快照:INCLUDE/DEDUCT/EXCLUDE',
|
||||
net_amount NUMERIC(14, 2) COMMENT '净工资 = 绩效±基本工资(按口径;试用期 0)',
|
||||
is_probation TINYINT NOT NULL COMMENT '试用期:1绩效照算不发放(net=0,超年累计记 note)0否' DEFAULT '0',
|
||||
probation_note VARCHAR(255) COMMENT '试用期说明(年累计超阈值部分年底一次性结算)',
|
||||
snapshot_json JSON COMMENT '个人明细快照(台账行引用列表)',
|
||||
CONSTRAINT pk_settlement_items PRIMARY KEY (id),
|
||||
CONSTRAINT fk_settlement_items_settlement_id FOREIGN KEY(settlement_id) REFERENCES monthly_settlements (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_settlement_items_personnel_id FOREIGN KEY(personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='月结明细表(按人固化;支持负数工资;锁定后只读)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE adjustment_orders (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '调整单号,自动生成 TZ{YYYYMMDD}-{4位当日序号}',
|
||||
settlement_month VARCHAR(7) NOT NULL COMMENT '调整月份(YYYY-MM;跨月规则待澄清 #9 前不限制)',
|
||||
personnel_id BIGINT UNSIGNED NOT NULL COMMENT '调整人员ID(personnel.id)',
|
||||
personnel_name VARCHAR(64) NOT NULL COMMENT '调整人员姓名(冗余固化)',
|
||||
direction TINYINT NOT NULL COMMENT '方向:1补发 2扣减',
|
||||
amount NUMERIC(14, 2) NOT NULL COMMENT '调整金额(恒正,方向表达语义)',
|
||||
reason VARCHAR(255) NOT NULL COMMENT '扣补说明(必填,审批与追溯锚点)',
|
||||
status TINYINT NOT NULL COMMENT '状态:1待审批 2已生效 3已驳回 4已撤回' DEFAULT '1',
|
||||
submitter_id BIGINT UNSIGNED COMMENT '提交人ID(账号删除后置空)',
|
||||
submitter_name VARCHAR(64) NOT NULL COMMENT '提交人用户名(冗余固化)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_adjustment_orders PRIMARY KEY (id),
|
||||
CONSTRAINT uk_adjustment_orders_order_no UNIQUE (order_no),
|
||||
CONSTRAINT fk_adjustment_orders_personnel_id FOREIGN KEY(personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_adjustment_orders_submitter_id FOREIGN KEY(submitter_id) REFERENCES users (id) ON DELETE SET NULL
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='调整单表(已锁定月份的补发/扣减;走审批;生效后不回写月结快照)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE performance_ledgers (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
receipt_id BIGINT UNSIGNED COMMENT '回款ID(receipts.id;聚合行可为 NULL)',
|
||||
calc_version TINYINT NOT NULL COMMENT '核算版本(重算+1,旧行 is_active=0)' DEFAULT '1',
|
||||
is_active TINYINT NOT NULL COMMENT '生效标记:1当前生效 0已被重算替代(重算受控 UPDATE 例外)' DEFAULT '1',
|
||||
calc_type TINYINT NOT NULL COMMENT '核算类型:1单笔回款 2经理团队聚合(月度)' DEFAULT '1',
|
||||
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(固化无外键;聚合行为该经理名下首行业务ID,报表下钻锚点)',
|
||||
business_no VARCHAR(32) NOT NULL COMMENT '业务编号(冗余固化,列表免联查)',
|
||||
personnel_id BIGINT UNSIGNED NOT NULL COMMENT '归属营销人员ID(personnel.id;核算归属锚点)',
|
||||
personnel_name VARCHAR(64) NOT NULL COMMENT '归属营销人员姓名(冗余固化)',
|
||||
role_code VARCHAR(32) NOT NULL COMMENT '岗位编码(business_roles.role_code,计提公式锚点)',
|
||||
customer_type VARCHAR(32) NOT NULL COMMENT '客户分类:personal/manager_personal/enterprise',
|
||||
product_category VARCHAR(32) NOT NULL COMMENT '产品类别:municipal_water/rail_transit/mixed(混合业务,行级明细入 snapshot_json)',
|
||||
ship_date DATE COMMENT '发货日期(回款天数基准)',
|
||||
receipt_date DATE NOT NULL COMMENT '回款日期(参数版本匹配日,裁决 #11)',
|
||||
payment_days INTEGER COMMENT '回款天数(发货日首日计第 1 天;未指定批次为 NULL;先款后货照实负值)',
|
||||
rate_text VARCHAR(32) NOT NULL COMMENT '计提比例原文(如 4/360,防 Fraction 截断;聚合行存 12.5%/5% 结构说明)',
|
||||
converted_profit NUMERIC(14, 2) COMMENT '折算毛利润额(可负;混合业务为各行汇总)',
|
||||
performance_amount NUMERIC(14, 2) COMMENT '绩效金额(可负,裁决:负绩效照算不归零)',
|
||||
barter_applied TINYINT NOT NULL COMMENT '以物抵顶折算:1已按 80% 折算 0否' DEFAULT '0',
|
||||
weight_r INTEGER COMMENT '支付权重 R(回款占销售额百分比向下取整)',
|
||||
payment_weight NUMERIC(7, 6) COMMENT '支付权重系数 R×R(展示用)',
|
||||
payable_amount NUMERIC(14, 2) COMMENT '当期应付绩效(绩效 × 支付权重;聚合行为当月汇总)',
|
||||
settlement_month VARCHAR(7) NOT NULL COMMENT '归属月(回款日期所在月,YYYY-MM;月结与报表维度)',
|
||||
is_probation TINYINT NOT NULL COMMENT '试用期:1照算不发放 0否' DEFAULT '0',
|
||||
missing_cost TINYINT NOT NULL COMMENT '成本缺失:1有明细行缺成本(该行毛利按 0 计)0否' DEFAULT '0',
|
||||
snapshot_json JSON COMMENT '计算快照(所用参数版本与行级折算明细,重算可回溯)',
|
||||
settlement_id BIGINT UNSIGNED COMMENT '所属月结ID(月结生成时回填;NULL=未入结)',
|
||||
calc_reason VARCHAR(255) COMMENT '核算/重算原因(重算必填留痕)',
|
||||
remark VARCHAR(255) COMMENT '备注',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_performance_ledgers PRIMARY KEY (id),
|
||||
CONSTRAINT uk_performance_ledgers_receipt_id_calc_version UNIQUE (receipt_id, calc_version),
|
||||
CONSTRAINT fk_performance_ledgers_settlement_id FOREIGN KEY(settlement_id) REFERENCES monthly_settlements (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_performance_ledgers_personnel_id FOREIGN KEY(personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_performance_ledgers_receipt_id FOREIGN KEY(receipt_id) REFERENCES receipts (id) ON DELETE RESTRICT
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='绩效核算台账表(append-only 版本行;重算插新版本旧行仅翻 is_active)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE INDEX idx_performance_ledgers_business_id ON performance_ledgers (business_id);
|
||||
|
||||
CREATE INDEX idx_performance_ledgers_month_active ON performance_ledgers (settlement_month, is_active);
|
||||
|
||||
CREATE INDEX idx_performance_ledgers_month_personnel_type ON performance_ledgers (settlement_month, personnel_id, calc_type);
|
||||
|
||||
CREATE INDEX idx_performance_ledgers_personnel_id ON performance_ledgers (personnel_id);
|
||||
|
||||
ALTER TABLE approval_orders MODIFY order_type TINYINT NOT NULL COMMENT '审批对象类型:1二次分配方案 2月度结算 3调整单';
|
||||
|
||||
-- 公司信息单行(system_settings 恒为一行,id=1 由迁移保底)
|
||||
INSERT INTO system_settings (id, company_name, logo_path) VALUES (1, '衍水集团', NULL);
|
||||
|
||||
@ -754,4 +872,4 @@ CREATE TABLE alembic_version (
|
||||
CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COLLATE utf8mb4_general_ci;
|
||||
|
||||
INSERT INTO alembic_version (version_num) VALUES ('79bcc5aa98d4');
|
||||
INSERT INTO alembic_version (version_num) VALUES ('3bca400421c6');
|
||||
|
||||
117
db/migration/V7__add_ledger_settlements_adjustments.sql
Normal file
117
db/migration/V7__add_ledger_settlements_adjustments.sql
Normal file
@ -0,0 +1,117 @@
|
||||
-- Running upgrade 79bcc5aa98d4 -> 3bca400421c6
|
||||
|
||||
CREATE TABLE monthly_settlements (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
settlement_month VARCHAR(7) NOT NULL COMMENT '结算月份(YYYY-MM;全表唯一)',
|
||||
status TINYINT NOT NULL COMMENT '状态:1草稿 2待审批 3已审定(终态=已锁定)4已驳回(回草稿)' DEFAULT '1',
|
||||
total_performance NUMERIC(14, 2) COMMENT '绩效金额合计(可负)',
|
||||
total_payable NUMERIC(14, 2) COMMENT '应付绩效合计(可负)',
|
||||
person_count TINYINT NOT NULL COMMENT '结算人数' DEFAULT '0',
|
||||
snapshot_json JSON COMMENT '月度总览快照(生成时刻的确认回款口径说明与汇总参数)',
|
||||
approval_order_no VARCHAR(32) COMMENT '关联审批单号(冗余固化;开关关闭自动通过时也有单)',
|
||||
submitter_id BIGINT UNSIGNED COMMENT '提交人ID(账号删除后置空)',
|
||||
submitter_name VARCHAR(64) NOT NULL COMMENT '提交人用户名(冗余固化)',
|
||||
locked_at DATETIME COMMENT '锁定时间(status=3 时回填)',
|
||||
locked_by VARCHAR(64) COMMENT '锁定操作人(冗余固化)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_monthly_settlements PRIMARY KEY (id),
|
||||
CONSTRAINT fk_monthly_settlements_submitter_id FOREIGN KEY(submitter_id) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT uk_monthly_settlements_settlement_month UNIQUE (settlement_month)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='月度结算表(按月一行;通过即锁定,锁定后走调整单)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE adjustment_orders (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '调整单号,自动生成 TZ{YYYYMMDD}-{4位当日序号}',
|
||||
settlement_month VARCHAR(7) NOT NULL COMMENT '调整月份(YYYY-MM;跨月规则待澄清 #9 前不限制)',
|
||||
personnel_id BIGINT UNSIGNED NOT NULL COMMENT '调整人员ID(personnel.id)',
|
||||
personnel_name VARCHAR(64) NOT NULL COMMENT '调整人员姓名(冗余固化)',
|
||||
direction TINYINT NOT NULL COMMENT '方向:1补发 2扣减',
|
||||
amount NUMERIC(14, 2) NOT NULL COMMENT '调整金额(恒正,方向表达语义)',
|
||||
reason VARCHAR(255) NOT NULL COMMENT '扣补说明(必填,审批与追溯锚点)',
|
||||
status TINYINT NOT NULL COMMENT '状态:1待审批 2已生效 3已驳回 4已撤回' DEFAULT '1',
|
||||
submitter_id BIGINT UNSIGNED COMMENT '提交人ID(账号删除后置空)',
|
||||
submitter_name VARCHAR(64) NOT NULL COMMENT '提交人用户名(冗余固化)',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_adjustment_orders PRIMARY KEY (id),
|
||||
CONSTRAINT fk_adjustment_orders_personnel_id FOREIGN KEY(personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_adjustment_orders_submitter_id FOREIGN KEY(submitter_id) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT uk_adjustment_orders_order_no UNIQUE (order_no)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='调整单表(已锁定月份的补发/扣减;走审批;生效后不回写月结快照)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE settlement_items (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
settlement_id BIGINT UNSIGNED NOT NULL COMMENT '月结ID(monthly_settlements.id;头删明细随删)',
|
||||
personnel_id BIGINT UNSIGNED NOT NULL COMMENT '结算人员ID(personnel.id)',
|
||||
personnel_name VARCHAR(64) NOT NULL COMMENT '结算人员姓名(冗余固化)',
|
||||
role_code VARCHAR(32) NOT NULL COMMENT '岗位编码(主岗,聚合锚点)',
|
||||
calc_type TINYINT NOT NULL COMMENT '核算类型:1单笔回款 2经理团队聚合',
|
||||
performance_sum NUMERIC(14, 2) NOT NULL COMMENT '绩效金额合计(可负;聚合行排除经理个人户业绩,#12 暂口径)',
|
||||
payable_sum NUMERIC(14, 2) NOT NULL COMMENT '应付绩效合计(可负)',
|
||||
basic_amount NUMERIC(14, 2) COMMENT '月工资标准(personnel.salary_standard 当月快照)',
|
||||
salary_mode VARCHAR(16) COMMENT '月工资口径快照:INCLUDE/DEDUCT/EXCLUDE',
|
||||
net_amount NUMERIC(14, 2) COMMENT '净工资 = 绩效±基本工资(按口径;试用期 0)',
|
||||
is_probation TINYINT NOT NULL COMMENT '试用期:1绩效照算不发放(net=0,超年累计记 note)0否' DEFAULT '0',
|
||||
probation_note VARCHAR(255) COMMENT '试用期说明(年累计超阈值部分年底一次性结算)',
|
||||
snapshot_json JSON COMMENT '个人明细快照(台账行引用列表)',
|
||||
CONSTRAINT pk_settlement_items PRIMARY KEY (id),
|
||||
CONSTRAINT fk_settlement_items_personnel_id FOREIGN KEY(personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_settlement_items_settlement_id FOREIGN KEY(settlement_id) REFERENCES monthly_settlements (id) ON DELETE CASCADE
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='月结明细表(按人固化;支持负数工资;锁定后只读)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE performance_ledgers (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
receipt_id BIGINT UNSIGNED COMMENT '回款ID(receipts.id;聚合行可为 NULL)',
|
||||
calc_version TINYINT NOT NULL COMMENT '核算版本(重算+1,旧行 is_active=0)' DEFAULT '1',
|
||||
is_active TINYINT NOT NULL COMMENT '生效标记:1当前生效 0已被重算替代(重算受控 UPDATE 例外)' DEFAULT '1',
|
||||
calc_type TINYINT NOT NULL COMMENT '核算类型:1单笔回款 2经理团队聚合(月度)' DEFAULT '1',
|
||||
business_id BIGINT UNSIGNED NOT NULL COMMENT '业务ID(固化无外键;聚合行为该经理名下首行业务ID,报表下钻锚点)',
|
||||
business_no VARCHAR(32) NOT NULL COMMENT '业务编号(冗余固化,列表免联查)',
|
||||
personnel_id BIGINT UNSIGNED NOT NULL COMMENT '归属营销人员ID(personnel.id;核算归属锚点)',
|
||||
personnel_name VARCHAR(64) NOT NULL COMMENT '归属营销人员姓名(冗余固化)',
|
||||
role_code VARCHAR(32) NOT NULL COMMENT '岗位编码(business_roles.role_code,计提公式锚点)',
|
||||
customer_type VARCHAR(32) NOT NULL COMMENT '客户分类:personal/manager_personal/enterprise',
|
||||
product_category VARCHAR(32) NOT NULL COMMENT '产品类别:municipal_water/rail_transit/mixed(混合业务,行级明细入 snapshot_json)',
|
||||
ship_date DATE COMMENT '发货日期(回款天数基准)',
|
||||
receipt_date DATE NOT NULL COMMENT '回款日期(参数版本匹配日,裁决 #11)',
|
||||
payment_days INTEGER COMMENT '回款天数(发货日首日计第 1 天;未指定批次为 NULL;先款后货照实负值)',
|
||||
rate_text VARCHAR(32) NOT NULL COMMENT '计提比例原文(如 4/360,防 Fraction 截断;聚合行存 12.5%/5% 结构说明)',
|
||||
converted_profit NUMERIC(14, 2) COMMENT '折算毛利润额(可负;混合业务为各行汇总)',
|
||||
performance_amount NUMERIC(14, 2) COMMENT '绩效金额(可负,裁决:负绩效照算不归零)',
|
||||
barter_applied TINYINT NOT NULL COMMENT '以物抵顶折算:1已按 80% 折算 0否' DEFAULT '0',
|
||||
weight_r INTEGER COMMENT '支付权重 R(回款占销售额百分比向下取整)',
|
||||
payment_weight NUMERIC(7, 6) COMMENT '支付权重系数 R×R(展示用)',
|
||||
payable_amount NUMERIC(14, 2) COMMENT '当期应付绩效(绩效 × 支付权重;聚合行为当月汇总)',
|
||||
settlement_month VARCHAR(7) NOT NULL COMMENT '归属月(回款日期所在月,YYYY-MM;月结与报表维度)',
|
||||
is_probation TINYINT NOT NULL COMMENT '试用期:1照算不发放 0否' DEFAULT '0',
|
||||
missing_cost TINYINT NOT NULL COMMENT '成本缺失:1有明细行缺成本(该行毛利按 0 计)0否' DEFAULT '0',
|
||||
snapshot_json JSON COMMENT '计算快照(所用参数版本与行级折算明细,重算可回溯)',
|
||||
settlement_id BIGINT UNSIGNED COMMENT '所属月结ID(月结生成时回填;NULL=未入结)',
|
||||
calc_reason VARCHAR(255) COMMENT '核算/重算原因(重算必填留痕)',
|
||||
remark VARCHAR(255) COMMENT '备注',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_performance_ledgers PRIMARY KEY (id),
|
||||
CONSTRAINT fk_performance_ledger_personnel_id FOREIGN KEY(personnel_id) REFERENCES personnel (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_performance_ledger_receipt_id FOREIGN KEY(receipt_id) REFERENCES receipts (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_performance_ledger_settlement_id FOREIGN KEY(settlement_id) REFERENCES monthly_settlements (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT uk_performance_ledger_receipt_id_calc_version UNIQUE (receipt_id, calc_version)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='绩效核算台账表(append-only 版本行;重算插新版本旧行仅翻 is_active)' COLLATE utf8mb4_general_ci;
|
||||
|
||||
CREATE INDEX idx_performance_ledger_business_id ON performance_ledgers (business_id);
|
||||
|
||||
CREATE INDEX idx_performance_ledger_month_active ON performance_ledgers (settlement_month, is_active);
|
||||
|
||||
CREATE INDEX idx_performance_ledger_month_personnel_type ON performance_ledgers (settlement_month, personnel_id, calc_type);
|
||||
|
||||
CREATE INDEX idx_performance_ledger_personnel_id ON performance_ledgers (personnel_id);
|
||||
|
||||
ALTER TABLE approval_orders MODIFY order_type TINYINT NOT NULL COMMENT '审批对象类型:1二次分配方案 2月度结算 3调整单';
|
||||
|
||||
INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order, status) VALUES (19, 'approval.adjustment.enabled', '调整单审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '0=调整单提交即生效(系统自动通过留痕);1=走财务复核→领导审核审批流。调整单是已锁定月份补发/扣减的唯一通道', 19, 1);
|
||||
|
||||
INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (22, 19, '', '2026-01-01', '0', 1);
|
||||
|
||||
UPDATE alembic_version SET version_num='3bca400421c6' WHERE alembic_version.version_num = '79bcc5aa98d4';
|
||||
|
||||
Binary file not shown.
@ -11,48 +11,53 @@
|
||||
| 1 | **推送 feat/m3-increments(27 个提交)** | ⛔ 被远端阻塞 | 43.140.215.244:7040 的 Git 服务应用层无响应(ping 通、TCP 通、HTTP 不回包),需**服务器管理员重启 Git 服务**;恢复后执行 `git push -u origin feat/m3-increments`。本地提交完整无丢失 |
|
||||
| 2 | **M2 分支合并 main** | 未开始 | feat/base-business-data(M2 全部 + code-review 32 条修复)尚未合入 main;远端恢复后与 #1 一并处理(PR 评审或本地合并) |
|
||||
|
||||
## 二、待业务方澄清(24 项,答复后回填开发计划 3.3 与需求记录)
|
||||
## 二、业务方澄清(24 项,**2026-10-16 答复已到**,回执见《待澄清问题清单_2026-10-15(1)(2).xlsx》H 列)
|
||||
|
||||
> 答复汇总:**12 项确认按默认执行**(#7/#8/#9/#10/#11/#13/#15/#17/#18/#21/#23/#24);**4 项答复与默认不同,以答复为准**——#5 二次分配审批开关**默认开启**、#14 **首日整 10‰**、#16 试用期基本工资**按人员档案逐人维护**、#19 备份**加增量补充**;仍未落定见下表「答复状态」列。答复后待回填开发计划 3.3 与需求记录。
|
||||
|
||||
### 高优先级(阻塞对应模块开工,8 项)
|
||||
|
||||
| 清单编号 | 问题 | 建议默认(未答复时执行) | 阻塞模块 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 二次分配的分配标的:绩效金额、折算毛利润还是业绩额? | 未答复前只做方案录入,不实现分摊(已按此交付) | ④ 分摊计算 |
|
||||
| 2 | 分配比例合计不足 100% 时余额归谁? | 余额归合同负责营销人员(仅展示不落库) | ④ 分摊计算 |
|
||||
| 3 | 二次分配与已作废的「协助 25% 分成」是什么关系? | 独立新机制;协助仍仅记录不核算 | ④ |
|
||||
| 4 | 二次分配方案的修改窗口与权限? | 首次月结前可改(留痕),月结后只读走调整单 | ③/④ |
|
||||
| 6 | 营销后勤/财务/领导的角色权限清单(可见菜单/可操作点)? | 最小授权:后勤=提交录入,财务=复核+导出,领导=审核只读 | ⑦ / permissions 激活 |
|
||||
| 8 | 回款何时算「确认回款」? | 需人工确认动作(系统已按此预留) | ④ 月结取数 |
|
||||
| 9 | 调整单是否走同一审批链?能否跨月调整已锁月份? | 同一审批链;后续月份扣补为默认路径 | ④/⑦ |
|
||||
| 12 | **经理个人户业绩是否计入其 12.5% 基数?(无默认,必须答复)** | —— | ④ 台账表结构设计 |
|
||||
| 清单编号 | 问题 | 建议默认(未答复时执行) | 阻塞模块 | 答复状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 二次分配的分配标的:绩效金额、折算毛利润还是业绩额? | 未答复前只做方案录入,不实现分摊(已按此交付) | ④ 分摊计算 | ❌ 未答复 |
|
||||
| 2 | 分配比例合计不足 100% 时余额归谁? | 余额归合同负责营销人员(仅展示不落库) | ④ 分摊计算 | ❌ 未答复 |
|
||||
| 3 | 二次分配与已作废的「协助 25% 分成」是什么关系? | 独立新机制;协助仍仅记录不核算 | ④ | ❌ 未答复 |
|
||||
| 4 | 二次分配方案的修改窗口与权限? | 首次月结前可改(留痕),月结后只读走调整单 | ③/④ | ⚠️ 部分:「暂定可改走调整单」,窗口/权限未定 |
|
||||
| 6 | 营销后勤/财务/领导的角色权限清单(可见菜单/可操作点)? | 最小授权:后勤=提交录入,财务=复核+导出,领导=审核只读 | ⑦ / permissions 激活 | ⚠️ 部分:「先预设角色」,名称与职权待客户确认 |
|
||||
| 8 | 回款何时算「确认回款」? | 需人工确认动作(系统已按此预留) | ④ 月结取数 | ✅ 确认:人工确认,可手动关闭 |
|
||||
| 9 | 调整单是否走同一审批链?能否跨月调整已锁月份? | 同一审批链;后续月份扣补为默认路径 | ④/⑦ | ✅ 确认默认;跨月需管理员授权并留痕 |
|
||||
| 12 | **经理个人户业绩是否计入其 12.5% 基数?(无默认,必须答复)** | —— | ④ 台账表结构设计 | ⚠️ 模糊:「做参数可调」,初始值未给 |
|
||||
|
||||
### 中优先级(模块实现期内答复即可,9 项)
|
||||
|
||||
| 清单编号 | 问题 | 建议默认 |
|
||||
| 清单编号 | 问题 | 答复状态(2026-10-16) |
|
||||
| --- | --- | --- |
|
||||
| 5 | 二次分配审批开关默认值? | 默认关闭(提交直接生效) |
|
||||
| 7 | 月度结算审批链默认开还是关? | 默认开启(从严) |
|
||||
| 10 | 「风险预警」包含哪些情形与阈值? | 首批:回款逾期 90 天/同步失败/审批超时 3 天,参数化 |
|
||||
| 11 | 「待办事项」范围? | 待审批+同步失败+回款未指定批次起步 |
|
||||
| 13 | 支付环节 5 项默认值顺手确认(起付 60%/R×R/预付 10‰/抵顶 80%/月工资口径) | 按默认执行(已参数化) |
|
||||
| 15 | 支付权重「余额模式」是否需要? | 一期只用 FULL |
|
||||
| 16 | 试用期基本工资(3000 元)是统一参数还是逐人维护? | 暂按统一参数 |
|
||||
| 20 | 含税口径:毛利/运费/价税合计是否含税? | 均不含税 |
|
||||
| 22 | 预收款/保证金/代收款是否计入绩效回款? | 不计入,转正式收款后按到账日计提 |
|
||||
| 5 | 二次分配审批开关默认值? | 🔁 **与默认不同:默认开启**,业务可关闭 |
|
||||
| 7 | 月度结算审批链默认开还是关? | ✅ 默认开启,可手动关闭 |
|
||||
| 10 | 「风险预警」包含哪些情形与阈值? | ✅ 方向确认:参数化;首批三条(逾期 90 天/同步失败/超时 3 天)未点名,按默认起步 |
|
||||
| 11 | 「待办事项」范围? | ✅ 确认:待审批+同步失败+回款未指定批次起步 |
|
||||
| 13 | 支付环节 5 项默认值顺手确认(起付 60%/R×R/预付 10‰/抵顶 80%/月工资口径) | ✅ 确认按默认执行 |
|
||||
| 15 | 支付权重「余额模式」是否需要? | ✅ 一期只用 FULL |
|
||||
| 16 | 试用期基本工资(3000 元)是统一参数还是逐人维护? | 🔁 **与默认不同:按人员档案逐人维护**(用 `personnel.salary_standard`,撤 `probation.salary.basic` 参数) |
|
||||
| 20 | 含税口径:毛利/运费/价税合计是否含税? | ❌ 未答复:「需与客户再次确认具体计算方式」 |
|
||||
| 22 | 预收款/保证金/代收款是否计入绩效回款? | ❌ 未答复:「当前未提到预收款情况」,暂按不计入 |
|
||||
|
||||
### 低优先级(已有默认口径,顺手确认,7 项)
|
||||
|
||||
| 清单编号 | 问题 | 建议默认 |
|
||||
| 清单编号 | 问题 | 答复状态(2026-10-16) |
|
||||
| --- | --- | --- |
|
||||
| 14 | 需规图示「第 1 天 10‰」与公式不一致,是否认可按公式? | 按公式 9.9889‰ |
|
||||
| 17 | 计提参数等敏感配置普通用户可否查看? | 查看可、修改仅管理员 |
|
||||
| 18 | 企业户 02-17 之前的业务是否按 2‰ 回溯? | 已按回溯 |
|
||||
| 19 | 备份保留周期与恢复目标? | 每日全量、保留 30 天 |
|
||||
| 21 | 退货/红字是否冲减绩效? | 当期冲减(未结算月),已结算走调整单 |
|
||||
| 23 | 回款按到账金额还是扣手续费净额? | 按到账金额 |
|
||||
| 24 | 「首期不生成负绩效」与裁决「负绩效不归零」冲突,以哪个为准? | 维持裁决:负数照算、月结支持扣补 |
|
||||
| 14 | 需规图示「第 1 天 10‰」与公式不一致,是否认可按公式? | 🔁 **与默认不同:首日整 10‰**。实现口径待追问:A 仅首日覆盖(保「360 天=6‰」锚点,推荐)/ B 整体平移(破坏锚点) |
|
||||
| 17 | 计提参数等敏感配置普通用户可否查看? | ✅ 查看可、修改仅管理员 |
|
||||
| 18 | 企业户 02-17 之前的业务是否按 2‰ 回溯? | ✅ 确认按回溯 |
|
||||
| 19 | 备份保留周期与恢复目标? | 🔁 **新增要求:每日全量 + 有更新时增量补充**(现三脚本仅全量) |
|
||||
| 21 | 退货/红字是否冲减绩效? | ✅ 当期冲减(未结算月),已结算走调整单 |
|
||||
| 23 | 回款按到账金额还是扣手续费净额? | ✅ 按到账金额 |
|
||||
| 24 | 「首期不生成负绩效」与裁决「负绩效不归零」冲突,以哪个为准? | ✅ 维持裁决:负数照算不归零、月结支持扣补(业务补注:按当前算法首期不会出现负绩效) |
|
||||
|
||||
## 三、待 IT / T+ 侧答复(18 项,见《T+对接数据需求清单》v1.2.1「待IT确认事项」表)
|
||||
## 三、待 IT / T+ 侧答复(18 项,见《T+对接数据需求清单》v1.3「待IT确认事项」表)
|
||||
|
||||
> **2026-10-16 业务方已对「口径与用途说明」修订,已收编清单升 v1.3**:#18 回款↔发货批次定稿**人工指定**(T+ 核销关系仅作辅助,`shipment_id` 保持「同步不覆盖」,v1.2.1「T+ 主源」修订作废,M2 回款页现状不动);「确认回款」两处 #8 开放引用收敛;特殊业务标记待确认降为低优。
|
||||
> **新增追问 2 条**(随下一轮回执发业务方):① 「负责营销人员」口径被改为「人员编码**以姓名**为唯一识别」,与「编码是业绩归属唯一锚点(防重名错钱)」矛盾,疑笔误;② 数据清单回款「关联订单号」仍写「由 T+ 收款分配关系指定,人工辅助」,与不覆盖表「人工指定」主次相反,疑漏改。
|
||||
|
||||
### 高优先级(8 项)
|
||||
|
||||
@ -63,7 +68,7 @@
|
||||
| 3 | 到账日期口径(银行到账日/核销日) |
|
||||
| 4 | 幂等唯一键规则(作废重开是否换号) |
|
||||
| 5 | 三单关联键(订单/发货/回款如何关联) |
|
||||
| 6 | **人员唯一编码(工号)+ 业务员↔经理隶属**(隶属已列必须) |
|
||||
| 6 | **人员唯一编码(工号)+ 业务员↔经理隶属**(隶属已列必须;识别口径疑笔误见上方追问①) |
|
||||
| 7 | 对接方式与安全(REST/视图/中间表/文件) |
|
||||
| 8 | 负数/红字/退货/冲销处理规则与原单关联 |
|
||||
| 16 | 回款确认/核销状态字段与枚举 |
|
||||
@ -76,7 +81,7 @@
|
||||
|
||||
| 里程碑 | 内容 | 前置条件 |
|
||||
| --- | --- | --- |
|
||||
| M3 剩余 | ④ 核算引擎 + 月结/调整单/负数工资 + ⑦ 月结审批对象接入 | #12/#20–#24 答复后按同粒度另出计划 |
|
||||
| M3 剩余 | ④ 核算引擎 + 月结/调整单/负数工资 + ⑦ 月结审批对象接入 | #12(基数参数初始值)、#20(含税口径)、#1–#3(二次分摊)落定后按同粒度另出计划;#14 首日 10‰ 口径(A/B)确认后随本批实现 |
|
||||
| M4 | ⑤ 报表(签字导出模板)+ ⑧ 工作台 | ④ 交付后 |
|
||||
| M4(并行) | ⑥ T+ 数据同步 | IT 答复字段映射后 |
|
||||
| code-review 遗留 2 项 | 方案生效版本承载、审批单内容快照 | ④ 分摊语义落定时一并设计 |
|
||||
@ -85,4 +90,5 @@
|
||||
## 五、文档待办(低优)
|
||||
|
||||
- 需规过时条款未修订(26 天示例、含运费公式、协助 25%);2026-10-15 补充需求未回填需规——对外引用需规前须先修订或注明以对照表/计划第三章为准
|
||||
- 澄清答复后:回填开发计划 3.3、AI_CONTEXT、《需求规格说明书》升 V2.1
|
||||
- 澄清答复已到(2026-10-16):待回填开发计划 3.3、AI_CONTEXT、《需求规格说明书》升 V2.1;《T+对接数据需求清单》已升 **v1.3**(收编业务方口径修订,生成脚本 build_tplus_xlsx.py 同步对齐)
|
||||
- 新口径待落实(业务答复与默认不同的 4 项):#5 审批开关默认开启(核对 M3 参数种子)、#14 首日整 10‰(④ 实现,A/B 口径待追问)、#16 试用期工资迁人员档案、#19 备份补增量能力
|
||||
|
||||
Loading…
Reference in New Issue
Block a user