"""隔离 MySQL 验证需求补齐的页面接口、版本和工资原子提交。""" from datetime import date, timedelta from decimal import Decimal from io import BytesIO import pytest from openpyxl import load_workbook from sqlalchemy import func, select from app.core.config import get_settings from app.core.db import get_session_factory from app.models import ParamChangeBatch, ProbationPayment from tests.integration.test_formal_payroll import formal_chain as formal_chain from tests.integration.test_formal_payroll import ( unconfirmed_formal_chain as unconfirmed_formal_chain, ) pytestmark = pytest.mark.integration def test_系数表未来版本与工作簿往返(client, admin): headers = admin["headers"] base = "/api/v1/system/coefficients" data = client.get(base, headers=headers).json()["data"] assert len(data["rows"]) == 10 rows = [ { k: row.get(k) for k in ("product_category", "customer_type", "stage", "value", "slope", "boundary") } for row in data["rows"] if not row.get("readonly") ] rows[-1]["value"] = "5" payload = { "effective_date": str(date.today() + timedelta(days=2)), "base_date": str(date.today()), "fingerprint": data["fingerprint"], "rows": rows, } response = client.put(base, headers=headers, json=payload) assert response.status_code == 200, response.text assert client.put(base, headers=headers, json=payload).status_code == 400 future = client.get(base, headers=headers, params={"as_of": payload["effective_date"]}).json()[ "data" ] assert future["rows"][-2]["value"] == "5" export = client.get(base + "/export", headers=headers) assert export.status_code == 200 preview = client.post( base + "/import-preview", headers=headers, files={ "file": ( "coefficients.xlsx", export.content, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ) }, ) assert preview.status_code == 200, preview.text assert preview.json()["data"]["can_apply"] book = load_workbook(BytesIO(export.content)) book["系数参数"].cell(2, 4, "bad-value") output = BytesIO() book.save(output) with get_session_factory()() as session: before = session.scalar(select(func.count()).select_from(ParamChangeBatch)) bad = client.post( base + "/import-preview", headers=headers, files={"file": ("bad.xlsx", output.getvalue())} ) assert bad.status_code == 200, bad.text assert not bad.json()["data"]["can_apply"] with get_session_factory()() as session: assert session.scalar(select(func.count()).select_from(ParamChangeBatch)) == before def test_业务提取产品及重复预览不重复建档(client, admin, formal_chain): headers = admin["headers"] business, _, _, _, _ = formal_chain detail = client.get(f"/api/v1/business/businesses/{business['id']}", headers=headers).json()[ "data" ] ids = [row["id"] for row in detail["items"]] base = "/api/v1/base/products/extract" plan = client.post(base + "/preview", headers=headers, json={"item_ids": ids}) assert plan.status_code == 200, plan.text data = plan.json()["data"] applied = client.post( base, headers=headers, json={"item_ids": ids, "fingerprint": data["fingerprint"]} ) assert applied.status_code == 200, applied.text target = applied.json()["data"]["rows"][0]["target_id"] assert target again = client.post(base + "/preview", headers=headers, json={"item_ids": ids}).json()["data"] assert again["rows"][0]["action"] == "LINK" assert again["rows"][0]["target_id"] == target def test_无绩效试用期工资及冻结分项导出(client, admin, monkeypatch): monkeypatch.setattr(get_settings(), "payroll_allow_legacy_calculation", False) headers = admin["headers"] roles = client.get("/api/v1/system/business-roles", headers=headers).json()["data"] if isinstance(roles, dict): roles = roles.get("list", []) role = next(row for row in roles if row["role_code"] == "salesperson") person = client.post( "/api/v1/base/personnel", headers=headers, json={ "personnel_name": "t_无绩效基本工资", "business_role_id": role["id"], "salary_standard": "3000", "hire_date": "2026-01-01", "probation_start_date": "2026-01-01", "regularization_date": "2027-01-01", "is_probation": 1, }, ) assert person.status_code == 200, person.text generated = client.post( "/api/v1/settlements/generate", headers=headers, json={"month": "2026-11"} ) assert generated.status_code == 200, generated.text item = next( row for row in generated.json()["data"]["items"] if row["personnel_id"] == person.json()["data"]["id"] ) assert Decimal(item["net_amount"]) == Decimal("3000") assert Decimal(item["normal_payable_sum"]) == 0 assert ( client.post( "/api/v1/settlements/generate", headers=headers, json={"month": "2026-11"} ).status_code == 400 ) export = client.get("/api/v1/payroll/salary-export/2026-11", headers=headers) assert export.status_code == 200 assert "工资分项" in load_workbook(BytesIO(export.content)).sheetnames with get_session_factory()() as session: assert session.scalar(select(func.count()).select_from(ProbationPayment)) == 0 def test_只读账号不能维护系数或执行提取(client, normal): headers = normal["headers"] response = client.post("/api/v1/base/products/extract", headers=headers, json={"item_ids": [1]}) assert response.status_code == 403 response = client.put("/api/v1/system/coefficients", headers=headers, json={}) assert response.status_code == 403 def test_已生成工资的年度续付走当月调整且防重(client, admin, formal_chain, monkeypatch): from app.models import PerformanceLedger from app.services import payroll_months monkeypatch.setattr(get_settings(), "payroll_allow_legacy_calculation", False) monkeypatch.setattr(get_settings(), "payroll_approval_enabled", False) monkeypatch.setattr(payroll_months, "current_month", lambda: "2027-01") headers = admin["headers"] business, _, _, owner, _ = formal_chain response = client.post( f"/api/v1/payroll/businesses/{business['id']}/calculate", headers=headers, json={"month": "2026-03"}, ) assert response.status_code == 200, response.text # 隔离合成台账:年度计提20000、年底仅1000符合业务支付条件。 with get_session_factory()() as session: row = session.scalar( select(PerformanceLedger).where(PerformanceLedger.business_id == business["id"]) ) row.is_probation = 1 row.performance_amount = Decimal("20000") row.payable_amount = Decimal("1000") session.commit() december = client.post( "/api/v1/settlements/generate", headers=headers, json={"month": "2026-12"} ) assert december.status_code == 200, december.text first = next( row for row in december.json()["data"]["items"] if row["personnel_id"] == owner["id"] ) assert Decimal(first["probation_payment_sum"]) == Decimal("1000") january = client.post( "/api/v1/settlements/generate", headers=headers, json={"month": "2027-01"} ) assert january.status_code == 200, january.text # 工资生成后确认额外满足支付条件的1000,原年度权益只剩1000。 with get_session_factory()() as session: row = session.scalar( select(PerformanceLedger).where( PerformanceLedger.business_id == business["id"], PerformanceLedger.calc_type == 1 ) ) row.payable_amount = Decimal("3000") session.commit() preview = client.get( "/api/v1/payroll/probation", headers=headers, params={"month": "2027-01", "personnel_id": owner["id"]}, ) assert preview.status_code == 200, preview.text balance = preview.json()["data"]["rows"][0] assert Decimal(balance["delta"]) == Decimal("1000") payload = { "personnel_id": owner["id"], "year": "2026", "fingerprint": balance["fingerprint"], "reason": "隔离样本:原年度后续支付条件满足", } adjustment = client.post("/api/v1/payroll/probation/adjustments", headers=headers, json=payload) assert adjustment.status_code == 200, adjustment.text assert Decimal(adjustment.json()["data"]["amount"]) == Decimal("1000") assert ( client.post( "/api/v1/payroll/probation/adjustments", headers=headers, json=payload ).status_code == 400 ) final = client.get( "/api/v1/payroll/probation", headers=headers, params={"month": "2027-01", "personnel_id": owner["id"]}, ).json()["data"]["rows"][0] assert Decimal(final["paid"]) == Decimal("2000") assert Decimal(final["delta"]) == 0