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