PayrollManagementSystem/backend/app/services/payroll_inputs.py
liuwenxin 71b7846a79 refactor(后端): 将工资与工作区业务逻辑下沉服务层
将月份状态、资料输出、调整单编排及工作区查询、
CSV 生成移至现有服务层,路由仅绑定参数并返回响应。

保持接口路径、参数、响应模型和权限依赖一致,
补充九项内存库行为回归并同步测试记录。
2026-11-02 09:42:55 +08:00

192 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""人工业务核算准备:复用已有单据,显式确认期初、成本与发货归属。"""
from decimal import Decimal
from sqlalchemy import select
from app.core.exceptions import BadRequestError, NotFoundError
from app.models import (
Business,
BusinessItem,
BusinessRole,
Customer,
PayrollInput,
Personnel,
Receipt,
Shipment,
ShipmentItem,
)
from app.services import audit_service, payroll_months
from app.services.audit_service import AuditRecord
from app.services.payroll_calculation import latest_input
from app.services.tplus.workbench import digest
def detail(session, business_id):
"""返回最新核算资料;未准备资料的业务保持空结果。"""
record = latest_input(session, business_id)
if record is None:
return None
return {
"id": record.id,
"version": record.version,
"source": record.source_database,
"fingerprint": record.fingerprint,
"data": record.data_json,
}
def setup(session, business_id, payload, operator, meta=None):
with audit_service.audit(
session,
operator=operator,
record=AuditRecord(
module="绩效核算",
action="核算资料版本",
summary=f"业务 #{business_id} 核算资料",
request_data=payload.model_dump(mode="json"),
),
meta=meta,
):
payroll_months.ensure_allocation_editable(session, payload.month, operator)
business = session.get(Business, business_id, with_for_update=True)
if business is None:
raise NotFoundError("业务不存在")
current = latest_input(session, business_id, for_update=True)
if current and current.source_database != "MANUAL":
raise BadRequestError("源导入业务请通过来源差异核对更新,不能用人工资料替换")
if (current.version if current else 0) != payload.expected_version:
raise BadRequestError("核算资料版本已变化,请刷新")
customer = session.get(Customer, business.customer_id)
heads = list(session.scalars(select(Shipment).where(Shipment.business_id == business_id)))
by_head = {h.id: h for h in heads}
items = list(
session.scalars(select(ShipmentItem).where(ShipmentItem.shipment_id.in_(by_head)))
)
assignments = {i.shipment_item_id: i for i in payload.shipment_ownerships}
if len(assignments) != len(payload.shipment_ownerships) or set(assignments) != {
i.id for i in items
}:
raise BadRequestError("请逐条核实全部发货明细归属,不可遗漏、重复或跨业务")
business_items = list(
session.scalars(select(BusinessItem).where(BusinessItem.business_id == business_id))
)
roles = {r.id: r.role_code for r in session.scalars(select(BusinessRole))}
people = {p.id: p for p in session.scalars(select(Personnel))}
key = business.business_no
ships = []
for item in items:
owner = assignments[item.id]
if owner.personnel_id not in people or owner.manager_id not in people:
raise BadRequestError("发货归属人员或分管经理不存在")
if roles.get(people[owner.manager_id].business_role_id) != "sales_manager":
raise BadRequestError("分管人员必须为营销经理")
candidates = [
b
for b in business_items
if (
b.product_id == item.product_id
if item.product_id
else b.product_name == item.product_name and b.spec_model == item.spec_model
)
]
if len(candidates) != 1:
raise BadRequestError("发货产品无法唯一确定核算分类,请先核对业务明细")
head = by_head[item.shipment_id]
if item.amount <= 0:
raise BadRequestError("请先补齐发货销售金额")
ships.append(
{
"source_id": str(head.id),
"line_id": str(item.id),
"source_no": head.shipment_no,
"date": head.ship_date.isoformat(),
"group_key": key,
"business_key": key,
"personnel_id": str(owner.personnel_id),
"manager_id": str(owner.manager_id),
"amount": str(item.amount),
"quantity": str(item.quantity),
"unit": item.unit,
"category": candidates[0].product_category,
"customer_type": customer.customer_type,
"cost_total": str(
(Decimal(owner.unit_cost) * item.quantity).quantize(
Decimal("0.01"), rounding="ROUND_HALF_UP"
)
),
"unit_cost": owner.unit_cost,
"cost_month_basis": "SHIPMENT",
"pending": [],
}
)
receipts = list(session.scalars(select(Receipt).where(Receipt.business_id == business_id)))
if any(r.is_barter for r in receipts):
raise BadRequestError("抵顶特殊绩效继续线下处理,不进入普通FIFO核算")
if current:
previous_costs = {s["line_id"]: s for s in current.data_json["shipments"]}
for ship in ships:
old = previous_costs.get(ship["line_id"])
if old and (old["cost_total"] != ship["cost_total"] or old["date"] != ship["date"]):
raise BadRequestError("原发货月份指导成本已封存,不得随后续回款更改")
handovers = [h.model_dump(mode="json") for h in payload.handovers]
if any(h["business_key"] != key for h in handovers):
raise BadRequestError("交接记录不属于本业务")
values = {
"group": {
"key": key,
"scope_confirmed": True,
"opening_confirmed": True,
"opening_balance": payload.opening_balance,
"historical_settlement_confirmed": True,
},
"shipments": ships,
"receipts": [
{
"source_id": str(r.id),
"source_no": r.receipt_batch_no,
"date": r.receipt_date.isoformat(),
"group_key": key,
"amount": str(r.amount),
"voucher_confirmed": True,
"pending": [],
}
for r in receipts
],
"facts": {
"personnel": {
str(p.id): {"role_code": roles.get(p.business_role_id), "status": p.status}
for p in people.values()
},
"handovers": handovers,
},
"maps": {
"receipts": {str(r.id): r.id for r in receipts},
"personnel": {str(p.id): p.id for p in people.values()},
"shipments": {str(h.id): h.id for h in heads},
"shipment_items": {str(i.id): i.id for i in items},
},
"evidence": payload.evidence,
"guidance_finalized": True,
"as_of_month": payload.month,
}
# 旧资料包含的单据金额和归属保持稳定,新增版本用于新增发货与接手事实。
record = PayrollInput(
business_id=business_id,
version=payload.expected_version + 1,
source_database="MANUAL",
group_key=key,
fingerprint=digest(values),
data_json=values,
requested_by=operator.id,
)
session.add(record)
session.flush()
result = {
"input_id": record.id,
"version": record.version,
"fingerprint": record.fingerprint,
}
session.commit()
return result