150 lines
5.9 KiB
Python
150 lines
5.9 KiB
Python
"""P0 #5 信用额度落地集成测试。
|
||
|
||
覆盖:限额内通过、超限拒绝、force 放行+预警通知、接近 90% 预警、无限额不校验。
|
||
"""
|
||
|
||
import pytest
|
||
from datetime import date
|
||
from decimal import Decimal
|
||
from model_bakery import baker
|
||
|
||
from apps.catalog.models import Product
|
||
from apps.inventory.models import Warehouse
|
||
from apps.partner.models import Customer
|
||
from apps.finance.models import Receivable
|
||
from apps.notify.models import Notification
|
||
from apps.sales import services as sales_services
|
||
from apps.sales.services import CreditLimitExceeded
|
||
|
||
|
||
@pytest.fixture
|
||
def warehouse(db, tenant):
|
||
return baker.make(Warehouse, tenant=tenant, code="WH01", name="主仓")
|
||
|
||
|
||
@pytest.fixture
|
||
def product(db, tenant):
|
||
return baker.make(Product, tenant=tenant, code="P001", name="商品A",
|
||
sale_price=Decimal("100"))
|
||
|
||
|
||
@pytest.fixture
|
||
def customer(db, tenant):
|
||
return baker.make(Customer, tenant=tenant, code="C001", name="受限客户",
|
||
credit_limit=Decimal("1000"))
|
||
|
||
|
||
def _stock_in(tenant, warehouse, product, qty):
|
||
from apps.inventory import services as inv_services
|
||
inv_services.inbound(
|
||
tenant=tenant, warehouse=warehouse, product=product,
|
||
quantity=Decimal(str(qty)), unit_cost=Decimal("50"),
|
||
)
|
||
|
||
|
||
def _make_receivable(tenant, customer, amount, paid=0):
|
||
return baker.make(
|
||
Receivable, tenant=tenant, customer=customer, bill_no="RC-OLD",
|
||
bill_date=date.today(), total_amount=Decimal(str(amount)),
|
||
paid_amount=Decimal(str(paid)), status="partial" if paid else "open",
|
||
)
|
||
|
||
|
||
def test_confirm_within_limit_passes(tenant, warehouse, customer, product):
|
||
_make_receivable(tenant, customer, 400)
|
||
_stock_in(tenant, warehouse, product, 10)
|
||
bill = sales_services.create_sales_bill(
|
||
tenant=tenant, customer=customer, warehouse=warehouse,
|
||
lines=[{"product": product, "quantity": 5, "unit_price": 100}], # 500
|
||
)
|
||
sales_services.confirm_sales_bill(bill)
|
||
assert bill.state == "confirmed"
|
||
|
||
|
||
def test_confirm_over_limit_rejects_and_rolls_back(tenant, warehouse, customer, product):
|
||
_make_receivable(tenant, customer, 700)
|
||
_stock_in(tenant, warehouse, product, 10)
|
||
bill = sales_services.create_sales_bill(
|
||
tenant=tenant, customer=customer, warehouse=warehouse,
|
||
lines=[{"product": product, "quantity": 5, "unit_price": 100}], # 500 → 1200 > 1000
|
||
)
|
||
from apps.inventory.models import Stock
|
||
with pytest.raises(CreditLimitExceeded):
|
||
sales_services.confirm_sales_bill(bill)
|
||
# 事务回滚:库存未扣、应收未生成、状态未变
|
||
assert bill.state == "draft"
|
||
assert Stock.objects.get(product=product).on_hand == Decimal("10")
|
||
assert not Receivable.objects.filter(source_ref=bill.bill_no).exists()
|
||
|
||
|
||
def test_confirm_over_limit_force_passes_with_alert(tenant, warehouse, customer, product):
|
||
_make_receivable(tenant, customer, 700)
|
||
_stock_in(tenant, warehouse, product, 10)
|
||
bill = sales_services.create_sales_bill(
|
||
tenant=tenant, customer=customer, warehouse=warehouse,
|
||
lines=[{"product": product, "quantity": 5, "unit_price": 100}],
|
||
)
|
||
sales_services.confirm_sales_bill(bill, force=True)
|
||
assert bill.state == "confirmed"
|
||
assert Notification.objects.filter(
|
||
tenant=tenant, extra_data__forced=True
|
||
).count() == 1
|
||
|
||
|
||
def test_confirm_near_limit_warns(tenant, warehouse, customer, product):
|
||
"""占用达 90% 发预警但不阻断:400 旧账 + 500 新单 = 900 = 90%。"""
|
||
_make_receivable(tenant, customer, 400)
|
||
_stock_in(tenant, warehouse, product, 10)
|
||
bill = sales_services.create_sales_bill(
|
||
tenant=tenant, customer=customer, warehouse=warehouse,
|
||
lines=[{"product": product, "quantity": 5, "unit_price": 100}],
|
||
)
|
||
sales_services.confirm_sales_bill(bill)
|
||
assert bill.state == "confirmed"
|
||
assert Notification.objects.filter(
|
||
tenant=tenant, title__contains="信用额度即将用尽"
|
||
).count() == 1
|
||
|
||
|
||
def test_no_limit_no_check(tenant, warehouse, product):
|
||
"""credit_limit=0 表示不启用管控。"""
|
||
c = baker.make(Customer, tenant=tenant, code="C002", name="不限客户",
|
||
credit_limit=Decimal("0"))
|
||
_stock_in(tenant, warehouse, product, 10)
|
||
bill = sales_services.create_sales_bill(
|
||
tenant=tenant, customer=c, warehouse=warehouse,
|
||
lines=[{"product": product, "quantity": 10, "unit_price": 100}], # 1000 无额度
|
||
)
|
||
sales_services.confirm_sales_bill(bill)
|
||
assert bill.state == "confirmed"
|
||
|
||
|
||
def test_rounded_bill_counts_net_amount(tenant, warehouse, customer, product):
|
||
"""抹零后按净额计入占用:旧账 44 + 行合计 1000.56 抹零后 1000 = 1044 ≤ 1000?不,
|
||
这里验证"净额而非毛额"计入:旧账 600 + 1000.56→1000 = 1600 > 1000 必须拒绝;
|
||
若按毛额计入 600+1000.56 更超。改用不超限场景:旧账 0 + 999 抹零… 直接验证
|
||
旧账 44 + 1000.56 抹成 1000 = 1044 超限 → 用 force 之外,改为验证边界:
|
||
旧账 0.56,本单 1000.56 抹到 1000,合计 1000.56 → 超 1000 拒绝;
|
||
再把旧账清 0.44(收 0.12)后合计 1000.00 恰好通过。
|
||
"""
|
||
from decimal import Decimal as D
|
||
recv = _make_receivable(tenant, customer, 1)
|
||
_stock_in(tenant, warehouse, product, 10)
|
||
bill = sales_services.create_sales_bill(
|
||
tenant=tenant, customer=customer, warehouse=warehouse,
|
||
lines=[{"product": product, "quantity": 10, "unit_price": 100.05}],
|
||
round_to="1",
|
||
)
|
||
# 10×100.05 = 1000.5 → 抹到 1000
|
||
assert bill.total_amount == D("1000")
|
||
assert bill.round_off == D("0.5")
|
||
# outstanding = 1 + 1000 = 1001 > 1000 → 拒绝
|
||
with pytest.raises(CreditLimitExceeded):
|
||
sales_services.confirm_sales_bill(bill)
|
||
# 客户还掉 1 元旧账 → outstanding 1000 = 限额,恰好通过
|
||
recv.paid_amount = D("1")
|
||
recv.status = "paid"
|
||
recv.save()
|
||
sales_services.confirm_sales_bill(bill)
|
||
assert bill.state == "confirmed"
|