"""finance 集成测试。""" import pytest from datetime import date from decimal import Decimal from model_bakery import baker from apps.partner.models import Customer, Supplier from apps.finance import services from apps.finance.models import Receivable, Payable, Allocation # --- services 单元测试 --- @pytest.fixture def customer(tenant): return baker.make(Customer, tenant=tenant, code="C001", name="客户A") @pytest.fixture def supplier(tenant): return baker.make(Supplier, tenant=tenant, code="S001", name="供应商A") @pytest.mark.django_db def test_create_receivable_from_sale(tenant, customer): recv = services.create_receivable_from_sale( tenant=tenant, customer=customer, total_amount=Decimal("1000.00"), source_ref="SO-001", ) assert recv.bill_no.startswith("RC") assert recv.customer == customer assert recv.total_amount == Decimal("1000.00") assert recv.paid_amount == Decimal("0") assert recv.status == "open" assert recv.source_type == "sale" @pytest.mark.django_db def test_create_payable_from_purchase(tenant, supplier): pay = services.create_payable_from_purchase( tenant=tenant, supplier=supplier, total_amount=Decimal("2000.00"), source_ref="PO-001", ) assert pay.bill_no.startswith("PY") assert pay.total_amount == Decimal("2000.00") assert pay.status == "open" @pytest.mark.django_db def test_allocate_receipt_partial(tenant, customer): recv = services.create_receivable_from_sale( tenant=tenant, customer=customer, total_amount=Decimal("1000.00"), source_ref="SO-001", ) receipt = baker.make( __import__("apps.finance.models", fromlist=["Receipt"]).Receipt, tenant=tenant, customer=customer, bill_no="RCT-001", bill_date=date.today(), amount=Decimal("400.00"), ) services.allocate_receipt(receipt, [(recv, Decimal("400.00"))]) recv.refresh_from_db() receipt.refresh_from_db() assert recv.paid_amount == Decimal("400") assert recv.status == "partial" assert receipt.status == "posted" assert Allocation.objects.filter(receivable=recv).count() == 1 @pytest.mark.django_db def test_allocate_receipt_full_paid(tenant, customer): recv = services.create_receivable_from_sale( tenant=tenant, customer=customer, total_amount=Decimal("1000.00"), ) receipt = baker.make( __import__("apps.finance.models", fromlist=["Receipt"]).Receipt, tenant=tenant, customer=customer, bill_no="RCT-002", bill_date=date.today(), amount=Decimal("1000.00"), ) services.allocate_receipt(receipt, [(recv, Decimal("1000.00"))]) recv.refresh_from_db() assert recv.status == "paid" @pytest.mark.django_db def test_allocate_payment_partial(tenant, supplier): pay = services.create_payable_from_purchase( tenant=tenant, supplier=supplier, total_amount=Decimal("2000.00"), ) payment = baker.make( __import__("apps.finance.models", fromlist=["Payment"]).Payment, tenant=tenant, supplier=supplier, bill_no="PMT-001", bill_date=date.today(), amount=Decimal("500.00"), ) services.allocate_payment(payment, [(pay, Decimal("500.00"))]) pay.refresh_from_db() assert pay.paid_amount == Decimal("500") assert pay.status == "partial" @pytest.mark.django_db def test_allocate_overpay_raises(tenant, customer): recv = services.create_receivable_from_sale( tenant=tenant, customer=customer, total_amount=Decimal("1000.00"), ) receipt = baker.make( __import__("apps.finance.models", fromlist=["Receipt"]).Receipt, tenant=tenant, customer=customer, bill_no="RCT-003", bill_date=date.today(), amount=Decimal("2000.00"), ) with pytest.raises(ValueError): services.allocate_receipt(receipt, [(recv, Decimal("2000.00"))]) @pytest.mark.django_db def test_allocate_amount_mismatch_raises(tenant, customer): recv = services.create_receivable_from_sale( tenant=tenant, customer=customer, total_amount=Decimal("1000.00"), ) receipt = baker.make( __import__("apps.finance.models", fromlist=["Receipt"]).Receipt, tenant=tenant, customer=customer, bill_no="RCT-004", bill_date=date.today(), amount=Decimal("300.00"), ) with pytest.raises(ValueError): services.allocate_receipt(receipt, [(recv, Decimal("200.00"))]) @pytest.mark.django_db def test_allocate_receipt_rejects_cross_tenant_and_wrong_customer( tenant, customer, other_tenant, ): from apps.finance.models import Receipt receipt = baker.make( Receipt, tenant=tenant, customer=customer, bill_no="RCT-CONSISTENCY-001", bill_date=date.today(), amount=Decimal("100.00"), ) foreign_customer = baker.make(Customer, tenant=other_tenant, code="C-OTHER") foreign_recv = baker.make( Receivable, tenant=other_tenant, customer=foreign_customer, bill_no="RC-CONSISTENCY-001", bill_date=date.today(), total_amount=Decimal("100.00"), ) with pytest.raises(ValueError, match="同一租户"): services.allocate_receipt(receipt, [(foreign_recv, Decimal("100.00"))]) other_customer = baker.make(Customer, tenant=tenant, code="C002") wrong_party_recv = baker.make( Receivable, tenant=tenant, customer=other_customer, bill_no="RC-CONSISTENCY-002", bill_date=date.today(), total_amount=Decimal("100.00"), ) with pytest.raises(ValueError, match="客户.*不一致"): services.allocate_receipt(receipt, [(wrong_party_recv, Decimal("100.00"))]) @pytest.mark.django_db def test_allocate_payment_rejects_cross_tenant_and_wrong_supplier( tenant, supplier, other_tenant, ): from apps.finance.models import Payment payment = baker.make( Payment, tenant=tenant, supplier=supplier, bill_no="PMT-CONSISTENCY-001", bill_date=date.today(), amount=Decimal("100.00"), ) foreign_supplier = baker.make(Supplier, tenant=other_tenant, code="S-OTHER") foreign_payable = baker.make( Payable, tenant=other_tenant, supplier=foreign_supplier, bill_no="PY-CONSISTENCY-001", bill_date=date.today(), total_amount=Decimal("100.00"), ) with pytest.raises(ValueError, match="同一租户"): services.allocate_payment(payment, [(foreign_payable, Decimal("100.00"))]) other_supplier = baker.make(Supplier, tenant=tenant, code="S002") wrong_party_payable = baker.make( Payable, tenant=tenant, supplier=other_supplier, bill_no="PY-CONSISTENCY-002", bill_date=date.today(), total_amount=Decimal("100.00"), ) with pytest.raises(ValueError, match="供应商.*不一致"): services.allocate_payment(payment, [(wrong_party_payable, Decimal("100.00"))]) @pytest.mark.django_db def test_allocate_split_across_multiple_bills(tenant, customer): recv1 = services.create_receivable_from_sale( tenant=tenant, customer=customer, total_amount=Decimal("600.00"), ) recv2 = services.create_receivable_from_sale( tenant=tenant, customer=customer, total_amount=Decimal("400.00"), ) receipt = baker.make( __import__("apps.finance.models", fromlist=["Receipt"]).Receipt, tenant=tenant, customer=customer, bill_no="RCT-005", bill_date=date.today(), amount=Decimal("800.00"), ) services.allocate_receipt(receipt, [ (recv1, Decimal("500.00")), (recv2, Decimal("300.00")), ]) recv1.refresh_from_db() recv2.refresh_from_db() assert recv1.paid_amount == Decimal("500") assert recv2.paid_amount == Decimal("300")