fix(核算): 对齐抵顶口径并完善界面细节

回款确认时间收敛到秒级;抵顶回款不再自动折算并保留标记。补齐公式模板角色标签与提示、指导成本登记入口及基础布局、分页、设置等响应式细节。同步忽略本地排查产物。
This commit is contained in:
liuwenxin committed 2026-11-06 16:27:45 +08:00
1 parent 35fea3802f
commit b3de35c2ec
53 files changed
+1381 -362

No files matched your search

+12
View File
@@ -393,3 +393,15 @@ uv run pytest tests/integration -q # 仅集成
- 模板定义迁移至 `calc_template_definitions`(V28 种子 5 个内置模板);骨架 `FORMULAS` 受控,核算/预览按骨架分派,岗位模板校验改为数据库定义优先、旧库注册表兜底。
- 更新 `test_param_registry` 模板断言:内置模板参数绑定覆盖骨架角色且参数编码已注册。单元 24 项通过;`test_calc_helpers`、`test_param_snapshot`、`test_formal_payroll` 合计 63 项通过;Ruff 与前端类型/构建通过。
- 岗位相关 MySQL 集成需独立测试库环境(`PAYROLL_TEST_DATABASE`),本轮未执行;新增模板 CRUD 集成用例待补。
## 更正单与备份恢复(2026-11-03)
105 项定点单元、37 项隔离 MySQL 集成通过。新用例覆盖正负成对核对、回款转原业务、来源防重/原子拒绝/同号实体隔离、来源变化拦截、普通抵顶不打折、备份篡改/断链/服务器更换/无更新不归档。独立 Docker 全量+增量重放逐值核对通过;说明及证据见 `docs/更正单与备份恢复交付_2026-11-03.md`。
## 真实样本闭环与历史岗位(2026-11-03)
新增 `tests/unit/test_historical_roles.py`,覆盖按月任职区间、重叠拒绝、跨月转岗、经理本人业务排除、正式台账与月结冻结岗位、客户负责人暂缺、带实际成交参照的产品导入。完整单元 961 项、隔离 MySQL 专项 19 项通过;辽宁兴晟达真实样本完成正式导入至月结、四张报表及导出,30 个不同检查点通过。证据与边界见 `docs/真实样本业务闭环验收_2026-11-03.md`。
定点复验:`python -m pytest tests/unit/test_historical_roles.py -q`。真实样本只证明本组经理本人排除场景,不能据 0 元结果认定人员整月工资为零。
+3 -1
View File
@@ -390,9 +390,11 @@ def tplus_tasks(monkeypatch):
@pytest.fixture
def legacy_calculation(monkeypatch):
"""明确启用旧引擎兼容回归;生产和正式工资测试默认关闭。"""
"""旧引擎回归使用关闭确认开关的口径;确认门槛由独立用例覆盖。"""
from app.core.config import get_settings
from app.services import receipt_confirmation
monkeypatch.setattr(get_settings(), "payroll_allow_legacy_calculation", True)
monkeypatch.setattr(receipt_confirmation, "enabled", lambda _session: False)
@pytest.fixture
+3 -4
View File
@@ -307,11 +307,10 @@ class TestGuards:
resp = _calc(client, admin["headers"], receipt["id"])
assert resp.status_code == 200, resp.text
ledger = resp.json()["data"]["ledger"]
# 成本缺失 → 毛利按 0 → 折算 = 销售额×(0−30%) = **−3000**(保守少算
# 方向:缺成本的行宁少算不多发),missing_cost=1
# 旧引擎缺成本标记保留,零毛利按现行公式特例折算为零。
assert ledger["missing_cost"] == 1
assert float(ledger["converted_profit"]) == -3000.0
assert float(ledger["performance_amount"]) == pytest.approx(-29.33, abs=0.01)
assert float(ledger["converted_profit"]) == 0.0
assert float(ledger["performance_amount"]) == 0.0
def test_负绩效照算不归零(self, client: TestClient, admin: dict) -> None:
items = [
@@ -30,8 +30,9 @@ def post(client, path, headers, data):
@pytest.fixture
def formal_chain(client, admin):
def unconfirmed_formal_chain(client, admin, request):
headers = admin["headers"]
unit_cost = getattr(request, "param", {}).get("unit_cost", "25")
with get_session_factory()() as s:
roles = {r.role_code: r.id for r in s.scalars(select(BusinessRole))}
persons = []
@@ -56,7 +57,7 @@ def formal_chain(client, admin):
"product_category": "municipal_water",
"quantity": "2",
"sale_price": "50",
"guided_price": "25",
"guided_price": unit_cost,
}
business = post(
client,
@@ -103,7 +104,7 @@ def formal_chain(client, admin):
"shipment_item_id": detail["items"][0]["id"],
"personnel_id": owner["id"],
"manager_id": manager["id"],
"unit_cost": "25",
"unit_cost": unit_cost,
}
],
}
@@ -111,6 +112,15 @@ def formal_chain(client, admin):
return business, receipt, setup, owner, manager
@pytest.fixture
def formal_chain(client, admin, unconfirmed_formal_chain):
business, receipt, setup, owner, manager = unconfirmed_formal_chain
confirmed = post(client, f"/api/v1/business/receipts/{receipt['id']}/confirm", admin["headers"], {
"expected_revision": receipt["confirmation_revision"],
})
return business, confirmed, setup, owner, manager
def test_正式核算生成工资锁定及报表一致(client, admin, formal_chain):
business, _, setup, owner, manager = formal_chain
headers = admin["headers"]
@@ -321,3 +331,30 @@ def test_无正式资料时默认拒绝旧核算(client, admin, formal_chain):
f"/api/v1/calc/receipts/{empty_receipt['id']}/calc", headers=admin["headers"]
)
assert response.status_code == 400 and "正式核算" in response.json()["message"]
def test_抵顶标记不阻断正式平账且不自动打八折(client, admin, formal_chain):
from app.models import Receipt
business, cash, setup, _, _ = formal_chain
with get_session_factory()() as session:
receipt = session.get(Receipt, cash["id"])
receipt.is_barter = 1
session.commit()
post(client, f"{BASE}/businesses/{business['id']}/input", admin["headers"],
{**setup, "expected_version": 1})
rejected = client.post(f"{BASE}/businesses/{business['id']}/calculate", headers=admin["headers"],
json={"month": "2026-03"})
assert rejected.status_code == 400 and "人工确认" in rejected.json()["message"]
current = client.get(f"/api/v1/business/receipts/{cash['id']}", headers=admin["headers"])
assert current.status_code == 200, current.text
post(client, f"/api/v1/business/receipts/{cash['id']}/confirm", admin["headers"], {
"expected_revision": current.json()["data"]["confirmation_revision"],
})
result = post(client, f"{BASE}/businesses/{business['id']}/calculate", admin["headers"],
{"month": "2026-03"})
assert result["performance"] == "0.20" and result["payable"] == "0.20"
with get_session_factory()() as session:
assert session.get(Receipt, cash["id"]).is_barter == 1
assert all(session.get(PerformanceLedger, ledger_id).barter_applied == 0
for ledger_id in result["ledger_ids"])
@@ -87,10 +87,11 @@ def _snapshot_value(client: TestClient, admin: dict, param_code: str, *, as_of:
class TestCatalog:
"""参数目录:种子完整性与时点化回溯。"""
def test_目录含27项及产品系数组(self, client: TestClient, admin: dict) -> None:
"""V10 起含工作台预警规则三项(16 计提项 + 3 BOOL 开关 + 3 预警项)。"""
def test_目录含28项及产品系数组(self, client: TestClient, admin: dict) -> None:
"""目录包含回款人工确认开关和既有参数。"""
items = client.get(_PARAMS, headers=admin["headers"]).json()["data"]
assert len(items) == 27
assert len(items) == 28
assert any(item["param_code"] == "receipt.confirmation.enabled" for item in items)
groups = {item["param_group"] for item in items}
assert groups == {
"业务员个人户",
@@ -99,6 +100,7 @@ class TestCatalog:
"企业户",
"信息员",
"支付环节",
"回款确认",
"产品阈值",
"月工资口径",
"审批开关",
@@ -355,7 +357,7 @@ class TestPreview:
"salesperson.rate.floor",
"salesperson.days.boundary",
"product.threshold",
"barter.discount",
# 2026-11-03 口径:抵顶线下核算,预览不再读取 barter.discount(参数保留作回退位)。
"payment.start.ratio",
"probation.annual.threshold",
]
@@ -495,7 +497,7 @@ class TestPermission:
session.commit()
resp = client.get(_PARAMS, headers=headers)
assert resp.status_code == 200
assert len(resp.json()["data"]) == 27
assert len(resp.json()["data"]) == 28
def test_未登录访问目录_403(self, client: TestClient) -> None:
# LoginUser 依赖无令牌即拒(401 语义归登录态失效,此处由依赖抛 403/401
@@ -10,6 +10,8 @@ from app.core.db import get_session_factory
from app.models import OperationLog, PayrollInput, PerformanceLedger, Receipt
from tests.integration.test_formal_payroll import (
post,
)
from tests.integration.test_formal_payroll import (
unconfirmed_formal_chain as unconfirmed_formal_chain,
)
@@ -128,7 +130,7 @@ def test_正式核算零毛利特例但低阈值仍为负(client, admin, unconfi
@pytest.mark.parametrize("role,profit,converted,performance", [
("salesperson", "0", "0", "0"),
("salesperson", "10", "-20", "-0.20"),
("info_officer", "0", "0", "0.10"),
("info_officer", "0", "100", "0.10"),
])
def test_试算零毛利与信息员基数保持独立(client, admin, role, profit, converted, performance):
data = post(client, "/api/v1/system/params/preview", admin["headers"], {
@@ -12,6 +12,9 @@ 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
+26 -5
View File
@@ -14,6 +14,7 @@
注:本文件不需要容器,放在 integration 下是因为它校验的是交付产物而非应用代码。
"""
import ast
import re
from pathlib import Path
@@ -24,7 +25,7 @@ MIGRATION_DIR = PROJECT_ROOT / "db" / "migration"
INIT_DEV_FILE = PROJECT_ROOT / "db" / "init" / "init-dev.sql"
ALEMBIC_VERSIONS_DIR = PROJECT_ROOT / "backend" / "alembic" / "versions"
_CREATE_TABLE = re.compile(r"CREATE TABLE\s+`?(\w+)`?\s*\((.*?)\n\)", re.DOTALL)
_CREATE_TABLE = re.compile(r"CREATE TABLE\s+(?:IF NOT EXISTS\s+)?`?(\w+)`?\s*\((.*?)\n\)", re.DOTALL | re.IGNORECASE)
_COLUMN_LINE = re.compile(r"^[ \t]+`?(\w+)`?[ \t]", re.MULTILINE)
# 这些开头的是约束定义,不是列定义
_NON_COLUMN_KEYWORDS = {"CONSTRAINT", "PRIMARY", "UNIQUE", "FOREIGN", "KEY", "INDEX"}
@@ -32,6 +33,7 @@ _NON_COLUMN_KEYWORDS = {"CONSTRAINT", "PRIMARY", "UNIQUE", "FOREIGN", "KEY", "IN
# MODIFY 只改类型不改列名,对列集合无影响,无需处理
_ALTER_ADD = re.compile(r"ALTER TABLE\s+`?(\w+)`?\s+ADD(?:\s+COLUMN)?\s+`?(\w+)`?", re.IGNORECASE)
_ALTER_DROP = re.compile(r"ALTER TABLE\s+`?(\w+)`?\s+DROP(?:\s+COLUMN)?\s+`?(\w+)`?", re.IGNORECASE)
_RENAME_TABLE = re.compile(r"RENAME TABLE\s+`?(\w+)`?\s+TO\s+`?(\w+)`?", re.IGNORECASE)
def _parse_tables(sql: str) -> dict[str, set[str]]:
@@ -67,6 +69,8 @@ def _migration_final_state(paths: list[Path]) -> dict[str, set[str]]:
merged.setdefault(table, set()).add(column)
for table, column in _ALTER_DROP.findall(sql):
merged.setdefault(table, set()).discard(column)
for old, new in _RENAME_TABLE.findall(sql):
merged[new] = merged.pop(old)
return merged
@@ -83,8 +87,15 @@ def _alembic_head() -> str:
text = path.read_text(encoding="utf-8")
if found := re.search(r"^revision(?:\s*:\s*str)?\s*=\s*['\"](\w+)['\"]", text, re.MULTILINE):
revisions.add(found.group(1))
if found := re.search(r"^down_revision(?:\s*:.*?)?\s*=\s*['\"](\w+)['\"]", text, re.MULTILINE):
referenced.add(found.group(1))
for node in ast.parse(text).body:
if isinstance(node, (ast.Assign, ast.AnnAssign)):
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
if any(isinstance(target, ast.Name) and target.id == "down_revision" for target in targets):
value = ast.literal_eval(node.value)
if isinstance(value, str):
referenced.add(value)
elif isinstance(value, tuple):
referenced.update(value)
heads = revisions - referenced
assert len(heads) == 1, f"迁移链存在分叉或缺失,heads={sorted(heads)}"
@@ -94,14 +105,14 @@ def _alembic_head() -> str:
@pytest.fixture(scope="module")
def migration_tables() -> dict[str, set[str]]:
"""按文件序重放 db/migration/ 下全部 V*.sql,取迁移链终态。"""
files = sorted(MIGRATION_DIR.glob("V*.sql"))
files = sorted(MIGRATION_DIR.glob("V*.sql"), key=lambda path: int(re.match(r"V(\d+)", path.name).group(1)))
assert files, f"未找到迁移脚本:{MIGRATION_DIR}"
return _migration_final_state(files)
@pytest.fixture(scope="module")
def init_dev_tables() -> dict[str, set[str]]:
return _parse_tables(INIT_DEV_FILE.read_text(encoding="utf-8"))
return _migration_final_state([INIT_DEV_FILE])
class TestSchemaSync:
@@ -111,6 +122,16 @@ class TestSchemaSync:
assert list(MIGRATION_DIR.glob("V*.sql")), f"缺少迁移脚本:{MIGRATION_DIR}"
assert INIT_DEV_FILE.is_file(), f"缺少开发初始化脚本:{INIT_DEV_FILE}"
def test_迁移解析支持建表后增加列与重命名(self, tmp_path: Path) -> None:
migration = tmp_path / "V1__example.sql"
migration.write_text(
"CREATE TABLE old_name (\n id BIGINT\n);\n"
"ALTER TABLE old_name ADD extra VARCHAR(8);\n"
"RENAME TABLE old_name TO new_name;\n",
encoding="utf-8",
)
assert _migration_final_state([migration]) == {"new_name": {"id", "extra"}}
def test_表集合一致(self, migration_tables: dict, init_dev_tables: dict) -> None:
assert set(init_dev_tables) == set(migration_tables), (
"两份 DDL 的表集合不一致 —— 表结构变更时需同时维护 "
+91
View File
@@ -32,6 +32,7 @@ from app.services.payroll_access import can_write_payroll
from app.services.tplus.business_import import execute_business
from app.services.tplus.workbench import digest
from tests.unit.test_tplus_workbench import apply, fact, prepare_calc, preview
from tests.unit.test_tplus_workbench import source as publish_source
from tests.unit.test_tplus_workbench import work_session as work_session
@@ -192,6 +193,96 @@ def test_正式导入防重与来源可追溯(payroll_case):
assert record.data_json["source_facts"]
@pytest.fixture
def correction_case(payroll_case, monkeypatch):
from contextlib import nullcontext
from app.models import TPlusSyncRecord
from app.schemas.tplus_workbench import CorrectionPairIn
from app.services.tplus import corrections
from app.services.tplus.snapshots import source_records
s, user, _ = payroll_case
original = source_records(s, "test_source", "shipments")[0].payload_json
original_line = source_records(s, "test_source", "shipment_items")[0].payload_json
# 收款ID与负更正发货ID相同,必须由实体前缀区分。
publish_source(s, "shipments", [original,
{**original, "id": 30, "code": "A-NEG"},
{**original, "id": 40, "code": "B-POS", "idcustomer": 88}])
publish_source(s, "shipment_items", [original_line,
{**original_line, "id": 31, "source_parent_id": 30, "taxAmount": "-100", "quantity": "-2"},
{**original_line, "id": 41, "source_parent_id": 40}])
publish_source(s, "receipts", [{"id": 30, "code": "B-CASH", "idpartner": 88,
"voucherDate": "2026-03-10", "isReceiveFlag": 1, "voucherstate": 189, "amount": "100",
"accounting_voucher_count": 1, "valid_accounting_voucher_count": 1,
"accounting_voucher_id": 123, "accounting_voucher_code": "TEST-GL"}])
monkeypatch.setattr(corrections, "sync_lock", lambda db: nullcontext())
payload = CorrectionPairIn(negative_shipment_id="30", positive_shipment_id="40",
receipt_ids=["30"], target_group_key="g", evidence="隔离转客户例单核对")
before = s.scalar(select(func.count()).select_from(TPlusSyncRecord))
return s, user, corrections, payload, before
def test_更正对贯通正式导入与FIFO_保留日期金额且重复不增加(payroll_case, correction_case):
from app.models import TPlusFact, TPlusSyncRecord
s, user, service, payload, before = correction_case
first = service.save_pair(s, payload, user.id)
again = service.save_pair(s, payload, user.id)
assert first["pair_key"] == again["pair_key"]
assert s.scalar(select(func.count()).select_from(TPlusFact).where(TPlusFact.kind == "CORRECTION")) == 3
business = imported(payroll_case)
assert business.total_sale_amount == Decimal("100")
cash = s.scalar(select(Receipt))
assert cash.source_no == "B-CASH" and cash.receipt_date == date(2026, 3, 10)
assert cash.amount == Decimal("100") and cash.business_id == business.id
assert s.scalar(select(func.count()).select_from(Shipment)) == 1
assert s.scalar(select(func.count()).select_from(TPlusSyncRecord)) == before
result = payroll_calculation.calculate(s, business.id, "2026-03", operator=user)
assert result["performance"] == "0.20" and result["payable"] == "0.20"
repeated = business_work(s, user, "BUSINESS_PREVIEW")
assert repeated.summary_json["counts"] == {"SKIP": 1}
with pytest.raises(BadRequestError):
payroll_calculation.calculate(s, business.id, "2026-03", operator=user)
@pytest.mark.parametrize("change", ["wrong_sign", "wrong_group", "wrong_receipt", "missing_source"])
def test_无效更正对原子拒绝不留下半边事实(correction_case, change):
from app.models import TPlusFact
s, user, service, payload, _ = correction_case
updates = {"wrong_sign": {"negative_shipment_id": "40", "positive_shipment_id": "30"},
"wrong_group": {"target_group_key": "unknown"},
"wrong_receipt": {"receipt_ids": ["999"]},
"missing_source": {"positive_shipment_id": "999"}}
with pytest.raises(BadRequestError):
service.save_pair(s, payload.model_copy(update=updates[change]), user.id)
assert s.scalar(select(func.count()).select_from(TPlusFact).where(TPlusFact.kind == "CORRECTION")) == 0
def test_更正对后来源变化阻止静默排除(correction_case):
from app.services.tplus.snapshots import source_records
from app.services.tplus.workbench import latest_facts
s, user, service, payload, _ = correction_case
service.save_pair(s, payload, user.id)
rows = [r.payload_json for r in source_records(s, "test_source", "shipments")]
publish_source(s, "shipments", [{**r, "code": "CHANGED"} if r["id"] == 30 else r for r in rows])
record = source_records(s, "test_source", "shipments", ids=["30"])[0]
with pytest.raises(ValueError, match="CORRECTION_RECONFIRM_REQUIRED"):
service.verified_correction(s, "test_source", latest_facts(s, "test_source"), "SHIPMENT", record)
def test_抵顶预览显示未自动折算且不读取折扣参数():
from app.services.param_service import _preview_barter
def forbidden(_):
raise AssertionError("线下抵顶不得读取自动折扣参数")
result = _preview_barter(SimpleNamespace(payload=SimpleNamespace(barter=True), take=forbidden), Decimal("100"))
assert result.base_amount == Decimal("100")
assert result.applied is False and result.after_barter is None
def test_正式FIFO台账金额与逐段过程守恒(payroll_case):
s, user, manager = payroll_case
business = imported(payroll_case)
+6
View File
@@ -113,6 +113,12 @@ class TestConvertedProfit:
def test_毛利润额为零_折算为零_不除零(self) -> None:
assert converted_profit(Decimal("0"), Fraction(0), Fraction(30)) == Decimal("0")
def test_有销售但零毛利润_折算及普通绩效均为零(self) -> None:
rate = profit_rate(Decimal("100") - Decimal("100"), Decimal("100"))
base = converted_profit(Decimal("100"), rate, Fraction(30))
assert base == Decimal("0")
assert performance_amount(base, Fraction(1, 100)) == Decimal("0")
def test_毛利润率低于阈值_折算为负_不归零(self) -> None:
# 100 × (1/10 − 3/10) = -20(业务确认负值正常计算)
assert converted_profit(Decimal("100"), Fraction(1, 10), Fraction(30)) == Decimal("-20")
+1
View File
@@ -17,6 +17,7 @@ EXPECTED_TABLES = {
"authorization_settings", "user_permission_overrides",
"tplus_automation_states",
"tplus_automation_configs",
"tplus_runtime_controls",
"payroll_months", "payroll_inputs", "payroll_allocations",
"tplus_source_heads", "tplus_facts", "tplus_source_links", "tplus_work_runs", "tplus_work_lines",
"users",
@@ -207,6 +207,12 @@ class TestExcel模板:
text = _sheet_text(_build("business-stats", rows))
assert "已完成" in text
def test_业务销售额未知时导出明细和合计不显示零(self):
rows = [{"business_no": "YW001", "status": 1, "total_sale_amount": None}]
workbook = load_workbook(io.BytesIO(_build("business-stats", rows)))
values = [cell.value for row in workbook.active.iter_rows() for cell in row]
assert values.count("待核对") >= 2
def _member(
personnel_id: int,
@@ -4,9 +4,9 @@ from unittest.mock import Mock
import pytest
from app.api.v1.business.businesses import confirm_source_price
from app.api.v1.business.businesses import confirm_guidance, confirm_source_price
from app.core.exceptions import ForbiddenError
from app.schemas.business_source import SourcePriceConfirmIn
from app.schemas.business_source import GuidanceConfirmIn, SourcePriceConfirmIn
from app.services import authorization_service, business_source
from app.services.authorization_policy import permission_for
@@ -29,3 +29,46 @@ def test_无核算准备权限不能确认来源单价(monkeypatch):
with pytest.raises(ForbiddenError):
confirm_source_price(session, actor, 1, 1, payload, meta)
confirm.assert_not_called()
def test_指导成本登记必须同时具备核算准备和事实维护权限(monkeypatch):
session, actor, meta = Mock(), Mock(), Mock()
save = Mock(return_value={"status": "CREATED"})
permissions = []
def allow(current_session, current_actor, permission):
assert (current_session, current_actor) == (session, actor)
permissions.append(permission)
monkeypatch.setattr(authorization_service, "require_permission", allow)
monkeypatch.setattr(business_source, "confirm_guidance", save)
payload = GuidanceConfirmIn(shipment_item_id=1, amount="2.1234", evidence="核对月度指导成本")
result = confirm_guidance(session, actor, 1, payload, meta)
assert result.data == {"status": "CREATED"}
assert permissions == ["payroll:prepare", "tplus:facts"]
save.assert_called_once_with(session, 1, payload, actor, meta)
def test_缺事实维护权限不能登记指导成本(monkeypatch):
session, actor, meta = Mock(), Mock(), Mock()
save = Mock()
def deny(current_session, current_actor, permission):
if permission == "tplus:facts":
raise ForbiddenError("无权维护事实")
monkeypatch.setattr(authorization_service, "require_permission", deny)
monkeypatch.setattr(business_source, "confirm_guidance", save)
payload = GuidanceConfirmIn(shipment_item_id=1, amount="2", evidence="核对")
with pytest.raises(ForbiddenError):
confirm_guidance(session, actor, 1, payload, meta)
save.assert_not_called()
@pytest.mark.parametrize("amount", ["abc", "-1", "1.12345", "NaN"])
def test_指导成本入参拒绝非法精度和值(amount):
with pytest.raises(ValueError):
GuidanceConfirmIn(shipment_item_id=1, amount=amount, evidence="核对")
+2 -15
View File
@@ -1,10 +1,7 @@
"""业务接入以源单据为边界;核算资料缺失不得阻断建档。"""
from contextlib import nullcontext
from datetime import date
from decimal import Decimal
from fractions import Fraction
from types import SimpleNamespace
from uuid import NAMESPACE_URL, uuid4, uuid5
import pytest
@@ -17,7 +14,6 @@ from app.models import (
BusinessItem,
BusinessRole,
Customer,
OwnershipChangeLog,
PayrollInput,
Personnel,
Product,
@@ -26,41 +22,32 @@ from app.models import (
ShipmentItem,
TPlusBusinessDocument,
TPlusBusinessSyncConfig,
TPlusSourceHead,
TPlusSyncBatch,
TPlusWorkRun,
User,
)
from app.repositories import customer_repository, product_repository
from app.schemas.business_source import (
SalesRelationConfirmIn,
SalesRelationReleaseIn,
SourceMaterialsIn,
SourcePriceConfirmIn,
)
from app.schemas.category_batch import (
CustomerCategoryBatch,
CustomerOwnershipBatch,
ProductCategoryBatch,
)
from app.schemas.customer import CustomerQuery
from app.schemas.payroll import PayrollSetupIn
from app.schemas.product import ProductQuery
from app.schemas.report import BusinessStatsQuery
from app.services import (
business_service,
business_source,
category_batch,
customer_service,
payroll_months,
receipt_confirmation,
report_service,
)
from app.services.business_source import ensure_source_editable
from app.services.tplus import business_sync, business_sync_control, sales_relations
from app.services.tplus import business_sync, business_sync_control
from app.services.tplus.snapshots import source_manifest
from app.services.tplus.workbench import WorkbenchError, digest, execute_next
from tests.unit.test_tplus_workbench import fact, source
from tests.unit.test_tplus_workbench import source
from tests.unit.test_tplus_workbench import work_session as work_session