| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- """再编辑并重新提审后,历史审批记录不得阻塞当前轮审批。"""
- 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, "delete_products_by_contract_id"), 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
|