Files
dealerhub/backend/tests/test_finance.py
T

224 lines
7.6 KiB
Python

"""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")