PayrollManagementSystem/backend/app/services/payroll_inputs.py

203 lines
8.6 KiB
Python
Raw Normal View History

"""人工业务核算准备:复用已有单据,显式确认期初、成本与发货归属。"""
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):
source_business = session.get(Business, business_id)
if source_business and source_business.integration_json:
from app.services.business_source import prepare
return prepare(session, business_id, payload, operator, meta)
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)))
# 用户 2026-11-03:抵顶按普通业务计算,特殊应得绩效线下核实,
# 不自动乘80%,也不因抵顶标记阻断整个客户的正常平账。
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:
old_unit_cost = old.get("unit_cost")
if (old["cost_total"] != ship["cost_total"] or old["date"] != ship["date"]
or old_unit_cost is None or Decimal(str(old_unit_cost)) != Decimal(ship["unit_cost"])):
raise BadRequestError("原发货月份指导成本已封存,不得随后续回款更改")
if current and "handovers" not in payload.model_fields_set:
handovers = current.data_json.get("facts", {}).get("handovers", [])
else:
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