baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
"""inventory 集成测试。"""
import pytest
from datetime import date, timedelta
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.models import Warehouse, Stock, StockBatch, StockMovement
from apps.inventory import services as inv_services
@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(db, tenant):
return baker.make(Warehouse, tenant=tenant, code="WH01", name="主仓")
@pytest.fixture
def product(db, tenant):
from apps.catalog.models import Product
return baker.make(Product, tenant=tenant, code="P001", name="测试商品")
# --- services 单元测试(不依赖 API) ---
@pytest.mark.django_db
def test_inbound_creates_stock_record(tenant, warehouse, product):
result = inv_services.inbound(
tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("100"), unit_cost=Decimal("5"),
)
stock = Stock.objects.get(tenant=tenant, warehouse=warehouse, product=product)
assert stock.on_hand == Decimal("100")
assert stock.avg_cost == Decimal("5")
assert result.movement.movement_type == "inbound"
assert result.movement.quantity == Decimal("100")
@pytest.mark.django_db
def test_inbound_weighted_avg_cost(tenant, warehouse, product):
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("10"), unit_cost=Decimal("10"))
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("10"), unit_cost=Decimal("20"))
stock = Stock.objects.get(tenant=tenant, warehouse=warehouse, product=product)
# (10*10 + 20*10) / 20 = 15
assert stock.avg_cost == Decimal("15.0000")
@pytest.mark.django_db
def test_outbound_decreases_on_hand(tenant, warehouse, product):
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("100"))
inv_services.outbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("30"))
stock = Stock.objects.get(tenant=tenant, warehouse=warehouse, product=product)
assert stock.on_hand == Decimal("70")
@pytest.mark.django_db
def test_outbound_insufficient_raises(tenant, warehouse, product):
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("10"))
with pytest.raises(inv_services.InsufficientStock):
inv_services.outbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("20"))
@pytest.mark.django_db
def test_outbound_no_stock_record_raises(tenant, warehouse, product):
with pytest.raises(inv_services.InsufficientStock):
inv_services.outbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("1"))
@pytest.mark.django_db
def test_lock_and_unlock(tenant, warehouse, product):
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("100"))
inv_services.lock(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("40"))
stock = Stock.objects.get(tenant=tenant, warehouse=warehouse, product=product)
assert stock.on_hand == Decimal("100")
assert stock.locked == Decimal("40")
# 锁定后再出 70 应失败(可用 = 100 - 40 = 60 < 70)
with pytest.raises(inv_services.InsufficientStock):
inv_services.outbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("70"))
# 但出 60 可以
inv_services.outbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("60"))
inv_services.unlock(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("20"))
stock.refresh_from_db()
assert stock.on_hand == Decimal("40")
assert stock.locked == Decimal("20")
@pytest.mark.django_db
def test_adjust_sets_on_hand(tenant, warehouse, product):
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("100"))
inv_services.adjust(tenant=tenant, warehouse=warehouse, product=product,
new_on_hand=Decimal("85"))
stock = Stock.objects.get(tenant=tenant, warehouse=warehouse, product=product)
assert stock.on_hand == Decimal("85")
@pytest.mark.django_db
def test_movements_recorded(tenant, warehouse, product):
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("100"))
inv_services.outbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("30"))
movements = StockMovement.objects.filter(tenant=tenant).order_by("id")
assert movements.count() == 2
assert movements[0].movement_type == "inbound"
assert movements[1].movement_type == "outbound"
@pytest.mark.django_db
def test_negative_quantity_rejected(tenant, warehouse, product):
with pytest.raises(ValueError):
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("-1"))
with pytest.raises(ValueError):
inv_services.outbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("0"))
# --- API 集成测试 ---
@pytest.mark.django_db
def test_api_create_warehouse(auth_client, tenant):
resp = auth_client.post(
"/api/v1/inventory/warehouses/",
{"code": "WH02", "name": "二号仓", "is_default": False},
format="json",
)
assert resp.status_code == 201, resp.content
assert resp.json()["code"] == "WH02"
@pytest.mark.django_db
def test_api_list_warehouses(auth_client, tenant):
baker.make(Warehouse, tenant=tenant, code="WH1")
baker.make(Warehouse, tenant=tenant, code="WH2")
resp = auth_client.get("/api/v1/inventory/warehouses/")
assert resp.status_code == 200
assert resp.json()["count"] == 2
@pytest.mark.django_db
def test_api_list_stocks_readonly(auth_client, tenant, warehouse, product):
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("100"))
resp = auth_client.get("/api/v1/inventory/stocks/")
assert resp.status_code == 200
data = resp.json()
assert data["count"] == 1
assert data["results"][0]["on_hand"] == "100.0000"
assert data["results"][0]["available"] == "100.0000"
@pytest.mark.django_db
def test_api_list_movements(auth_client, tenant, warehouse, product):
inv_services.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("100"))
resp = auth_client.get("/api/v1/inventory/movements/")
assert resp.status_code == 200
assert resp.json()["count"] == 1
assert resp.json()["results"][0]["movement_type"] == "inbound"
@pytest.mark.django_db
def test_api_near_expiry_filter_is_server_side(auth_client, tenant, warehouse, product):
today = date.today()
baker.make(
StockBatch,
tenant=tenant,
warehouse=warehouse,
product=product,
batch_no="NEAR",
expiry_date=today + timedelta(days=7),
on_hand=Decimal("3"),
)
baker.make(
StockBatch,
tenant=tenant,
warehouse=warehouse,
product=product,
batch_no="FAR",
expiry_date=today + timedelta(days=90),
on_hand=Decimal("3"),
)
response = auth_client.get(
"/api/v1/inventory/batches/?in_stock=1&near_expiry=1&expiry_days=30"
)
assert response.status_code == 200
assert [item["batch_no"] for item in response.json()["results"]] == ["NEAR"]