2026-11-01 20:42:53 -05:00
|
|
|
|
"""正式FIFO核算:按月份固化逐段计提和累计可付差额,不覆盖历史工资。"""
|
|
|
|
|
|
|
|
|
|
|
|
import copy
|
|
|
|
|
|
from collections import defaultdict
|
|
|
|
|
|
from datetime import date
|
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
from fractions import Fraction
|
|
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import func, select
|
|
|
|
|
|
|
|
|
|
|
|
from app.core.exceptions import BadRequestError, NotFoundError
|
|
|
|
|
|
from app.core.response import PageResult
|
|
|
|
|
|
from app.models import (
|
|
|
|
|
|
Business,
|
|
|
|
|
|
BusinessRole,
|
|
|
|
|
|
PayrollAllocation,
|
|
|
|
|
|
PayrollInput,
|
|
|
|
|
|
PerformanceLedger,
|
|
|
|
|
|
Personnel,
|
|
|
|
|
|
Receipt,
|
|
|
|
|
|
Shipment,
|
|
|
|
|
|
ShipmentItem,
|
|
|
|
|
|
)
|
|
|
|
|
|
from app.services import audit_service, formula, payroll_months
|
|
|
|
|
|
from app.services.audit_service import AuditRecord
|
|
|
|
|
|
from app.services.param_service import resolve_snapshot
|
2026-11-05 05:47:13 -05:00
|
|
|
|
from app.services.personnel_roles import input_role
|
2026-11-01 20:42:53 -05:00
|
|
|
|
from app.services.tplus.calculation_preview import _parameters
|
2026-11-01 20:42:55 -05:00
|
|
|
|
from app.services.tplus.fifo import START, _beneficiary, amount_text, money, preview_group
|
2026-11-01 20:42:53 -05:00
|
|
|
|
from app.services.tplus.workbench import digest
|
|
|
|
|
|
|
2026-11-02 22:20:55 -05:00
|
|
|
|
# 核算规则版本:改公式或口径时同步更新,写入台账快照用于追溯。
|
2026-11-05 05:47:13 -05:00
|
|
|
|
RULE_VERSION = "2026-11-04.1"
|
2026-11-02 22:20:55 -05:00
|
|
|
|
|
2026-11-01 20:42:53 -05:00
|
|
|
|
|
|
|
|
|
|
def latest_input(session, business_id, *, for_update=False):
|
|
|
|
|
|
statement = (
|
|
|
|
|
|
select(PayrollInput)
|
|
|
|
|
|
.where(PayrollInput.business_id == business_id)
|
|
|
|
|
|
.order_by(PayrollInput.version.desc())
|
|
|
|
|
|
.limit(1)
|
|
|
|
|
|
)
|
|
|
|
|
|
if for_update:
|
|
|
|
|
|
statement = statement.with_for_update().execution_options(populate_existing=True)
|
|
|
|
|
|
return session.scalar(statement)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _event_key(line):
|
|
|
|
|
|
return digest([line.get("receipt_id"), line.get("shipment_id"), line.get("line_id")])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _previous(session, business_id, month):
|
|
|
|
|
|
rows = list(
|
|
|
|
|
|
session.scalars(
|
|
|
|
|
|
select(PerformanceLedger)
|
|
|
|
|
|
.where(
|
|
|
|
|
|
PerformanceLedger.business_id == business_id,
|
|
|
|
|
|
PerformanceLedger.calc_type == 1,
|
|
|
|
|
|
PerformanceLedger.is_active == 1,
|
|
|
|
|
|
)
|
|
|
|
|
|
.order_by(PerformanceLedger.id)
|
|
|
|
|
|
.with_for_update()
|
|
|
|
|
|
.execution_options(populate_existing=True)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if any(r.settlement_month >= month for r in rows):
|
|
|
|
|
|
raise BadRequestError("该业务本月或后续月份已有核算;重算需先处理未生成工资的后续月份")
|
|
|
|
|
|
events = {}
|
|
|
|
|
|
paid = defaultdict(lambda: Decimal(0))
|
|
|
|
|
|
for row in rows:
|
|
|
|
|
|
snapshot = row.snapshot_json or {}
|
|
|
|
|
|
if snapshot.get("engine") != "FIFO":
|
|
|
|
|
|
raise BadRequestError("该业务存在旧口径台账,请先核对历史承接,不能混算")
|
|
|
|
|
|
key = snapshot["event_key"]
|
|
|
|
|
|
if snapshot.get("first_accrual"):
|
|
|
|
|
|
events[key] = snapshot["event"]
|
|
|
|
|
|
paid[key] += row.payable_amount or Decimal(0)
|
|
|
|
|
|
return events, paid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _inputs(session, record, until):
|
2026-11-05 05:47:13 -05:00
|
|
|
|
from app.services.receipt_confirmation import ensure_confirmed
|
|
|
|
|
|
|
2026-11-01 20:42:53 -05:00
|
|
|
|
values = copy.deepcopy(record.data_json)
|
|
|
|
|
|
ships, cash = values["shipments"], values["receipts"]
|
|
|
|
|
|
for row in cash:
|
|
|
|
|
|
if row.get("exclusion"):
|
|
|
|
|
|
continue
|
|
|
|
|
|
current = session.get(Receipt, values["maps"]["receipts"].get(str(row["source_id"])))
|
|
|
|
|
|
if (
|
|
|
|
|
|
current is None
|
|
|
|
|
|
or current.amount != money(row["amount"])
|
|
|
|
|
|
or current.receipt_date.isoformat() != row["date"]
|
|
|
|
|
|
or current.business_id != record.business_id
|
|
|
|
|
|
):
|
|
|
|
|
|
raise BadRequestError("正式回款与核算资料不一致,请重新核对资料版本")
|
2026-11-05 05:47:13 -05:00
|
|
|
|
ensure_confirmed(session, [
|
|
|
|
|
|
values["maps"]["receipts"].get(str(row["source_id"]))
|
|
|
|
|
|
for row in cash if not row.get("exclusion") and row["date"] <= until.isoformat()
|
|
|
|
|
|
])
|
2026-11-01 20:42:53 -05:00
|
|
|
|
for row in ships:
|
|
|
|
|
|
item_id = values["maps"].get("shipment_items", {}).get(str(row["line_id"]))
|
|
|
|
|
|
if item_id is not None:
|
|
|
|
|
|
item = session.get(ShipmentItem, item_id)
|
|
|
|
|
|
head = session.get(Shipment, item.shipment_id) if item else None
|
|
|
|
|
|
if (
|
|
|
|
|
|
item is None
|
|
|
|
|
|
or head is None
|
2026-11-05 05:47:13 -05:00
|
|
|
|
or item.amount != money(row.get("gross_tax_amount", row["amount"]) if values.get("input_schema") == "TPLUS_SYNC_V1" else row["amount"])
|
2026-11-01 20:42:53 -05:00
|
|
|
|
or head.ship_date.isoformat() != row["date"]
|
|
|
|
|
|
or head.business_id != record.business_id
|
|
|
|
|
|
):
|
|
|
|
|
|
raise BadRequestError("正式发货与核算资料不一致,请重新核对资料版本")
|
|
|
|
|
|
days = {row["date"] for row in [*ships, *cash] if row["date"] <= until.isoformat()}
|
|
|
|
|
|
params = _parameters(session, days)
|
|
|
|
|
|
params["prepayment_parameter_basis"], params["august_confirmed"] = "RECEIPT", True
|
|
|
|
|
|
result = preview_group(values["group"], ships, cash, values["facts"], params, until)
|
|
|
|
|
|
if result["status"] != "SUCCEEDED":
|
|
|
|
|
|
reasons = set(result.get("reasons", []))
|
|
|
|
|
|
reasons.update(
|
|
|
|
|
|
reason
|
|
|
|
|
|
for line in result["lines"]
|
|
|
|
|
|
for reason in line.get("reasons", [])
|
|
|
|
|
|
if line["status"] == "PENDING"
|
|
|
|
|
|
)
|
|
|
|
|
|
raise BadRequestError("核算资料待补:" + ",".join(sorted(reasons)))
|
|
|
|
|
|
return values, result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _post(
|
|
|
|
|
|
session,
|
|
|
|
|
|
business,
|
|
|
|
|
|
record,
|
|
|
|
|
|
event,
|
|
|
|
|
|
key,
|
|
|
|
|
|
month,
|
|
|
|
|
|
owner,
|
|
|
|
|
|
manager,
|
|
|
|
|
|
performance,
|
|
|
|
|
|
payable,
|
|
|
|
|
|
weight_r,
|
|
|
|
|
|
first,
|
|
|
|
|
|
version,
|
|
|
|
|
|
reason=None,
|
|
|
|
|
|
):
|
2026-11-02 22:20:55 -05:00
|
|
|
|
from app.services.probation_service import record as record_probation
|
|
|
|
|
|
|
2026-11-01 20:42:53 -05:00
|
|
|
|
person = session.get(Personnel, owner)
|
|
|
|
|
|
if person is None:
|
|
|
|
|
|
raise BadRequestError("正式归属人员不存在")
|
|
|
|
|
|
role = session.get(BusinessRole, person.business_role_id)
|
|
|
|
|
|
if role is None:
|
|
|
|
|
|
raise BadRequestError("正式归属人员岗位缺失")
|
|
|
|
|
|
receipt_id = record.data_json["maps"]["receipts"].get(str(event["receipt_id"]))
|
|
|
|
|
|
if receipt_id is None:
|
|
|
|
|
|
raise BadRequestError("回款尚未正式导入")
|
|
|
|
|
|
row = PerformanceLedger(
|
|
|
|
|
|
receipt_id=receipt_id,
|
|
|
|
|
|
calc_version=version,
|
|
|
|
|
|
allocation_key=key,
|
|
|
|
|
|
is_active=1,
|
|
|
|
|
|
calc_type=1,
|
|
|
|
|
|
business_id=business.id,
|
|
|
|
|
|
business_no=business.business_no,
|
|
|
|
|
|
personnel_id=owner,
|
|
|
|
|
|
personnel_name=person.personnel_name,
|
2026-11-05 05:47:13 -05:00
|
|
|
|
role_code=input_role(record.data_json, owner, event["effective_date"]) or role.role_code,
|
2026-11-01 20:42:53 -05:00
|
|
|
|
customer_type=event["customer_type"],
|
|
|
|
|
|
product_category=event["category"],
|
|
|
|
|
|
ship_date=date.fromisoformat(event["shipment_date"]),
|
|
|
|
|
|
receipt_date=date.fromisoformat(event["receipt_date"]),
|
|
|
|
|
|
payment_days=event["days"],
|
|
|
|
|
|
rate_text=event["rate"],
|
|
|
|
|
|
converted_profit=money(event["converted_profit"]) if first else Decimal(0),
|
|
|
|
|
|
performance_amount=performance,
|
|
|
|
|
|
payable_amount=payable,
|
|
|
|
|
|
barter_applied=0,
|
|
|
|
|
|
weight_r=weight_r,
|
|
|
|
|
|
payment_weight=formula.to_decimal(formula.payment_weight(weight_r)),
|
|
|
|
|
|
settlement_month=month,
|
2026-11-02 22:20:55 -05:00
|
|
|
|
is_probation=1 if event.get("is_probation", False) else 0,
|
2026-11-01 20:42:53 -05:00
|
|
|
|
missing_cost=0,
|
|
|
|
|
|
calc_reason=reason,
|
|
|
|
|
|
snapshot_json={
|
|
|
|
|
|
"engine": "FIFO",
|
|
|
|
|
|
"input_id": record.id,
|
|
|
|
|
|
"event_key": key,
|
|
|
|
|
|
"event": event,
|
|
|
|
|
|
"first_accrual": first,
|
|
|
|
|
|
"manager_id": manager,
|
|
|
|
|
|
"manager_own_excluded": event.get("excluded_manager_own", False),
|
|
|
|
|
|
"payable_delta": amount_text(payable),
|
2026-11-02 22:20:55 -05:00
|
|
|
|
"rule_version": RULE_VERSION,
|
2026-11-01 20:42:53 -05:00
|
|
|
|
"recalc_reason": reason,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
session.add(row)
|
|
|
|
|
|
session.flush()
|
2026-11-02 22:20:55 -05:00
|
|
|
|
record_probation(session, row)
|
2026-11-01 20:42:53 -05:00
|
|
|
|
session.add(
|
|
|
|
|
|
PayrollAllocation(
|
|
|
|
|
|
ledger_id=row.id,
|
|
|
|
|
|
business_id=business.id,
|
|
|
|
|
|
line_no=1,
|
|
|
|
|
|
input_id=record.id,
|
|
|
|
|
|
amount=money(event["amount"]) if first else Decimal(0),
|
|
|
|
|
|
data_json={
|
|
|
|
|
|
**event,
|
|
|
|
|
|
"event_key": key,
|
|
|
|
|
|
"first_accrual": first,
|
|
|
|
|
|
"performance_delta": amount_text(performance),
|
|
|
|
|
|
"payable_delta": amount_text(payable),
|
|
|
|
|
|
"beneficiary_id": owner,
|
|
|
|
|
|
"manager_id": manager,
|
|
|
|
|
|
"recalc_reason": reason,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return row
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate(session, business_id, month, *, operator, meta=None, reason=None):
|
2026-11-05 05:47:13 -05:00
|
|
|
|
business = session.get(Business, business_id)
|
|
|
|
|
|
if business is not None and business.integration_json:
|
|
|
|
|
|
from app.services.business_source import source_calculation_lock
|
|
|
|
|
|
|
|
|
|
|
|
with source_calculation_lock(session):
|
|
|
|
|
|
return _calculate_locked(session, business_id, month, operator=operator, meta=meta, reason=reason)
|
|
|
|
|
|
return _calculate_locked(session, business_id, month, operator=operator, meta=meta, reason=reason)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _calculate_locked(session, business_id, month, *, operator, meta=None, reason=None):
|
2026-11-01 20:42:53 -05:00
|
|
|
|
"""重算从指定月起级联重建尚未生成工资的月份,同一事务保存全部版本。"""
|
|
|
|
|
|
from app.services.manager_share import month_end
|
|
|
|
|
|
|
|
|
|
|
|
month_end(month)
|
|
|
|
|
|
if reason is not None and not reason.strip():
|
|
|
|
|
|
raise BadRequestError("重算原因不能为空")
|
|
|
|
|
|
with audit_service.audit(
|
|
|
|
|
|
session,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
record=AuditRecord(
|
|
|
|
|
|
module="绩效核算",
|
|
|
|
|
|
action="正式FIFO重算" if reason else "正式FIFO核算",
|
|
|
|
|
|
summary=f"业务 #{business_id} 核算 {month}",
|
|
|
|
|
|
request_data={"business_id": business_id, "month": month, "reason": reason},
|
|
|
|
|
|
),
|
|
|
|
|
|
meta=meta,
|
|
|
|
|
|
):
|
|
|
|
|
|
months = {month}
|
|
|
|
|
|
if reason:
|
|
|
|
|
|
months.update(
|
|
|
|
|
|
session.scalars(
|
|
|
|
|
|
select(PerformanceLedger.settlement_month).where(
|
|
|
|
|
|
PerformanceLedger.business_id == business_id,
|
|
|
|
|
|
PerformanceLedger.calc_type == 1,
|
|
|
|
|
|
PerformanceLedger.settlement_month >= month,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
for current_month in sorted(months):
|
|
|
|
|
|
payroll_months.ensure_allocation_editable(session, current_month, operator)
|
|
|
|
|
|
business = session.get(Business, business_id, with_for_update=True)
|
|
|
|
|
|
if business is None:
|
|
|
|
|
|
raise NotFoundError("业务不存在")
|
|
|
|
|
|
record = latest_input(session, business_id, for_update=True)
|
|
|
|
|
|
if record is None:
|
|
|
|
|
|
raise BadRequestError("请先补录该业务的正式核算资料")
|
2026-11-05 05:47:13 -05:00
|
|
|
|
if record.data_json.get("input_schema") == "TPLUS_SYNC_V1":
|
|
|
|
|
|
from app.services.business_source import LABELS, readiness
|
|
|
|
|
|
|
|
|
|
|
|
state = readiness(session, business_id)
|
|
|
|
|
|
blocking = {r["code"] for r in state["reasons"]} & {
|
|
|
|
|
|
"SOURCE_CONFLICT", "SOURCE_SNAPSHOT_PENDING", "INPUT_REFRESH_REQUIRED", "CUSTOMER_CATEGORY_PENDING",
|
|
|
|
|
|
"PRODUCT_CATEGORY_PENDING", "NO_ACTUAL_SHIPMENT", "SALES_RELATION_PENDING",
|
|
|
|
|
|
}
|
|
|
|
|
|
if blocking:
|
|
|
|
|
|
raise BadRequestError("核算资料待补:" + ";".join(LABELS[code] for code in sorted(blocking)))
|
2026-11-01 20:42:53 -05:00
|
|
|
|
if reason:
|
|
|
|
|
|
old_rows = list(
|
|
|
|
|
|
session.scalars(
|
|
|
|
|
|
select(PerformanceLedger)
|
|
|
|
|
|
.where(
|
|
|
|
|
|
PerformanceLedger.business_id == business_id,
|
|
|
|
|
|
PerformanceLedger.is_active == 1,
|
|
|
|
|
|
PerformanceLedger.calc_type == 1,
|
|
|
|
|
|
PerformanceLedger.settlement_month >= month,
|
|
|
|
|
|
)
|
|
|
|
|
|
.with_for_update()
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if not any(r.settlement_month == month for r in old_rows):
|
|
|
|
|
|
raise BadRequestError("该月尚无可重算台账")
|
|
|
|
|
|
if {r.settlement_month for r in old_rows} - months:
|
|
|
|
|
|
raise BadRequestError("后续月份在核算期间变化,请刷新后重试")
|
|
|
|
|
|
for old in old_rows:
|
|
|
|
|
|
if old.settlement_id is not None:
|
|
|
|
|
|
raise BadRequestError("后续月份已有工资生成,原工资保留,请走当月调整单")
|
|
|
|
|
|
old.is_active = 0
|
|
|
|
|
|
session.flush()
|
|
|
|
|
|
outputs = [
|
|
|
|
|
|
_calculate_month(
|
|
|
|
|
|
session, business, record, current_month, reason.strip() if reason else None
|
|
|
|
|
|
)
|
|
|
|
|
|
for current_month in sorted(months)
|
|
|
|
|
|
]
|
|
|
|
|
|
out = outputs[0]
|
|
|
|
|
|
out["recalculated_months"] = sorted(months) if reason else []
|
|
|
|
|
|
session.commit()
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _calculate_month(session, business, record, month, reason=None):
|
|
|
|
|
|
from app.services.manager_share import month_end
|
2026-11-02 22:20:55 -05:00
|
|
|
|
from app.services.probation_service import on_date
|
2026-11-01 20:42:53 -05:00
|
|
|
|
|
|
|
|
|
|
business_id = business.id
|
|
|
|
|
|
until = month_end(month)
|
|
|
|
|
|
previous, paid = _previous(session, business_id, month)
|
|
|
|
|
|
values, result = _inputs(session, record, until)
|
|
|
|
|
|
active_lines = [line for line in result["lines"] if line["kind"] == "SHIPMENT"]
|
|
|
|
|
|
current = {}
|
|
|
|
|
|
for line in active_lines:
|
|
|
|
|
|
key = _event_key(line)
|
|
|
|
|
|
if key in previous:
|
|
|
|
|
|
if any(
|
|
|
|
|
|
str(line.get(field)) != str(previous[key].get(field))
|
|
|
|
|
|
for field in ("amount", "shipment_date", "receipt_date")
|
|
|
|
|
|
):
|
|
|
|
|
|
raise BadRequestError("历史分配金额或日期已变化,请通过更正或调整处理")
|
|
|
|
|
|
current[key] = previous[key]
|
|
|
|
|
|
elif line["effective_date"][:7] < month:
|
|
|
|
|
|
raise BadRequestError(f"请先完成 {line['effective_date'][:7]} 的业务核算")
|
|
|
|
|
|
else:
|
2026-11-02 22:20:55 -05:00
|
|
|
|
shipments_by_line = {
|
|
|
|
|
|
str(s["line_id"]): s for s in values["shipments"]
|
|
|
|
|
|
}
|
|
|
|
|
|
ship = shipments_by_line.get(str(line["line_id"]))
|
|
|
|
|
|
if ship is None:
|
|
|
|
|
|
raise BadRequestError(f"发货明细缺失,行号 {line['line_id']} 未找到,请重新核对资料")
|
2026-11-01 20:42:53 -05:00
|
|
|
|
current[key] = {
|
|
|
|
|
|
**line,
|
|
|
|
|
|
"category": ship["category"],
|
|
|
|
|
|
"customer_type": ship["customer_type"],
|
|
|
|
|
|
"source_owner": ship["personnel_id"],
|
|
|
|
|
|
"business_key": ship["business_key"],
|
|
|
|
|
|
}
|
|
|
|
|
|
if set(previous) - current.keys():
|
|
|
|
|
|
raise BadRequestError("历史已核算事件发生变化,请核对更正或调整,不可直接覆盖")
|
2026-11-01 20:42:55 -05:00
|
|
|
|
# 启用前的发货由期初应收承接,不再进入新业务的起付比例分母。
|
2026-11-01 20:42:53 -05:00
|
|
|
|
sales = sum(
|
|
|
|
|
|
(
|
|
|
|
|
|
money(s["amount"])
|
|
|
|
|
|
for s in values["shipments"]
|
2026-11-01 20:42:55 -05:00
|
|
|
|
if START.isoformat() <= s["date"] <= until.isoformat()
|
|
|
|
|
|
and not s.get("performance_excluded")
|
2026-11-01 20:42:53 -05:00
|
|
|
|
),
|
|
|
|
|
|
Decimal(0),
|
|
|
|
|
|
)
|
|
|
|
|
|
matched = sum((money(line["amount"]) for line in active_lines), Decimal(0))
|
|
|
|
|
|
snapshot = resolve_snapshot(session, until)
|
|
|
|
|
|
threshold = Fraction(snapshot.get("payment.start.ratio"))
|
|
|
|
|
|
weight_r = formula.clamp_weight_r(matched, sales)
|
|
|
|
|
|
start_ok = formula.start_payment_ok(matched, sales, threshold)
|
|
|
|
|
|
weight = formula.payment_weight(weight_r) if start_ok else Fraction(0)
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
receipt_versions = {}
|
|
|
|
|
|
for key, event in current.items():
|
|
|
|
|
|
first = key not in previous
|
|
|
|
|
|
performance = money(event["performance"])
|
|
|
|
|
|
desired = formula.to_decimal(Fraction(performance) * weight).quantize(
|
|
|
|
|
|
Decimal("0.01"), rounding="ROUND_HALF_UP"
|
|
|
|
|
|
)
|
|
|
|
|
|
delta = desired - paid[key]
|
|
|
|
|
|
if not first and delta == 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
target = _beneficiary(
|
|
|
|
|
|
{"personnel_id": event["personnel_id"], "business_key": event["business_key"]},
|
|
|
|
|
|
{},
|
|
|
|
|
|
values["facts"].get("handovers", []),
|
|
|
|
|
|
until.isoformat(),
|
|
|
|
|
|
)
|
|
|
|
|
|
owner = values["maps"]["personnel"].get(str(target))
|
|
|
|
|
|
if owner is None:
|
|
|
|
|
|
raise BadRequestError("接手人员尚未正式导入")
|
2026-11-02 22:20:55 -05:00
|
|
|
|
person = session.get(Personnel, owner)
|
|
|
|
|
|
# 试用判定以回款日为准:_post 会把结果写进快照,历史台账不再按当前状态重分类。
|
|
|
|
|
|
receipt_day = date.fromisoformat(event["receipt_date"])
|
|
|
|
|
|
event = {
|
|
|
|
|
|
**event,
|
|
|
|
|
|
"is_probation": on_date(person, receipt_day),
|
|
|
|
|
|
"probation_year": event["receipt_date"][:4],
|
|
|
|
|
|
}
|
2026-11-01 20:42:53 -05:00
|
|
|
|
source_manager = event.get("manager_id")
|
|
|
|
|
|
manager = values["maps"]["personnel"].get(str(source_manager)) if source_manager else None
|
|
|
|
|
|
if not event.get("excluded_manager_own") and not manager:
|
|
|
|
|
|
raise BadRequestError("请补录该业务在发货时的经理分管关系")
|
|
|
|
|
|
rid = values["maps"]["receipts"].get(str(event["receipt_id"]))
|
|
|
|
|
|
if rid not in receipt_versions:
|
|
|
|
|
|
receipt_versions[rid] = (
|
|
|
|
|
|
int(
|
|
|
|
|
|
session.scalar(
|
|
|
|
|
|
select(func.max(PerformanceLedger.calc_version)).where(
|
|
|
|
|
|
PerformanceLedger.receipt_id == rid
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
or 0
|
|
|
|
|
|
)
|
|
|
|
|
|
+ 1
|
|
|
|
|
|
)
|
|
|
|
|
|
rows.append(
|
|
|
|
|
|
_post(
|
|
|
|
|
|
session,
|
|
|
|
|
|
business,
|
|
|
|
|
|
record,
|
|
|
|
|
|
event,
|
|
|
|
|
|
key,
|
|
|
|
|
|
month,
|
|
|
|
|
|
owner,
|
|
|
|
|
|
manager,
|
|
|
|
|
|
performance if first else Decimal(0),
|
|
|
|
|
|
delta,
|
|
|
|
|
|
weight_r,
|
|
|
|
|
|
first,
|
|
|
|
|
|
receipt_versions[rid],
|
|
|
|
|
|
reason=reason,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if not rows and not reason:
|
|
|
|
|
|
raise BadRequestError("本月没有新增计提或应付差额")
|
|
|
|
|
|
for row in rows:
|
|
|
|
|
|
row.calc_reason = reason
|
|
|
|
|
|
if reason:
|
|
|
|
|
|
row.snapshot_json = {**row.snapshot_json, "recalc_reason": reason}
|
|
|
|
|
|
for rid in receipt_versions:
|
|
|
|
|
|
session.get(Receipt, rid).settlement_status = 1
|
|
|
|
|
|
session.flush()
|
|
|
|
|
|
out = {
|
|
|
|
|
|
"month": month,
|
|
|
|
|
|
"business_id": business_id,
|
|
|
|
|
|
"ledger_ids": [r.id for r in rows],
|
|
|
|
|
|
"performance": amount_text(sum((r.performance_amount for r in rows), Decimal(0))),
|
|
|
|
|
|
"payable": amount_text(sum((r.payable_amount for r in rows), Decimal(0))),
|
|
|
|
|
|
"start_payment_ok": start_ok,
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def allocation_page(session, ledger_id, query):
|
|
|
|
|
|
if session.get(PerformanceLedger, ledger_id) is None:
|
|
|
|
|
|
raise NotFoundError("台账不存在")
|
|
|
|
|
|
condition = PayrollAllocation.ledger_id == ledger_id
|
|
|
|
|
|
rows = session.scalars(
|
|
|
|
|
|
select(PayrollAllocation)
|
|
|
|
|
|
.where(condition)
|
|
|
|
|
|
.order_by(PayrollAllocation.line_no)
|
|
|
|
|
|
.offset(query.offset)
|
|
|
|
|
|
.limit(query.page_size)
|
|
|
|
|
|
)
|
|
|
|
|
|
return PageResult(
|
|
|
|
|
|
records=[
|
|
|
|
|
|
{"id": r.id, "input_id": r.input_id, "amount": str(r.amount), **r.data_json}
|
|
|
|
|
|
for r in rows
|
|
|
|
|
|
],
|
|
|
|
|
|
total=session.scalar(select(func.count()).select_from(PayrollAllocation).where(condition))
|
|
|
|
|
|
or 0,
|
|
|
|
|
|
page_num=query.page_num,
|
|
|
|
|
|
page_size=query.page_size,
|
|
|
|
|
|
)
|