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
+272
View File
@@ -0,0 +1,272 @@
"""P0 #2 多单位换算 / #3 自动取价 / #4 最低售价+抹零 集成测试。"""
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.catalog.models import Product, Unit, UnitConversion
from apps.catalog import services as catalog_services
from apps.partner.models import Customer, PriceLevel, CustomerProductPrice
from apps.sales import services as sales_services
from apps.sales.models import SalesBill
from apps.core.services import compute_round_off, BelowMinPrice
@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):
from apps.inventory.models import Warehouse
return baker.make(Warehouse, tenant=tenant, code="WH01", name="主仓")
@pytest.fixture
def product(db, tenant):
return baker.make(
Product, tenant=tenant, code="COLA", name="可乐",
sale_price=Decimal("5"), cost_price=Decimal("3"),
)
@pytest.fixture
def box_unit(db, tenant, product):
"""1 箱 = 24 瓶。"""
base = baker.make(Unit, tenant=tenant, code="bottle", name="瓶", is_base=True)
product.base_unit = base
product.save()
box = baker.make(Unit, tenant=tenant, code="box", name="箱", is_base=False)
UnitConversion.objects.create(tenant=tenant, product=product, unit=box, rate=Decimal("24"))
product.refresh_from_db()
return box
@pytest.fixture
def customer(db, tenant):
return baker.make(Customer, tenant=tenant, code="C001", name="张三商店")
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("3"),
)
# ---------- #2 多单位换算 ----------
def test_to_base_with_conversion(tenant, product, box_unit):
assert catalog_services.to_base(product, box_unit, Decimal("2")) == Decimal("48")
def test_to_base_missing_conversion_raises(tenant, product):
other = baker.make(Unit, tenant=tenant, code="kg", name="千克", is_base=False)
with pytest.raises(catalog_services.UnitConversionNotFound):
catalog_services.to_base(product, other, Decimal("1"))
def test_sales_line_with_unit_conversion(tenant, warehouse, customer, product, box_unit):
"""2 箱 × 96 元/箱 → 基本数量 48 瓶、金额 192、按基本单位出库。"""
_stock_in(tenant, warehouse, product, 100)
bill = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 2, "unit_price": 96,
"source_unit": box_unit}],
)
line = bill.lines.first()
assert line.quantity == Decimal("48") # 基本单位
assert line.source_quantity == Decimal("2") # 录入数量
assert line.source_unit == box_unit
assert line.amount == Decimal("192")
assert bill.total_amount == Decimal("192")
sales_services.confirm_sales_bill(bill)
from apps.inventory.models import Stock
assert Stock.objects.get(product=product).on_hand == Decimal("52")
def test_sales_line_unit_without_price_converts_default_price(tenant, warehouse, customer, product, box_unit):
"""带单位不传价:默认价 5 元/瓶 × 24 = 120 元/箱。"""
_stock_in(tenant, warehouse, product, 100)
bill = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 1, "source_unit": box_unit}],
)
line = bill.lines.first()
assert line.unit_price == Decimal("120")
assert line.amount == Decimal("120")
def test_source_unit_min_price_compares_once(tenant, warehouse, customer, product, box_unit):
"""最低售价以基本单位计价,录入单位价格只换算一次。"""
product.min_sale_price = Decimal("5")
product.save(update_fields=["min_sale_price"])
with pytest.raises(BelowMinPrice):
sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 1, "unit_price": 96,
"source_unit": box_unit}],
)
# ---------- #3 自动取价 ----------
def test_quote_priority_customer_price(tenant, customer, product):
CustomerProductPrice.objects.create(
tenant=tenant, customer=customer, product=product, price=Decimal("4.2")
)
q = __import__("apps.partner.services", fromlist=["quote_price"]).quote_price(
tenant=tenant, customer=customer, product=product
)
assert q["price"] == Decimal("4.2")
assert q["source"] == "customer"
def test_quote_priority_level_price(tenant, customer, product):
level = baker.make(PriceLevel, tenant=tenant, code="VIP", name="VIP",
discount_rate=Decimal("0.9"))
customer.price_level = level
customer.save()
q = __import__("apps.partner.services", fromlist=["quote_price"]).quote_price(
tenant=tenant, customer=customer, product=product
)
assert q["price"] == Decimal("4.5000")
assert q["source"] == "level"
def test_quote_priority_last_price(tenant, warehouse, customer, product):
_stock_in(tenant, warehouse, product, 100)
b1 = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 1, "unit_price": 4.8}],
)
sales_services.confirm_sales_bill(b1)
q = __import__("apps.partner.services", fromlist=["quote_price"]).quote_price(
tenant=tenant, customer=customer, product=product
)
assert q["price"] == Decimal("4.8")
assert q["source"] == "last"
assert q["last_price"] == Decimal("4.8")
assert q["min_price"] == Decimal("4.8")
assert q["max_price"] == Decimal("4.8")
def test_quote_default_when_nothing(tenant, customer, product):
q = __import__("apps.partner.services", fromlist=["quote_price"]).quote_price(
tenant=tenant, customer=customer, product=product
)
assert q["price"] == Decimal("5")
assert q["source"] == "default"
def test_create_bill_auto_price(tenant, warehouse, customer, product):
_stock_in(tenant, warehouse, product, 100)
CustomerProductPrice.objects.create(
tenant=tenant, customer=customer, product=product, price=Decimal("4.2")
)
bill = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 10}], # 不传价
)
assert bill.lines.first().unit_price == Decimal("4.2")
assert bill.total_amount == Decimal("42")
def test_price_quote_api(db, auth_client, tenant, customer, product):
resp = auth_client.get(
f"/api/v1/partner/price-quote/?customer_id={customer.id}&product_ids={product.id}"
)
assert resp.status_code == 200, resp.content
data = resp.json()
assert data["results"][0]["price"] == "5.0000"
assert data["results"][0]["source"] == "default"
def test_credit_usage_api(db, auth_client, tenant, customer):
from apps.finance.models import Receivable
baker.make(
Receivable, tenant=tenant, customer=customer, bill_no="RC1",
bill_date=date.today(), total_amount=Decimal("100"), paid_amount=Decimal("40"),
status="partial",
)
resp = auth_client.get(f"/api/v1/partner/credit-usage/{customer.id}/")
assert resp.status_code == 200, resp.content
data = resp.json()
assert data["outstanding"] == "60.0000"
# ---------- #4 最低售价 + 抹零 ----------
def test_min_price_blocks_and_allows_override(tenant, warehouse, customer, product):
product.min_sale_price = Decimal("4")
product.save()
_stock_in(tenant, warehouse, product, 100)
with pytest.raises(BelowMinPrice):
sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 1, "unit_price": 3.5}],
)
# 审批放行
bill = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 1, "unit_price": 3.5,
"allow_below_min": True}],
)
assert bill.total_amount == Decimal("3.5")
def test_round_off_to_yuan(tenant, warehouse, customer, product):
_stock_in(tenant, warehouse, product, 100)
bill = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 3, "unit_price": 5.52}], # 16.56
round_to="1",
)
assert bill.total_amount == Decimal("16.0000")
assert bill.round_off == Decimal("0.5600")
def test_round_off_noop_at_cents(tenant, warehouse, customer, product):
_stock_in(tenant, warehouse, product, 100)
bill = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 1, "unit_price": 5}],
round_to="0.01",
)
assert bill.round_off == Decimal("0")
assert bill.total_amount == Decimal("5")
def test_compute_round_off_matrix():
assert compute_round_off(Decimal("100.56"), Decimal("1")) == Decimal("0.5600")
assert compute_round_off(Decimal("100.56"), Decimal("0.1")) == Decimal("0.0600")
assert compute_round_off(Decimal("100.56"), Decimal("0.01")) == Decimal("0.0000")
assert compute_round_off(Decimal("100"), None) == Decimal("0")
def test_receivable_amount_after_round_off(tenant, warehouse, customer, product):
from apps.finance.models import Receivable
_stock_in(tenant, warehouse, product, 100)
bill = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 3, "unit_price": 5.52}],
round_to="1",
)
sales_services.confirm_sales_bill(bill)
recv = Receivable.objects.get(source_ref=bill.bill_no)
assert recv.total_amount == bill.total_amount == Decimal("16.0000")