PayrollManagementSystem/backend/tests/unit/test_requirement_completion.py

321 lines
12 KiB
Python
Raw Normal View History

"""需求补齐的业务金额回归,覆盖配置实际消费而非页面存在性。"""
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")