"""端到端冒烟测试:完整业务链。 链:建商品 → 建仓库 → 建客户/供应商 → 进货单过账(写库存 + 生成应付) → 付款单核销 → 销售单过账(扣库存 + 生成应收)→ 收款单核销 → 最终库存/应付/应收 余额一致 """ import pytest from datetime import date from decimal import Decimal from model_bakery import baker from rest_framework.test import APIClient from rest_framework_simplejwt.tokens import RefreshToken from apps.inventory import services as inv_services from apps.inventory.models import Stock from apps.finance import services as fin_services from apps.finance.models import Receivable, Payable, Receipt, Payment from apps.purchase import services as purchase_services from apps.sales import services as sales_services from apps.purchase.models import PurchaseBill from apps.sales.models import SalesBill @pytest.fixture def auth_client(db, user, tenant): c = APIClient() refresh = RefreshToken.for_user(user) c.credentials( HTTP_AUTHORIZATION=f"Bearer {refresh.access_token}", HTTP_X_TENANT_ID=tenant.code, ) return c @pytest.fixture def warehouse(tenant): from apps.inventory.models import Warehouse return baker.make(Warehouse, tenant=tenant, code="WH01", name="主仓") @pytest.fixture def product(tenant): from apps.catalog.models import Product return baker.make(Product, tenant=tenant, code="P001", name="商品A") @pytest.fixture def supplier(tenant): from apps.partner.models import Supplier return baker.make(Supplier, tenant=tenant, code="S001", name="供应商A") @pytest.fixture def customer(tenant): from apps.partner.models import Customer return baker.make(Customer, tenant=tenant, code="C001", name="客户A") @pytest.mark.django_db def test_full_business_loop(tenant, warehouse, product, supplier, customer): # === 1. 进货:向供应商进 20 件,单价 8 元 === pb = purchase_services.create_purchase_bill( tenant=tenant, supplier=supplier, warehouse=warehouse, lines=[{"product": product, "quantity": Decimal("20"), "unit_price": Decimal("8")}], ) purchase_services.confirm_purchase_bill(pb) # 库存应有 20 件,avg_cost = 8 stock = Stock.objects.get(tenant=tenant, warehouse=warehouse, product=product) assert stock.on_hand == Decimal("20") assert stock.avg_cost == Decimal("8") # 应付单 20*8 = 160 payable = Payable.objects.get(tenant=tenant, source_ref=pb.bill_no) assert payable.total_amount == Decimal("160") assert payable.status == "open" # === 2. 付供应商 100 元(部分付款)=== payment = baker.make( Payment, tenant=tenant, supplier=supplier, bill_no="PMT-001", bill_date=date.today(), amount=Decimal("100"), ) fin_services.allocate_payment(payment, [(payable, Decimal("100"))]) payable.refresh_from_db() payment.refresh_from_db() assert payable.paid_amount == Decimal("100") assert payable.status == "partial" # === 3. 销售:卖给客户 7 件,单价 12 元 === sb = sales_services.create_sales_bill( tenant=tenant, customer=customer, warehouse=warehouse, lines=[{"product": product, "quantity": Decimal("7"), "unit_price": Decimal("12")}], ) sales_services.confirm_sales_bill(sb) # 库存减到 13 stock.refresh_from_db() assert stock.on_hand == Decimal("13") # 应收 7*12 = 84 recv = Receivable.objects.get(tenant=tenant, source_ref=sb.bill_no) assert recv.total_amount == Decimal("84") assert recv.status == "open" # === 4. 收客户 84 元(一次结清)=== receipt = baker.make( Receipt, tenant=tenant, customer=customer, bill_no="RCT-001", bill_date=date.today(), amount=Decimal("84"), ) fin_services.allocate_receipt(receipt, [(recv, Decimal("84"))]) recv.refresh_from_db() receipt.refresh_from_db() assert recv.status == "paid" assert receipt.status == "posted" # === 5. 再付供应商 60 元(结清)=== payment2 = baker.make( Payment, tenant=tenant, supplier=supplier, bill_no="PMT-002", bill_date=date.today(), amount=Decimal("60"), ) fin_services.allocate_payment(payment2, [(payable, Decimal("60"))]) payable.refresh_from_db() assert payable.status == "paid" # === 6. 收尾 === # 最终库存 13 stock.refresh_from_db() assert stock.on_hand == Decimal("13") # 应收已结清 assert Receivable.objects.filter(tenant=tenant, status="open").count() == 0 # 应付已结清 assert Payable.objects.filter(tenant=tenant, status="open").count() == 0 @pytest.mark.django_db def test_two_sales_cannot_overdraw_stock(tenant, warehouse, product, customer): inv_services.inbound( tenant=tenant, warehouse=warehouse, product=product, quantity=Decimal("5"), ) sb1 = sales_services.create_sales_bill( tenant=tenant, customer=customer, warehouse=warehouse, lines=[{"product": product, "quantity": Decimal("4"), "unit_price": Decimal("10")}], ) sales_services.confirm_sales_bill(sb1) # 库存剩 1,再卖 2 失败 sb2 = sales_services.create_sales_bill( tenant=tenant, customer=customer, warehouse=warehouse, lines=[{"product": product, "quantity": Decimal("2"), "unit_price": Decimal("10")}], ) from apps.inventory.services import InsufficientStock with pytest.raises(InsufficientStock): sales_services.confirm_sales_bill(sb2) # 事务回滚:库存仍 1,sb2 仍 draft stock = Stock.objects.get(tenant=tenant, warehouse=warehouse, product=product) assert stock.on_hand == Decimal("1") sb2.refresh_from_db() assert sb2.state == "draft" @pytest.mark.django_db def test_api_ping_works_through_asgi(): from rest_framework.test import APIClient c = APIClient() r = c.get("/api/v1/ping/") assert r.status_code == 200 assert r.json()["ok"] is True