177 lines
5.9 KiB
Python
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
|