"""需求补齐的业务金额回归,覆盖配置实际消费而非页面存在性。""" from datetime import date from decimal import Decimal from fractions import Fraction from io import BytesIO import pytest from openpyxl import load_workbook from app.core.exceptions import BadRequestError from app.schemas.business import BusinessItemIn from app.schemas.coefficient import CoefficientRowIn from app.services import coefficient_service from app.services.param_service import ParamSnapshot from app.services.rate_rules import decide from app.services.tplus.calculation_preview import _parameters from tests.unit.test_formal_payroll import imported from tests.unit.test_formal_payroll import payroll_case as payroll_case from tests.unit.test_tplus_fifo import inputs, run from tests.unit.test_tplus_workbench import work_session as work_session def test_企业户分类及信息员金额(monkeypatch): snapshot = ParamSnapshot.from_rows( date(2026, 11, 2), [ ("enterprise.rate", "", "3", date(2026, 8, 13)), ("enterprise.rate", "municipal_water", "2", date(2026, 8, 13)), ("enterprise.rate", "rail_transit", "5", date(2026, 8, 13)), ], ) monkeypatch.setattr( "app.services.tplus.calculation_preview.param_service.resolve_snapshot", lambda *args: snapshot, ) params = _parameters(None, {"2026-11-02"})["2026-11-02"] assert decide( params, category="municipal_water", customer_type="enterprise", role_code="salesperson", days=1, )[0] == Fraction(2, 1000) assert decide( params, category="rail_transit", customer_type="enterprise", role_code="salesperson", days=1 )[0] == Fraction(5, 1000) group, ship, cash, facts, values = inputs() ship.update(amount="10000", cost_total="5000", customer_type="enterprise") cash["amount"] = "10000" facts["personnel"]["001"]["role_code"] = "info_officer" values["2026-03-10"]["info"] = "1" result = run(group, [ship], [cash], facts, values) assert result["lines"][0]["performance"] == "10.00" assert result["lines"][0]["calculation_base"] == "10000.00" def test_指导单位成本自动换算且冲突拒绝(): data = { "product_name": "测试", "product_category": "municipal_water", "quantity": "10", "sale_price": "100", "guided_price": "50", } assert BusinessItemIn(**data).cost_amount == "500.00" with pytest.raises(ValueError, match="不一致"): BusinessItemIn(**data, cost_amount="501") def test_系数工作簿往返与派生冲突(monkeypatch): snapshot = ParamSnapshot.from_rows(date.today(), []) monkeypatch.setattr( coefficient_service.param_service, "resolve_snapshot", lambda *args: snapshot ) content = coefficient_service.workbook(None) preview = coefficient_service.import_preview(None, content) assert preview["can_apply"], preview["errors"] assert len(preview["rows"]) == 8 book = load_workbook(BytesIO(content)) book["系数参数"].cell(2, 7, "999999") output = BytesIO() book.save(output) assert not coefficient_service.import_preview(None, output.getvalue())["can_apply"] def test_非法系数整批拒绝(): row = CoefficientRowIn( product_category="municipal_water", customer_type="personal", stage="linear", value="1", slope="1", boundary=360, ) with pytest.raises(BadRequestError): coefficient_service.items_for([row]) def test_分界整数小数原文及预收优先级(): params = {"start": "10", "first": "11", "slope": "4/360", "floor": "4", "boundary": "360.0", "enterprise": "3"} rate, _, _ = decide(params, category="municipal_water", customer_type="personal", role_code="salesperson", days=360) assert rate == Fraction(6, 1000) rate, _, _ = decide(params, category="municipal_water", customer_type="enterprise", role_code="salesperson", days=-8, prepayment=True) assert rate == Fraction(11, 1000) rate, _, _ = decide(params, category="municipal_water", customer_type="enterprise", role_code="sales_manager", days=1) assert rate == 0 def test_基本工资仅计一次及试用期年度结清(payroll_case): from sqlalchemy import select from app.models import MonthlySettlement, PerformanceLedger, Personnel from app.services import payroll_calculation, probation_service, salary_completion session, user, _ = payroll_case business = imported(payroll_case) payroll_calculation.calculate(session, business.id, "2026-03", operator=user) row = session.scalar(select(PerformanceLedger)) person = session.get(Personnel, row.personnel_id) person.salary_standard = Decimal("3000") person.probation_start_date = date(2026, 3, 1) person.regularization_date = date(2027, 1, 1) row.is_probation = 1 row.performance_amount = Decimal("20000") row.payable_amount = Decimal("1000") session.flush() before, _ = salary_completion.build_items(session, [row], "2026-11") item = next(i for i in before if i.personnel_id == person.id) assert item.basic_amount == Decimal("3000") assert item.net_amount == Decimal("3000") assert item.payable_sum == 0 assert item.deferred_sum == Decimal("1000") december, _ = salary_completion.build_items(session, [row], "2026-12") item = next(i for i in december if i.personnel_id == person.id) assert item.probation_payment_sum == Decimal("1000") assert item.net_amount == Decimal("4000") settlement = MonthlySettlement( settlement_month="2026-12", status=3, submitter_id=user.id, submitter_name=user.username ) session.add(settlement) session.flush() probation_service.persist_payments(session, settlement, december) session.flush() assert probation_service.preview(session, "2027-01")["rows"][0]["delta"] == "0.00" row.payable_amount = Decimal("3000") session.flush() assert Decimal(probation_service.preview(session, "2027-01")["rows"][0]["delta"]) == Decimal( "1000" ) row.performance_amount = Decimal("18000") session.flush() assert Decimal(probation_service.preview(session, "2027-01")["rows"][0]["delta"]) == Decimal( "-1000" ) def test_无绩效在职人员有基本工资(payroll_case): from sqlalchemy import select from app.models import Personnel from app.services import salary_completion session, _, _ = payroll_case person = session.scalar(select(Personnel).where(Personnel.source_no == "001")) if person is None: person = session.scalar(select(Personnel)) person.salary_standard = Decimal("3000") person.is_probation = 1 session.flush() items, ids = salary_completion.build_items(session, [], "2026-11") item = next(i for i in items if i.personnel_id == person.id) assert item.net_amount == Decimal("3000") assert ids == [] def test_最近有效发货顺序与人工保护(work_session): from app.models import Product from app.services.product_completion import recent_trades from app.services.product_service import _to_out from tests.unit.test_tplus_workbench import source source( work_session, "shipments", [ {"id": 20, "code": "SH-20", "voucherdate": "2026-11-01", "voucherState": 189}, {"id": 21, "code": "SH-21", "voucherdate": "2026-11-01", "voucherState": 189}, { "id": 22, "code": "SH-22", "voucherdate": "2026-11-02", "voucherState": 189, "isCounteractDelivery": 1, }, ], ) source( work_session, "shipment_items", [ { "id": 1, "source_parent_id": 20, "idinventory": 9, "quantity": "2", "taxPrice": "12.1234", }, { "id": 2, "source_parent_id": 21, "idinventory": 9, "quantity": "3", "taxPrice": "22.1234", }, {"id": 3, "source_parent_id": 22, "idinventory": 9, "quantity": "4", "taxPrice": "99"}, ], ) trade = recent_trades(work_session, "test_source", ["9"])["9"] assert trade["source_no"] == "SH-21" assert trade["quantity"] == "3" product = Product( product_name="测试来源", spec_model="", product_category="municipal_water", source_trade_json=trade, stock_quantity=Decimal("7"), sale_price=Decimal("88"), quantity_price_manual=1, status=1, ) work_session.add(product) work_session.flush() assert _to_out(product).stock_quantity == "7" product.quantity_price_manual = 0 assert _to_out(product).sale_price == "22.1234" def test_产品提取冲突不覆盖已有档案(payroll_case): from sqlalchemy import select from app.models import BusinessItem, Product from app.services.product_completion import extraction_plan session, _, _ = payroll_case business = imported(payroll_case) item = session.scalar(select(BusinessItem).where(BusinessItem.business_id == business.id)) target = session.get(Product, item.product_id) item.product_id = None target.unit = "不同单位" session.flush() plan = extraction_plan(session, [item.id]) assert plan["rows"][0]["action"] == "CONFLICT" assert target.unit == "不同单位" def test_历史工资标准及离职按目标日期解析(work_session): from types import SimpleNamespace from app.services import personnel_salary person = SimpleNamespace( salary_standard=Decimal("9000"), business_role_id=1, hire_date=date(2026, 1, 1), resignation_date=date(2026, 10, 1), probation_start_date=date(2026, 1, 1), regularization_date=date(2026, 4, 1), is_probation=0, status=0, salary_history_json=[ { "effective_date": "2026-01-01", "salary_standard": "3000", "business_role_id": "1", "hire_date": "2026-01-01", "resignation_date": "2026-10-01", "probation_start_date": "2026-01-01", "regularization_date": "2026-04-01", "is_probation": "1", } ], ) profile = personnel_salary.resolve(person, date(2026, 3, 31)) assert profile.salary_standard == Decimal("3000") assert personnel_salary.employed(person, profile, date(2026, 3, 31)) assert not personnel_salary.employed(person, profile, date(2026, 10, 31)) def test_驳回的后续结清不阻塞早月且待审占用分列(payroll_case): from sqlalchemy import select from app.models import AdjustmentOrder, PerformanceLedger, ProbationPayment from app.services import payroll_calculation, probation_service, salary_completion session, user, _ = payroll_case business = imported(payroll_case) payroll_calculation.calculate(session, business.id, "2026-03", operator=user) ledger = session.scalar(select(PerformanceLedger)) ledger.is_probation = 1 ledger.performance_amount = Decimal("20000") ledger.payable_amount = Decimal("3000") order = AdjustmentOrder(order_no="TZ-test-probation", personnel_id=ledger.personnel_id, personnel_name=ledger.personnel_name, settlement_month="2027-02", amount=Decimal("1000"), direction=1, reason="测试驳回释放", status=3, submitter_id=user.id, submitter_name=user.username) session.add(order) session.flush() session.add(ProbationPayment(adjustment_order_id=order.id, personnel_id=ledger.personnel_id, year="2026", month="2027-02", amount=Decimal("1000"), snapshot_json={})) session.flush() items, _ = salary_completion.build_items(session, [], "2027-01") assert next(i for i in items if i.personnel_id == ledger.personnel_id).probation_payment_sum == Decimal("2000") order.status = 1 session.flush() data = probation_service.preview(session, "2027-02")["rows"][0] assert Decimal(data["paid"]) == 0 assert Decimal(data["reserved"]) == Decimal("1000") assert Decimal(data["delta"]) == Decimal("1000") with pytest.raises(BadRequestError, match="后续月份"): salary_completion.build_items(session, [], "2027-01")