Files
dealerhub/backend/apps/purchase/views.py
T

166 lines
6.6 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.
"""purchase async ViewSets。
开单页动作接口:
- `create_bill`(POST /purchase/bills/create-bill/):建草稿(支持批次号/生产日/到期日/录入单位)
- `confirm_bill`(POST /purchase/bills/<id>/confirm/):过账(写库存 + 生成应付)
"""
from asgiref.sync import sync_to_async
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from apps.core.services import InvalidLinePrice, InvalidLineQuantity
from apps.core.viewset import BaseTenantViewSet
from .models import PurchaseOrder, PurchaseBill
from .serializers import PurchaseOrderSerializer, PurchaseBillSerializer
from . import services as purchase_services
class PurchaseOrderViewSet(BaseTenantViewSet):
model = PurchaseOrder
serializer_class = PurchaseOrderSerializer
search_fields = ["bill_no", "supplier__code", "supplier__name"]
select_related_fields = ("supplier", "warehouse")
prefetch_related_fields = (
"lines", "lines__product", "lines__source_unit", "lines__product__base_unit",
)
class PurchaseBillViewSet(BaseTenantViewSet):
model = PurchaseBill
serializer_class = PurchaseBillSerializer
search_fields = ["bill_no", "supplier__code", "supplier__name"]
select_related_fields = ("supplier", "warehouse")
prefetch_related_fields = (
"lines", "lines__product", "lines__source_unit", "lines__product__base_unit",
)
@action(detail=False, methods=["post"], url_path="create-bill")
async def create_bill(self, request):
"""POST /api/v1/purchase/bills/create-bill/
body: {supplier, warehouse, bill_date?, remark?, lines: [
{product, quantity, unit_price, source_unit?, batch_no?,
production_date?, expiry_date?}]}
"""
tenant = await self.get_tenant()
if tenant is None:
raise ValidationError({"tenant": "无法识别租户"})
payload = request.data or {}
# 套餐配额:月度单据量(批次 C1)
from apps.billing import quota as billing_quota
def _quota_check():
return billing_quota.check_and_count(tenant, "bills_monthly", delta=1)
try:
await sync_to_async(_quota_check)()
except billing_quota.QuotaExceeded as exc:
return Response(exc.as_dict(), status=status.HTTP_403_FORBIDDEN)
supplier_id, warehouse_id = payload.get("supplier"), payload.get("warehouse")
lines = payload.get("lines") or []
if not supplier_id or not warehouse_id:
raise ValidationError({"detail": "supplier 与 warehouse 必填"})
if not lines:
raise ValidationError({"detail": "lines 不能为空"})
if not isinstance(lines, list):
raise ValidationError({"detail": "lines 必须是数组"})
# 逐行结构校验:非 dict 或缺 product 直接 400(否则下游 .get() 会 500)
for idx, ln in enumerate(lines, start=1):
if not isinstance(ln, dict):
raise ValidationError({"detail": f"第 {idx} 行明细格式错误(应为对象)"})
if not ln.get("product"):
raise ValidationError({"detail": f"第 {idx} 行缺少 product"})
from apps.partner.models import Supplier
from apps.inventory.models import Warehouse
from apps.catalog.models import Product
def _load_refs():
supplier = Supplier.objects.filter(tenant=tenant, pk=supplier_id).first()
warehouse = Warehouse.objects.filter(tenant=tenant, pk=warehouse_id).first()
products = {
p.id: p for p in Product.objects.filter(
tenant=tenant, pk__in=[ln.get("product") for ln in lines]
)
}
return supplier, warehouse, products
supplier, warehouse, products = await sync_to_async(_load_refs)()
if supplier is None:
raise ValidationError({"supplier": "供应商不存在"})
if warehouse is None:
raise ValidationError({"warehouse": "仓库不存在"})
resolved_lines = []
for ln in lines:
product = products.get(ln.get("product"))
if product is None:
raise ValidationError({"lines": f"商品 {ln.get('product')} 不存在"})
if ln.get("unit_price") in (None, ""):
raise ValidationError({"lines": f"商品 {product.code} 缺少进货单价"})
resolved_lines.append({
"product": product,
"quantity": ln.get("quantity"),
"unit_price": ln.get("unit_price"),
"source_unit": ln.get("source_unit"),
"batch_no": ln.get("batch_no") or "",
"production_date": ln.get("production_date") or None,
"expiry_date": ln.get("expiry_date") or None,
})
from datetime import date as _date
kwargs = {
"tenant": tenant,
"supplier": supplier,
"warehouse": warehouse,
"lines": resolved_lines,
"remark": payload.get("remark") or "",
}
bill_date = payload.get("bill_date")
if bill_date:
kwargs["bill_date"] = _date.fromisoformat(bill_date)
# InvalidLine* 等由全局异常处理器统一映射
try:
bill = await sync_to_async(purchase_services.create_purchase_bill)(**kwargs)
except ValueError as exc:
return Response(
{"code": "invalid_line", "detail": str(exc)},
status=status.HTTP_400_BAD_REQUEST,
)
data = await sync_to_async(lambda: PurchaseBillSerializer(bill).data)()
return Response(data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=["post"], url_path="confirm")
async def confirm_bill(self, request, pk=None):
"""POST /api/v1/purchase/bills/<id>/confirm/"""
bill = await self.aget_object()
def _confirm():
return purchase_services.confirm_purchase_bill(
bill, user=request.user if request.user.is_authenticated else None,
)
try:
await sync_to_async(_confirm)()
except ValueError as exc:
return Response(
{"code": "invalid_state", "detail": str(exc)},
status=status.HTTP_400_BAD_REQUEST,
)
def _reload():
bill.refresh_from_db()
return PurchaseBillSerializer(bill).data
data = await sync_to_async(_reload)()
return Response(data)