"""人工业务核算准备:复用已有单据,显式确认期初、成本与发货归属。""" 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