172 lines
6.9 KiB
Python
172 lines
6.9 KiB
Python
"""再编辑并重新提审后,历史审批记录不得阻塞当前轮审批。"""
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from app.crud.crm.quote_approval import can_user_approve as quote_can_user_approve
|
|
from app.crud.crm.quote_approval import invalidate_all_approvals as quote_invalidate_all
|
|
from app.crud.crm.contract_approval import can_user_approve as contract_can_user_approve
|
|
from app.crud.crm.contract_approval import invalidate_all_approvals as contract_invalidate_all
|
|
|
|
|
|
def _approval(*, approver_id: str, result: str, step: int, is_current: int = 1, create_time=None):
|
|
from datetime import datetime
|
|
|
|
row = MagicMock()
|
|
row.approver_id = approver_id
|
|
row.approval_result = result
|
|
row.step = step
|
|
row.is_current = is_current
|
|
row.del_flag = 0
|
|
# 历史记录默认更早;pending 默认较新,便于按「当前轮」划分
|
|
if create_time is not None:
|
|
row.create_time = create_time
|
|
elif result == "pending":
|
|
row.create_time = datetime(2026, 8, 10, 12, 0, 0)
|
|
else:
|
|
row.create_time = datetime(2026, 8, 1, 12, 0, 0)
|
|
return row
|
|
|
|
|
|
def test_quote_can_approve_prefers_current_pending_over_historical_approved():
|
|
"""再提审后同审批人同时存在历史已通过 + 新 pending 时,应允许审批。"""
|
|
old_approved = _approval(approver_id="u1", result="approved", step=1)
|
|
new_pending = _approval(approver_id="u1", result="pending", step=1)
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [
|
|
old_approved,
|
|
new_pending,
|
|
]
|
|
assert quote_can_user_approve(db, "quote-1", "u1") is True
|
|
|
|
|
|
def test_quote_can_approve_false_when_only_historical_approved():
|
|
old_approved = _approval(approver_id="u1", result="approved", step=1)
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [
|
|
old_approved
|
|
]
|
|
assert quote_can_user_approve(db, "quote-1", "u1") is False
|
|
|
|
|
|
def test_contract_can_approve_prefers_current_pending_over_historical_approved():
|
|
old_approved = _approval(approver_id="u1", result="approved", step=1, is_current=0)
|
|
new_pending = _approval(approver_id="u1", result="pending", step=1, is_current=1)
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [
|
|
old_approved,
|
|
new_pending,
|
|
]
|
|
assert contract_can_user_approve(db, "contract-1", "u1") is True
|
|
|
|
|
|
def test_quote_invalidate_all_soft_deletes_approved_and_pending():
|
|
approved = _approval(approver_id="u1", result="approved", step=1)
|
|
pending = _approval(approver_id="u2", result="pending", step=2)
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.all.return_value = [approved, pending]
|
|
|
|
count = quote_invalidate_all(db, "quote-1")
|
|
|
|
assert count == 2
|
|
assert approved.del_flag == 1
|
|
assert pending.del_flag == 1
|
|
db.flush.assert_called_once()
|
|
|
|
|
|
def test_contract_invalidate_all_soft_deletes_approved_and_pending():
|
|
approved = _approval(approver_id="u1", result="approved", step=1, is_current=0)
|
|
pending = _approval(approver_id="u2", result="pending", step=2, is_current=1)
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.all.return_value = [approved, pending]
|
|
|
|
count = contract_invalidate_all(db, "contract-1")
|
|
|
|
assert count == 2
|
|
assert approved.del_flag == 1
|
|
assert pending.del_flag == 1
|
|
db.flush.assert_called_once()
|
|
|
|
|
|
def test_quote_update_invalidates_all_approvals_on_reedit():
|
|
"""编辑保存回草稿时须作废全部历史审批(含已通过),不能只删 pending。"""
|
|
from app.crud.crm import quote as quote_crud
|
|
|
|
old = MagicMock()
|
|
old.status = "accepted"
|
|
old.quote_type = "sales"
|
|
old.customer_id = "c1"
|
|
old.supplier_id = "s1"
|
|
old.quote_date = None
|
|
old.validity_days = None
|
|
old.quote_no = "Q1"
|
|
old.quote_title = "t"
|
|
old.customer_name = "客户"
|
|
old.id = "quote-1"
|
|
|
|
with patch.object(quote_crud, "_get_quote_in_scope", return_value=old), patch.object(
|
|
quote_crud, "invalidate_all_approvals"
|
|
) as inv, patch.object(quote_crud, "assert_products_allowed_for_supplier"), patch.object(
|
|
quote_crud, "calculate_quote_amount"
|
|
), patch.object(quote_crud, "list_quote_products", return_value=[]), patch.object(
|
|
quote_crud, "_enrich_contact_fields"
|
|
), patch.object(quote_crud, "_enrich_supplier_name"), patch.object(
|
|
quote_crud, "get_quote_detail", return_value={"id": "quote-1"}
|
|
), patch.object(quote_crud, "delete_pending_approvals") as del_pending:
|
|
db = MagicMock()
|
|
quote_crud.update_quote(
|
|
db,
|
|
"quote-1",
|
|
tenant_no="T1",
|
|
data={"quoteTitle": "new"},
|
|
user_id="u1",
|
|
)
|
|
inv.assert_called_once_with(db, "quote-1")
|
|
del_pending.assert_not_called()
|
|
assert old.status == "draft"
|
|
|
|
|
|
def test_contract_update_invalidates_all_approvals_on_reedit():
|
|
from app.crud.crm import contract as contract_crud
|
|
|
|
old = MagicMock()
|
|
old.status = "approved"
|
|
old.contract_type = "sale"
|
|
old.supplier_id = "s1"
|
|
old.customer_id = "c1"
|
|
old.customer_name = "客户"
|
|
old.party_a = "甲方"
|
|
old.party_b = "乙方"
|
|
old.contract_name = "旧合同名"
|
|
old.sign_date = None
|
|
old.contract_no = "C1"
|
|
old.id = "contract-1"
|
|
|
|
with patch.object(contract_crud, "_get_contract_row_in_scope", return_value=old), patch.object(
|
|
contract_crud, "invalidate_all_approvals"
|
|
) as inv, patch.object(contract_crud, "_validate_contract_type_and_products"), patch.object(
|
|
contract_crud, "_normalize_framework_contract_data"
|
|
), patch.object(contract_crud, "_validate_product_prices_by_contract_type"), patch.object(
|
|
contract_crud, "_validate_sale_purchase_contract_amount"
|
|
), patch.object(contract_crud, "_apply_contract_entity_fields"), patch.object(
|
|
contract_crud, "assert_products_allowed_for_supplier"
|
|
), patch.object(contract_crud, "list_contract_products", return_value=[]), patch.object(
|
|
contract_crud, "align_contract_no_to_sign_date", return_value="C1"
|
|
), patch.object(
|
|
contract_crud, "_build_contract_name_from_parties", return_value="新合同名"
|
|
), patch.object(contract_crud, "get_contract_detail", return_value={"id": "contract-1"}), patch.object(
|
|
contract_crud, "delete_pending_approvals"
|
|
) as del_pending, patch.object(contract_crud, "sync_contract_products"), patch.object(
|
|
contract_crud, "batch_insert_contract_products"
|
|
):
|
|
db = MagicMock()
|
|
contract_crud.update_contract(
|
|
db,
|
|
"contract-1",
|
|
tenant_no="T1",
|
|
data={"contractName": "new"},
|
|
user_id="u1",
|
|
)
|
|
inv.assert_called_once_with(db, "contract-1")
|
|
del_pending.assert_not_called()
|
|
assert old.status == "draft"
|
|
assert old.approval_status is None
|