224 lines
7.6 KiB
Python
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")
|