test_crm_approval_reedit.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """再编辑并重新提审后,历史审批记录不得阻塞当前轮审批。"""
  2. from unittest.mock import MagicMock, patch
  3. from app.crud.crm.quote_approval import can_user_approve as quote_can_user_approve
  4. from app.crud.crm.quote_approval import invalidate_all_approvals as quote_invalidate_all
  5. from app.crud.crm.contract_approval import can_user_approve as contract_can_user_approve
  6. from app.crud.crm.contract_approval import invalidate_all_approvals as contract_invalidate_all
  7. def _approval(*, approver_id: str, result: str, step: int, is_current: int = 1, create_time=None):
  8. from datetime import datetime
  9. row = MagicMock()
  10. row.approver_id = approver_id
  11. row.approval_result = result
  12. row.step = step
  13. row.is_current = is_current
  14. row.del_flag = 0
  15. # 历史记录默认更早;pending 默认较新,便于按「当前轮」划分
  16. if create_time is not None:
  17. row.create_time = create_time
  18. elif result == "pending":
  19. row.create_time = datetime(2026, 8, 10, 12, 0, 0)
  20. else:
  21. row.create_time = datetime(2026, 8, 1, 12, 0, 0)
  22. return row
  23. def test_quote_can_approve_prefers_current_pending_over_historical_approved():
  24. """再提审后同审批人同时存在历史已通过 + 新 pending 时,应允许审批。"""
  25. old_approved = _approval(approver_id="u1", result="approved", step=1)
  26. new_pending = _approval(approver_id="u1", result="pending", step=1)
  27. db = MagicMock()
  28. db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [
  29. old_approved,
  30. new_pending,
  31. ]
  32. assert quote_can_user_approve(db, "quote-1", "u1") is True
  33. def test_quote_can_approve_false_when_only_historical_approved():
  34. old_approved = _approval(approver_id="u1", result="approved", step=1)
  35. db = MagicMock()
  36. db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [
  37. old_approved
  38. ]
  39. assert quote_can_user_approve(db, "quote-1", "u1") is False
  40. def test_contract_can_approve_prefers_current_pending_over_historical_approved():
  41. old_approved = _approval(approver_id="u1", result="approved", step=1, is_current=0)
  42. new_pending = _approval(approver_id="u1", result="pending", step=1, is_current=1)
  43. db = MagicMock()
  44. db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [
  45. old_approved,
  46. new_pending,
  47. ]
  48. assert contract_can_user_approve(db, "contract-1", "u1") is True
  49. def test_quote_invalidate_all_soft_deletes_approved_and_pending():
  50. approved = _approval(approver_id="u1", result="approved", step=1)
  51. pending = _approval(approver_id="u2", result="pending", step=2)
  52. db = MagicMock()
  53. db.query.return_value.filter.return_value.all.return_value = [approved, pending]
  54. count = quote_invalidate_all(db, "quote-1")
  55. assert count == 2
  56. assert approved.del_flag == 1
  57. assert pending.del_flag == 1
  58. db.flush.assert_called_once()
  59. def test_contract_invalidate_all_soft_deletes_approved_and_pending():
  60. approved = _approval(approver_id="u1", result="approved", step=1, is_current=0)
  61. pending = _approval(approver_id="u2", result="pending", step=2, is_current=1)
  62. db = MagicMock()
  63. db.query.return_value.filter.return_value.all.return_value = [approved, pending]
  64. count = contract_invalidate_all(db, "contract-1")
  65. assert count == 2
  66. assert approved.del_flag == 1
  67. assert pending.del_flag == 1
  68. db.flush.assert_called_once()
  69. def test_quote_update_invalidates_all_approvals_on_reedit():
  70. """编辑保存回草稿时须作废全部历史审批(含已通过),不能只删 pending。"""
  71. from app.crud.crm import quote as quote_crud
  72. old = MagicMock()
  73. old.status = "accepted"
  74. old.quote_type = "sales"
  75. old.customer_id = "c1"
  76. old.supplier_id = "s1"
  77. old.quote_date = None
  78. old.validity_days = None
  79. old.quote_no = "Q1"
  80. old.quote_title = "t"
  81. old.customer_name = "客户"
  82. old.id = "quote-1"
  83. with patch.object(quote_crud, "_get_quote_in_scope", return_value=old), patch.object(
  84. quote_crud, "invalidate_all_approvals"
  85. ) as inv, patch.object(quote_crud, "assert_products_allowed_for_supplier"), patch.object(
  86. quote_crud, "calculate_quote_amount"
  87. ), patch.object(quote_crud, "list_quote_products", return_value=[]), patch.object(
  88. quote_crud, "_enrich_contact_fields"
  89. ), patch.object(quote_crud, "_enrich_supplier_name"), patch.object(
  90. quote_crud, "get_quote_detail", return_value={"id": "quote-1"}
  91. ), patch.object(quote_crud, "delete_pending_approvals") as del_pending:
  92. db = MagicMock()
  93. quote_crud.update_quote(
  94. db,
  95. "quote-1",
  96. tenant_no="T1",
  97. data={"quoteTitle": "new"},
  98. user_id="u1",
  99. )
  100. inv.assert_called_once_with(db, "quote-1")
  101. del_pending.assert_not_called()
  102. assert old.status == "draft"
  103. def test_contract_update_invalidates_all_approvals_on_reedit():
  104. from app.crud.crm import contract as contract_crud
  105. old = MagicMock()
  106. old.status = "approved"
  107. old.contract_type = "sale"
  108. old.supplier_id = "s1"
  109. old.customer_id = "c1"
  110. old.customer_name = "客户"
  111. old.party_a = "甲方"
  112. old.party_b = "乙方"
  113. old.contract_name = "旧合同名"
  114. old.sign_date = None
  115. old.contract_no = "C1"
  116. old.id = "contract-1"
  117. with patch.object(contract_crud, "_get_contract_row_in_scope", return_value=old), patch.object(
  118. contract_crud, "invalidate_all_approvals"
  119. ) as inv, patch.object(contract_crud, "_validate_contract_type_and_products"), patch.object(
  120. contract_crud, "_normalize_framework_contract_data"
  121. ), patch.object(contract_crud, "_validate_product_prices_by_contract_type"), patch.object(
  122. contract_crud, "_validate_sale_purchase_contract_amount"
  123. ), patch.object(contract_crud, "_apply_contract_entity_fields"), patch.object(
  124. contract_crud, "assert_products_allowed_for_supplier"
  125. ), patch.object(contract_crud, "list_contract_products", return_value=[]), patch.object(
  126. contract_crud, "align_contract_no_to_sign_date", return_value="C1"
  127. ), patch.object(
  128. contract_crud, "_build_contract_name_from_parties", return_value="新合同名"
  129. ), patch.object(contract_crud, "get_contract_detail", return_value={"id": "contract-1"}), patch.object(
  130. contract_crud, "delete_pending_approvals"
  131. ) as del_pending, patch.object(contract_crud, "delete_products_by_contract_id"), patch.object(
  132. contract_crud, "batch_insert_contract_products"
  133. ):
  134. db = MagicMock()
  135. contract_crud.update_contract(
  136. db,
  137. "contract-1",
  138. tenant_no="T1",
  139. data={"contractName": "new"},
  140. user_id="u1",
  141. )
  142. inv.assert_called_once_with(db, "contract-1")
  143. del_pending.assert_not_called()
  144. assert old.status == "draft"
  145. assert old.approval_status is None