baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user