107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
"""往来单位服务:开单取价 + 信用额度。
|
||
|
||
取价优先级(quote_price):
|
||
1. 客户专属价 CustomerProductPrice
|
||
2. 价格等级价 PriceLevel.discount_rate × 默认售价(rate>0 且 <1 时生效)
|
||
3. 最近成交价(该客户+商品最近一张已过账销售单行单价)
|
||
4. 默认售价 Product.sale_price
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
|
||
from .models import Customer, CustomerProductPrice, PriceLevel
|
||
|
||
|
||
def quote_price(*, tenant, customer: Customer, product) -> dict:
|
||
"""返回 {price, source, last_price, min_price, max_price}。
|
||
|
||
price 为基本单位价;last/min/max 为该客户历史成交参考(基本单位口径)。
|
||
"""
|
||
# 1. 客户专属价
|
||
cpp = CustomerProductPrice.objects.filter(
|
||
tenant=tenant, customer=customer, product=product
|
||
).first()
|
||
if cpp is not None:
|
||
price, source = cpp.price, "customer"
|
||
else:
|
||
# 2. 价格等级价
|
||
level: PriceLevel | None = customer.price_level
|
||
rate = level.discount_rate if level else Decimal("0")
|
||
if rate and Decimal("0") < rate < Decimal("1"):
|
||
price, source = product.sale_price * rate, "level"
|
||
else:
|
||
# 3. 最近成交价
|
||
last = _history(tenant, customer, product).first()
|
||
if last is not None:
|
||
price, source = last.unit_price, "last"
|
||
else:
|
||
price, source = product.sale_price, "default"
|
||
|
||
hist = list(_history(tenant, customer, product).values_list(
|
||
"unit_price", flat=True
|
||
)[:50])
|
||
return {
|
||
"price": price,
|
||
"source": source,
|
||
"last_price": hist[0] if hist else None,
|
||
"min_price": min(hist) if hist else None,
|
||
"max_price": max(hist) if hist else None,
|
||
}
|
||
|
||
|
||
def _history(tenant, customer: Customer, product):
|
||
"""该客户+商品的已过账销售行(新→旧)。"""
|
||
from apps.sales.models import SalesBillLine
|
||
|
||
return (
|
||
SalesBillLine.objects.filter(
|
||
tenant=tenant,
|
||
bill__customer=customer,
|
||
bill__state="confirmed",
|
||
product=product,
|
||
)
|
||
.order_by("-bill_id", "-id")
|
||
)
|
||
|
||
|
||
def credit_usage(*, tenant, customer: Customer) -> dict:
|
||
"""客户信用额度占用:{limit, outstanding, available}。
|
||
|
||
limit=0 表示未启用管控;available = limit - outstanding(未启用时为 None)。
|
||
"""
|
||
from django.db.models import Sum
|
||
|
||
from apps.finance.models import Receivable
|
||
|
||
agg = Receivable.objects.filter(
|
||
tenant=tenant,
|
||
customer=customer,
|
||
status__in=["open", "partial"],
|
||
is_deleted=False,
|
||
).aggregate(total=Sum("total_amount"), paid=Sum("paid_amount"))
|
||
outstanding = ((agg["total"] or Decimal("0")) - (agg["paid"] or Decimal("0"))).quantize(
|
||
Decimal("0.0001")
|
||
)
|
||
limit = customer.credit_limit or Decimal("0")
|
||
return {
|
||
"limit": limit,
|
||
"outstanding": outstanding,
|
||
"available": (limit - outstanding) if limit > 0 else None,
|
||
}
|
||
|
||
|
||
def check_credit(*, tenant, customer: Customer, extra_amount: Decimal) -> dict:
|
||
"""下单/过账前检查额度。返回 usage;超限抛 CreditLimitExceeded。"""
|
||
from apps.sales.services import CreditLimitExceeded
|
||
|
||
usage = credit_usage(tenant=tenant, customer=customer)
|
||
if usage["limit"] <= 0:
|
||
return usage
|
||
if usage["outstanding"] + extra_amount > usage["limit"]:
|
||
raise CreditLimitExceeded(
|
||
customer.code, usage["outstanding"], usage["limit"], extra_amount
|
||
)
|
||
return usage
|