forked from erp-dev/erp
feat: balance api
This commit is contained in:
163
api_v1/views/business/README.md
Normal file
163
api_v1/views/business/README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 业务域 API 文档(Purchase & Sales)
|
||||
|
||||
所有接口均位于 `/api/v1/` 前缀下,要求用户已登录且具备员工身份。除特殊说明外,返回值为 JSON,错误时返回 `{"error": "...", ...}`。
|
||||
|
||||
## 通用约定
|
||||
- **分页**:列表接口使用 `limit` / `offset`(默认 `limit=20`,最大 `100`)。
|
||||
- **items 结构**:与仓库模式相关
|
||||
- 严进/严进严出仓库:`numbers: [int, ...]`
|
||||
- 宽进宽出仓库:`quantity: number` + `num_of_rolls: int`
|
||||
- **状态枚举**:`PENDING=1`、`APPROVED=2`、`CANCELLED=3`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 采购单(PurchaseOrder)
|
||||
|
||||
### 1.1 列表
|
||||
- **GET** `/api/v1/purchase-orders/`
|
||||
- **查询参数**:`limit`、`offset`
|
||||
- **返回**:`{"count": int, "next": url|null, "previous": url|null, "results": [PurchaseOrder]}`
|
||||
每个 `PurchaseOrder` 记录包含 `supplier_name / operator_name / warehouse_name / total_amount / total_quantity / items[...]` 等字段。
|
||||
|
||||
### 1.2 创建
|
||||
- **POST** `/api/v1/purchase-orders/`
|
||||
- **请求体**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `supplier` | int | 供应商 ID(必填) |
|
||||
| `warehouse` | int | 仓库 ID,或使用 `warehouse_id`(必填) |
|
||||
| `order_date` | str (`YYYY-MM-DD`) | 采购日期 |
|
||||
| `items` | list | 产品明细(至少 1 条) |
|
||||
| `remarks` | str | 备注,可选 |
|
||||
|
||||
明细字段:
|
||||
|
||||
| 仓库模式 | 必填字段 |
|
||||
|----------|----------|
|
||||
| 严进/严进严出 | `product_id`, `numbers` (list[int]), `price`, `unit` |
|
||||
| 宽进宽出 | `product_id`, `quantity`, `num_of_rolls`, `price`, `unit` |
|
||||
|
||||
- **成功返回**:`201` + `{"id": int, "status": 1, "message": "采购单创建成功,等待审批"}`。
|
||||
|
||||
### 1.3 审批 / 作废
|
||||
- **POST** `/api/v1/purchase-orders/<id>/review/`
|
||||
- **请求体**:`{"action": "approve" | "cancel"}`
|
||||
- **返回**:最新的 `PurchaseOrder` 序列化结果。
|
||||
- `approve`:当商户开启自动入库时,会异步创建入库任务。
|
||||
- `cancel`:若已生成入库记录,返回 `400`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 销售单(SalesOrder)
|
||||
|
||||
接口与采购单保持一致,仅字段差异:
|
||||
- 关联主体:`customer`(客户 ID)
|
||||
- 日期字段:`order_date` 映射到 `sales_date`
|
||||
- 审批通过后触发**出库**任务,库存方向为 “出库/负数”。
|
||||
|
||||
### 2.1 列表
|
||||
- **GET** `/api/v1/sales-orders/`
|
||||
- **返回**:同采购列表,但字段为 `customer_name` 等。
|
||||
|
||||
### 2.2 创建
|
||||
- **POST** `/api/v1/sales-orders/`
|
||||
- **请求体**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `customer` | int | 客户 ID |
|
||||
| `warehouse` | int | 仓库 ID(或 `warehouse_id`) |
|
||||
| `order_date` | str | 销售日期 |
|
||||
| `items` | list | 产品明细,与采购单格式一致 |
|
||||
| `remarks` | str | 可选 |
|
||||
|
||||
`items` 结构(按仓库模式):
|
||||
|
||||
| 仓库模式 | 出库模式 | 必填字段 |
|
||||
|----------|----------|----------|
|
||||
| `UNRESTRICTED` | 宽出 | `product_id`, `quantity`, `num_of_rolls`, `price`, `unit` |
|
||||
| `RESTRICT_IN` | 宽出 | `product_id`, `numbers` (list[int]), `price`, `unit` |
|
||||
| `RESTRICT_IN_OUT` | 严出 | `product_id`, `consume_detail_ids` (list[int]), `quantity`, `price`, `unit` |
|
||||
|
||||
- **成功返回**:`201` + `{"id": int, "status": 1, "message": "销售单创建成功,等待审批"}`。
|
||||
|
||||
### 2.3 审批 / 作废
|
||||
- **POST** `/api/v1/sales-orders/<id>/review/`
|
||||
- **请求体**:`{"action": "approve" | "cancel"}`
|
||||
- **返回**:`SalesOrder` 序列化数据。
|
||||
- `approve`:若开启自动出库,则投递 `create_sales_order_stock_entries`。
|
||||
- `cancel`:若已生成出库记录(`StockChangeRecord`),返回 `400`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 响应字段说明(节选)
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `total_amount` | 明细金额合计(未带方向) |
|
||||
| `diff_quantity` | 空差数量合计 |
|
||||
| `total_quantity` | 原始数量合计 |
|
||||
| `items[].quantity_of_rolls` | 严进模式下的各条数明细(字符串,以逗号分隔) |
|
||||
| `items[].num_of_rolls` | 条数 |
|
||||
| `status` | 1=审批中、2=通过、3=作废 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 付款单(PaymentOrder)
|
||||
|
||||
### 4.1 列表
|
||||
- **GET** `/api/v1/payment-orders/`
|
||||
- 返回字段:`supplier_name`、`payment_date`、`amount`、`status` 等。
|
||||
|
||||
### 4.2 创建
|
||||
- **POST** `/api/v1/payment-orders/`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `supplier` | int | 供应商 ID |
|
||||
| `payment_date` | str (`YYYY-MM-DD`) | 付款日期 |
|
||||
| `amount` | decimal | 付款金额(必须 > 0) |
|
||||
| `remarks` | str | 可选 |
|
||||
|
||||
### 4.3 审批 / 作废
|
||||
- **POST** `/api/v1/payment-orders/<id>/review/`
|
||||
- `{"action": "approve"}` 或 `{"action": "cancel"}`
|
||||
|
||||
---
|
||||
|
||||
## 5. 收款单(ReceiptOrder)
|
||||
|
||||
接口与付款单类似,只是主体为 `customer`:
|
||||
|
||||
- **GET** `/api/v1/receipt-orders/`
|
||||
- **POST** `/api/v1/receipt-orders/`:需要 `customer`、`receipt_date`、`amount`
|
||||
- **POST** `/api/v1/receipt-orders/<id>/review/`
|
||||
|
||||
审批通过表示“确认收款”,作废则恢复为初始状态。
|
||||
|
||||
---
|
||||
|
||||
## 6. 错误示例
|
||||
|
||||
| 场景 | HTTP | 返回体 |
|
||||
|------|------|--------|
|
||||
| 未登录 | 401 | `{"detail": "Authentication credentials were not provided."}` |
|
||||
| 缺少必填字段 | 400 | `{"error": "缺少供应商 ID"}` 等 |
|
||||
| 无权限访问他商户单据 | 403 | `{"error": "无权限访问"}` |
|
||||
| 单据不存在 | 404 | `{"error": "采购单不存在"}` / `{"error": "销售单不存在"}` |
|
||||
| 已有库存记录仍尝试作废 | 400 | `{"error": "采购单已生成出入库记录,无法作废"}`(销售单同理) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 客户欠款查询
|
||||
|
||||
- **GET** `/api/v1/customers/<id>/balance/`
|
||||
- **描述**:返回指定客户当前的应收余额(审批通过的销售单金额累加减去收款单金额),数据来源于余额表,因此查询为 O(1)。
|
||||
- **响应**:`{"customer": 12, "customer_name": "张三", "balance": "1234.50"}`
|
||||
- `balance` 为字符串格式的十进制数,正数表示客户欠款,应收;负数表示已收超额。
|
||||
|
||||
如需对 `items` 结构、仓库模式或审批流程做深入了解,请参阅:
|
||||
- `docs/purchase_order_approval_and_red_flush.md`
|
||||
- `docs/sales_order_approval_and_red_flush.md`
|
||||
|
||||
6
api_v1/views/business/__init__.py
Normal file
6
api_v1/views/business/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
业务域相关的 API 视图与工具。
|
||||
|
||||
该包下按业务对象分类,例如 purchase、sales 等。
|
||||
"""
|
||||
|
||||
4
api_v1/views/business/balance/__init__.py
Normal file
4
api_v1/views/business/balance/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
余额相关 API 视图。
|
||||
"""
|
||||
|
||||
36
api_v1/views/business/balance/views.py
Normal file
36
api_v1/views/business/balance/views.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from rest_framework import views
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from business import services as business_services
|
||||
from api_v1.views.stock_change_views.mixins import StockChangeViewMixin
|
||||
|
||||
|
||||
class CustomerBalanceView(StockChangeViewMixin, views.APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, customer_id: int):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
try:
|
||||
customer = basic_models.Customer.objects.get(id=customer_id, merchant=merchant)
|
||||
except basic_models.Customer.DoesNotExist:
|
||||
return self.not_found_response('客户不存在')
|
||||
|
||||
balance: Decimal = business_services.BalanceService.get_customer_balance(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
'customer': customer.id,
|
||||
'customer_name': customer.name,
|
||||
'balance': str(balance),
|
||||
}
|
||||
)
|
||||
|
||||
133
api_v1/views/business/payment/views.py
Normal file
133
api_v1/views/business/payment/views.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from rest_framework import status, views, serializers, pagination
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from business import services as business_services
|
||||
from business import models as business_models
|
||||
from api_v1.views.stock_change_views.mixins import StockChangeViewMixin
|
||||
|
||||
|
||||
class PaymentOrderSerializer(serializers.ModelSerializer):
|
||||
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
||||
operator_name = serializers.CharField(source='operator.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = business_models.PaymentOrder
|
||||
fields = [
|
||||
'id', 'supplier', 'supplier_name', 'payment_date',
|
||||
'amount', 'operator', 'operator_name', 'status',
|
||||
'remarks', 'created_at', 'updated_at',
|
||||
]
|
||||
read_only_fields = ['id', 'supplier_name', 'operator_name', 'status', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
class PaymentOrderPagination(pagination.LimitOffsetPagination):
|
||||
default_limit = 20
|
||||
max_limit = 100
|
||||
|
||||
|
||||
class PaymentOrderView(StockChangeViewMixin, views.APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = PaymentOrderPagination
|
||||
|
||||
def get(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
merchant = request.user.employee.merchant
|
||||
queryset = business_models.PaymentOrder.objects.filter(merchant=merchant).select_related(
|
||||
'supplier', 'operator'
|
||||
).order_by('-created_at')
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request, view=self)
|
||||
serializer = PaymentOrderSerializer(page, many=True)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
merchant = request.user.employee.merchant
|
||||
data = request.data or {}
|
||||
supplier_id = data.get('supplier')
|
||||
payment_date = data.get('payment_date')
|
||||
amount = data.get('amount')
|
||||
remarks = data.get('remarks', '')
|
||||
|
||||
if not supplier_id:
|
||||
return Response({'error': '缺少供应商 ID'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not payment_date:
|
||||
return Response({'error': '缺少 payment_date'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if amount is None:
|
||||
return Response({'error': '缺少 amount'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
supplier = basic_models.Supplier.objects.get(id=supplier_id, merchant=merchant)
|
||||
except basic_models.Supplier.DoesNotExist:
|
||||
return Response({'error': f'供应商 {supplier_id} 不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
operator = request.user.employee
|
||||
|
||||
try:
|
||||
payment_order = business_services.create_payment_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
payment_date=payment_date,
|
||||
amount=amount,
|
||||
operator=operator,
|
||||
remarks=remarks,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'id': payment_order.id,
|
||||
'status': payment_order.status,
|
||||
'message': '付款单创建成功,等待审批',
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class PaymentOrderReviewSerializer(serializers.Serializer):
|
||||
action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')])
|
||||
|
||||
|
||||
class PaymentOrderReviewView(StockChangeViewMixin, views.APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
ACTION_STATUS_MAP = {
|
||||
'approve': business_models.PaymentOrderStatusEnum.APPROVED,
|
||||
'cancel': business_models.PaymentOrderStatusEnum.CANCELLED,
|
||||
}
|
||||
|
||||
def post(self, request, pk: int):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
try:
|
||||
payment_order = business_models.PaymentOrder.objects.select_related(
|
||||
'supplier', 'operator'
|
||||
).get(id=pk, merchant=merchant)
|
||||
except business_models.PaymentOrder.DoesNotExist:
|
||||
return self.not_found_response('付款单不存在')
|
||||
|
||||
serializer = PaymentOrderReviewSerializer(data=request.data or {})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
action = serializer.validated_data['action']
|
||||
target_status = self.ACTION_STATUS_MAP[action]
|
||||
|
||||
try:
|
||||
business_services.review_payment_order(
|
||||
payment_order=payment_order,
|
||||
target_status=target_status,
|
||||
reviewed_by=request.user,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
refreshed = business_models.PaymentOrder.objects.select_related(
|
||||
'supplier', 'operator'
|
||||
).get(id=payment_order.id)
|
||||
return Response(PaymentOrderSerializer(refreshed).data, status=status.HTTP_200_OK)
|
||||
|
||||
4
api_v1/views/business/purchase/__init__.py
Normal file
4
api_v1/views/business/purchase/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
采购域 API 视图。
|
||||
"""
|
||||
|
||||
177
api_v1/views/business/purchase/views.py
Normal file
177
api_v1/views/business/purchase/views.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from rest_framework import status, views, serializers, pagination
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from business import services as business_services
|
||||
from business import models as business_models
|
||||
from api_v1.views.stock_change_views.mixins import StockChangeViewMixin
|
||||
|
||||
|
||||
class PurchaseOrderItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = business_models.PurchaseOrderItem
|
||||
fields = [
|
||||
'id', 'product', 'price', 'color', 'quantity', 'unit',
|
||||
'empty_diff_percent', 'quantity_of_rolls', 'num_of_rolls',
|
||||
'batch_number', 'remarks', 'created_at', 'updated_at', 'spec',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'total_amount', 'diff_quantity', 'real_quantity']
|
||||
|
||||
|
||||
class PurchaseOrderSerializer(serializers.ModelSerializer):
|
||||
total_amount = serializers.SerializerMethodField(read_only=True)
|
||||
diff_quantity = serializers.SerializerMethodField(read_only=True)
|
||||
total_quantity = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_total_amount(self, obj: business_models.PurchaseOrder):
|
||||
return obj.get_total_amount()
|
||||
|
||||
def get_diff_quantity(self, obj: business_models.PurchaseOrder):
|
||||
return obj.get_total_diff_quantity()
|
||||
|
||||
def get_total_quantity(self, obj: business_models.PurchaseOrder):
|
||||
return obj.get_total_quantity()
|
||||
|
||||
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
||||
operator_name = serializers.CharField(source='operator.name', read_only=True)
|
||||
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||
items = PurchaseOrderItemSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = business_models.PurchaseOrder
|
||||
fields = [
|
||||
'id', 'supplier', 'supplier_name', 'purchase_date', 'kind',
|
||||
'total_amount', 'diff_quantity', 'total_quantity',
|
||||
'operator', 'operator_name', 'warehouse', 'warehouse_name',
|
||||
'status', 'remarks', 'created_at', 'updated_at', 'items',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'items', 'supplier_name', 'operator_name', 'warehouse_name']
|
||||
|
||||
|
||||
class PurchaseOrderPagination(pagination.LimitOffsetPagination):
|
||||
default_limit = 20
|
||||
max_limit = 100
|
||||
|
||||
|
||||
class PurchaseOrderView(StockChangeViewMixin, views.APIView):
|
||||
"""采购订单查询与创建"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = PurchaseOrderPagination
|
||||
|
||||
def get(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
queryset = business_models.PurchaseOrder.objects.filter(merchant=merchant).prefetch_related(
|
||||
'items', 'supplier', 'operator', 'warehouse'
|
||||
)
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset.order_by('-created_at'), request, view=self)
|
||||
serializer = PurchaseOrderSerializer(page, many=True)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
data = request.data or {}
|
||||
|
||||
supplier_id = data.get('supplier')
|
||||
warehouse_id = data.get('warehouse_id') or data.get('warehouse')
|
||||
order_date = data.get('order_date')
|
||||
items = data.get('items', [])
|
||||
remarks = data.get('remarks', '')
|
||||
|
||||
if not supplier_id:
|
||||
return Response({'error': '缺少供应商 ID'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not warehouse_id:
|
||||
return Response({'error': '缺少仓库 ID'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not order_date:
|
||||
return Response({'error': '缺少 order_date'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not isinstance(items, list) or not items:
|
||||
return Response({'error': 'items 需要为非空数组'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
supplier = basic_models.Supplier.objects.get(id=supplier_id, merchant=merchant)
|
||||
except basic_models.Supplier.DoesNotExist:
|
||||
return Response({'error': f'供应商 {supplier_id} 不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
warehouse = basic_models.WareHouse.objects.get(id=warehouse_id, merchant=merchant)
|
||||
except basic_models.WareHouse.DoesNotExist:
|
||||
return Response({'error': f'仓库 {warehouse_id} 不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
operator = request.user.employee
|
||||
|
||||
try:
|
||||
purchase_order = business_services.create_purchase_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=order_date,
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=items,
|
||||
remarks=remarks,
|
||||
created_by=request.user,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'id': purchase_order.id,
|
||||
'status': purchase_order.status,
|
||||
'message': '采购单创建成功,等待审批',
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class PurchaseOrderReviewSerializer(serializers.Serializer):
|
||||
action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')])
|
||||
|
||||
|
||||
class PurchaseOrderReviewView(StockChangeViewMixin, views.APIView):
|
||||
"""采购单审批 / 作废"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
ACTION_STATUS_MAP = {
|
||||
'approve': business_models.PurchaseOrderStatusEnum.APPROVED,
|
||||
'cancel': business_models.PurchaseOrderStatusEnum.CANCELLED,
|
||||
}
|
||||
|
||||
def post(self, request, pk: int):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
try:
|
||||
purchase_order = business_models.PurchaseOrder.objects.select_related(
|
||||
'supplier', 'operator', 'warehouse'
|
||||
).prefetch_related('items').get(id=pk, merchant=merchant)
|
||||
except business_models.PurchaseOrder.DoesNotExist:
|
||||
return self.not_found_response('采购单不存在')
|
||||
|
||||
serializer = PurchaseOrderReviewSerializer(data=request.data or {})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
action = serializer.validated_data['action']
|
||||
target_status = self.ACTION_STATUS_MAP[action]
|
||||
|
||||
try:
|
||||
business_services.review_purchase_order(
|
||||
purchase_order=purchase_order,
|
||||
target_status=target_status,
|
||||
reviewed_by=request.user,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
refreshed_order = business_models.PurchaseOrder.objects.select_related(
|
||||
'supplier', 'operator', 'warehouse'
|
||||
).prefetch_related('items').get(id=purchase_order.id)
|
||||
return Response(PurchaseOrderSerializer(refreshed_order).data, status=status.HTTP_200_OK)
|
||||
|
||||
133
api_v1/views/business/receipt/views.py
Normal file
133
api_v1/views/business/receipt/views.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from rest_framework import status, views, serializers, pagination
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from business import services as business_services
|
||||
from business import models as business_models
|
||||
from api_v1.views.stock_change_views.mixins import StockChangeViewMixin
|
||||
|
||||
|
||||
class ReceiptOrderSerializer(serializers.ModelSerializer):
|
||||
customer_name = serializers.CharField(source='customer.name', read_only=True)
|
||||
operator_name = serializers.CharField(source='operator.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = business_models.ReceiptOrder
|
||||
fields = [
|
||||
'id', 'customer', 'customer_name', 'receipt_date',
|
||||
'amount', 'operator', 'operator_name', 'status',
|
||||
'remarks', 'created_at', 'updated_at',
|
||||
]
|
||||
read_only_fields = ['id', 'customer_name', 'operator_name', 'status', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
class ReceiptOrderPagination(pagination.LimitOffsetPagination):
|
||||
default_limit = 20
|
||||
max_limit = 100
|
||||
|
||||
|
||||
class ReceiptOrderView(StockChangeViewMixin, views.APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = ReceiptOrderPagination
|
||||
|
||||
def get(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
merchant = request.user.employee.merchant
|
||||
queryset = business_models.ReceiptOrder.objects.filter(merchant=merchant).select_related(
|
||||
'customer', 'operator'
|
||||
).order_by('-created_at')
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request, view=self)
|
||||
serializer = ReceiptOrderSerializer(page, many=True)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
merchant = request.user.employee.merchant
|
||||
data = request.data or {}
|
||||
customer_id = data.get('customer')
|
||||
receipt_date = data.get('receipt_date')
|
||||
amount = data.get('amount')
|
||||
remarks = data.get('remarks', '')
|
||||
|
||||
if not customer_id:
|
||||
return Response({'error': '缺少客户 ID'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not receipt_date:
|
||||
return Response({'error': '缺少 receipt_date'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if amount is None:
|
||||
return Response({'error': '缺少 amount'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
customer = basic_models.Customer.objects.get(id=customer_id, merchant=merchant)
|
||||
except basic_models.Customer.DoesNotExist:
|
||||
return Response({'error': f'客户 {customer_id} 不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
operator = request.user.employee
|
||||
|
||||
try:
|
||||
receipt_order = business_services.create_receipt_order(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
receipt_date=receipt_date,
|
||||
amount=amount,
|
||||
operator=operator,
|
||||
remarks=remarks,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'id': receipt_order.id,
|
||||
'status': receipt_order.status,
|
||||
'message': '收款单创建成功,等待审批',
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class ReceiptOrderReviewSerializer(serializers.Serializer):
|
||||
action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')])
|
||||
|
||||
|
||||
class ReceiptOrderReviewView(StockChangeViewMixin, views.APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
ACTION_STATUS_MAP = {
|
||||
'approve': business_models.ReceiptOrderStatusEnum.APPROVED,
|
||||
'cancel': business_models.ReceiptOrderStatusEnum.CANCELLED,
|
||||
}
|
||||
|
||||
def post(self, request, pk: int):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
try:
|
||||
receipt_order = business_models.ReceiptOrder.objects.select_related(
|
||||
'customer', 'operator'
|
||||
).get(id=pk, merchant=merchant)
|
||||
except business_models.ReceiptOrder.DoesNotExist:
|
||||
return self.not_found_response('收款单不存在')
|
||||
|
||||
serializer = ReceiptOrderReviewSerializer(data=request.data or {})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
action = serializer.validated_data['action']
|
||||
target_status = self.ACTION_STATUS_MAP[action]
|
||||
|
||||
try:
|
||||
business_services.review_receipt_order(
|
||||
receipt_order=receipt_order,
|
||||
target_status=target_status,
|
||||
reviewed_by=request.user,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
refreshed = business_models.ReceiptOrder.objects.select_related(
|
||||
'customer', 'operator'
|
||||
).get(id=receipt_order.id)
|
||||
return Response(ReceiptOrderSerializer(refreshed).data, status=status.HTTP_200_OK)
|
||||
|
||||
177
api_v1/views/business/sales/views.py
Normal file
177
api_v1/views/business/sales/views.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from rest_framework import status, views, serializers, pagination
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from business import services as business_services
|
||||
from business import models as business_models
|
||||
from api_v1.views.stock_change_views.mixins import StockChangeViewMixin
|
||||
|
||||
|
||||
class SalesOrderItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = business_models.SalesOrderItem
|
||||
fields = [
|
||||
'id', 'product', 'price', 'color', 'quantity', 'unit',
|
||||
'empty_diff_percent', 'quantity_of_rolls', 'num_of_rolls',
|
||||
'consume_detail_ids', 'batch_number', 'remarks', 'created_at', 'updated_at', 'spec',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'total_amount', 'diff_quantity', 'real_quantity']
|
||||
|
||||
|
||||
class SalesOrderSerializer(serializers.ModelSerializer):
|
||||
total_amount = serializers.SerializerMethodField(read_only=True)
|
||||
diff_quantity = serializers.SerializerMethodField(read_only=True)
|
||||
total_quantity = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_total_amount(self, obj: business_models.SalesOrder):
|
||||
return obj.get_total_amount()
|
||||
|
||||
def get_diff_quantity(self, obj: business_models.SalesOrder):
|
||||
return obj.get_total_diff_quantity()
|
||||
|
||||
def get_total_quantity(self, obj: business_models.SalesOrder):
|
||||
return obj.get_total_quantity()
|
||||
|
||||
customer_name = serializers.CharField(source='customer.name', read_only=True)
|
||||
operator_name = serializers.CharField(source='operator.name', read_only=True)
|
||||
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||
items = SalesOrderItemSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = business_models.SalesOrder
|
||||
fields = [
|
||||
'id', 'customer', 'customer_name', 'sales_date', 'kind',
|
||||
'total_amount', 'diff_quantity', 'total_quantity',
|
||||
'operator', 'operator_name', 'warehouse', 'warehouse_name',
|
||||
'status', 'remarks', 'created_at', 'updated_at', 'items',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'items', 'customer_name', 'operator_name', 'warehouse_name']
|
||||
|
||||
|
||||
class SalesOrderPagination(pagination.LimitOffsetPagination):
|
||||
default_limit = 20
|
||||
max_limit = 100
|
||||
|
||||
|
||||
class SalesOrderView(StockChangeViewMixin, views.APIView):
|
||||
"""销售订单查询与创建"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = SalesOrderPagination
|
||||
|
||||
def get(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
queryset = business_models.SalesOrder.objects.filter(merchant=merchant).prefetch_related(
|
||||
'items', 'customer', 'operator', 'warehouse'
|
||||
)
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset.order_by('-created_at'), request, view=self)
|
||||
serializer = SalesOrderSerializer(page, many=True)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
data = request.data or {}
|
||||
|
||||
customer_id = data.get('customer')
|
||||
warehouse_id = data.get('warehouse_id') or data.get('warehouse')
|
||||
order_date = data.get('order_date')
|
||||
items = data.get('items', [])
|
||||
remarks = data.get('remarks', '')
|
||||
|
||||
if not customer_id:
|
||||
return Response({'error': '缺少客户 ID'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not warehouse_id:
|
||||
return Response({'error': '缺少仓库 ID'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not order_date:
|
||||
return Response({'error': '缺少 order_date'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not isinstance(items, list) or not items:
|
||||
return Response({'error': 'items 需要为非空数组'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
customer = basic_models.Customer.objects.get(id=customer_id, merchant=merchant)
|
||||
except basic_models.Customer.DoesNotExist:
|
||||
return Response({'error': f'客户 {customer_id} 不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
warehouse = basic_models.WareHouse.objects.get(id=warehouse_id, merchant=merchant)
|
||||
except basic_models.WareHouse.DoesNotExist:
|
||||
return Response({'error': f'仓库 {warehouse_id} 不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
operator = request.user.employee
|
||||
|
||||
try:
|
||||
sales_order = business_services.create_sales_order(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
order_date=order_date,
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=items,
|
||||
remarks=remarks,
|
||||
created_by=request.user,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'id': sales_order.id,
|
||||
'status': sales_order.status,
|
||||
'message': '销售单创建成功,等待审批',
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class SalesOrderReviewSerializer(serializers.Serializer):
|
||||
action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')])
|
||||
|
||||
|
||||
class SalesOrderReviewView(StockChangeViewMixin, views.APIView):
|
||||
"""销售单审批 / 作废"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
ACTION_STATUS_MAP = {
|
||||
'approve': business_models.SalesOrderStatusEnum.APPROVED,
|
||||
'cancel': business_models.SalesOrderStatusEnum.CANCELLED,
|
||||
}
|
||||
|
||||
def post(self, request, pk: int):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
try:
|
||||
sales_order = business_models.SalesOrder.objects.select_related(
|
||||
'customer', 'operator', 'warehouse'
|
||||
).prefetch_related('items').get(id=pk, merchant=merchant)
|
||||
except business_models.SalesOrder.DoesNotExist:
|
||||
return self.not_found_response('销售单不存在')
|
||||
|
||||
serializer = SalesOrderReviewSerializer(data=request.data or {})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
action = serializer.validated_data['action']
|
||||
target_status = self.ACTION_STATUS_MAP[action]
|
||||
|
||||
try:
|
||||
business_services.review_sales_order(
|
||||
sales_order=sales_order,
|
||||
target_status=target_status,
|
||||
reviewed_by=request.user,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
refreshed_order = business_models.SalesOrder.objects.select_related(
|
||||
'customer', 'operator', 'warehouse'
|
||||
).prefetch_related('items').get(id=sales_order.id)
|
||||
return Response(SalesOrderSerializer(refreshed_order).data, status=status.HTTP_200_OK)
|
||||
|
||||
Reference in New Issue
Block a user