Files

107 lines
3.5 KiB
Python
Raw Permalink 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.
"""往来单位服务:开单取价 + 信用额度。
取价优先级(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