Files
dealerhub/backend/tests/test_p0_unit_price.py
T

273 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")