Files

177 lines
5.9 KiB
Python

"""端到端冒烟测试:完整业务链。
链:建商品 → 建仓库 → 建客户/供应商 → 进货单过账(写库存 + 生成应付)
→ 付款单核销 → 销售单过账(扣库存 + 生成应收)→ 收款单核销
→ 最终库存/应付/应收 余额一致
"""
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