1
0
forked from erp-dev/erp

feat: added put/patch api to main oobject of business module

This commit is contained in:
2025-12-12 14:45:51 +08:00
parent 1a8c446365
commit 9e1f994416
14 changed files with 11611 additions and 0 deletions

View File

@@ -285,6 +285,38 @@ class PurchaseOrderAPITestCase(TestCase):
order = business_models.PurchaseOrder.objects.get(id=order_id) order = business_models.PurchaseOrder.objects.get(id=order_id)
self.assertEqual(order.status, business_models.PurchaseOrderStatusEnum.PENDING) self.assertEqual(order.status, business_models.PurchaseOrderStatusEnum.PENDING)
def test_update_purchase_order_success(self):
order_id = self._create_purchase_order(self.strict_payload)
payload = {
'supplier': self.supplier.id,
'warehouse': self.warehouse_relaxed.id,
'order_date': '2025-12-01',
'items': [
{
'product_id': self.product.id,
'quantity': 200,
'num_of_rolls': 4,
'price': '12.88',
}
],
'remarks': '更新采购单',
}
response = self.client.put(f'/api/v1/purchase-orders/{order_id}/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['remarks'], '更新采购单')
def test_update_purchase_order_rejects_non_pending(self):
order_id = self._create_purchase_order(self.relaxed_payload)
self.client.post(
f'/api/v1/purchase-orders/{order_id}/review/',
{'action': 'approve'},
format='json',
)
payload = copy.deepcopy(self.relaxed_payload)
response = self.client.put(f'/api/v1/purchase-orders/{order_id}/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('不可修改', response.data['error'])
@override_settings( @override_settings(
CELERY_TASK_ALWAYS_EAGER=True, CELERY_TASK_ALWAYS_EAGER=True,
@@ -432,6 +464,51 @@ class SalesOrderAPITestCase(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('consume_detail_ids', response.data['error']) self.assertIn('consume_detail_ids', response.data['error'])
def test_update_sales_order_success(self):
order_id = self._create_sales_order(self.strict_in_payload)
payload = {
'customer': self.customer.id,
'warehouse': self.warehouse_relaxed.id,
'order_date': '2025-12-01',
'items': [
{
'product_id': self.product.id,
'quantity': 100,
'num_of_rolls': 2,
'price': '22.5',
}
],
'remarks': '更新后的销售单',
}
response = self.client.put(f'/api/v1/sales-orders/{order_id}/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['remarks'], '更新后的销售单')
self.assertEqual(response.data['quantity_of_rolls'], [[]])
def test_update_sales_order_rejects_non_pending(self):
order_id = self._create_sales_order(self.strict_in_payload)
self.client.post(
f'/api/v1/sales-orders/{order_id}/review/',
{'action': 'approve'},
format='json',
)
payload = {
'customer': self.customer.id,
'warehouse': self.warehouse_relaxed.id,
'order_date': '2025-12-02',
'items': [
{
'product_id': self.product.id,
'quantity': 10,
'num_of_rolls': 1,
'price': '30',
}
],
}
response = self.client.put(f'/api/v1/sales-orders/{order_id}/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('不可修改', response.data['error'])
def test_create_sales_order_with_printing_job_binding(self): def test_create_sales_order_with_printing_job_binding(self):
payload = copy.deepcopy(self.relaxed_payload) payload = copy.deepcopy(self.relaxed_payload)
payload['items'][0]['printing_job'] = self.printing_job_for_sales.id payload['items'][0]['printing_job'] = self.printing_job_for_sales.id
@@ -598,6 +675,38 @@ class PurchaseReturnOrderAPITestCase(TestCase):
order = business_models.PurchaseReturnOrder.objects.get(id=order_id) order = business_models.PurchaseReturnOrder.objects.get(id=order_id)
self.assertEqual(order.status, business_models.PurchaseReturnStatusEnum.PENDING) self.assertEqual(order.status, business_models.PurchaseReturnStatusEnum.PENDING)
def test_update_purchase_return_order_success(self):
order_id = self._create_purchase_return_order(self.strict_payload)
payload = {
'supplier': self.supplier.id,
'warehouse': self.warehouse_relaxed.id,
'return_date': '2025-12-05',
'items': [
{
'product_id': self.product.id,
'quantity': 50,
'num_of_rolls': 2,
'price': '12.0',
}
],
'remarks': '更新采退',
}
response = self.client.put(f'/api/v1/purchase-return-orders/{order_id}/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['remarks'], '更新采退')
def test_update_purchase_return_order_rejects_non_pending(self):
order_id = self._create_purchase_return_order(self.strict_payload)
self.client.post(
f'/api/v1/purchase-return-orders/{order_id}/review/',
{'action': 'approve'},
format='json',
)
payload = copy.deepcopy(self.strict_payload)
response = self.client.put(f'/api/v1/purchase-return-orders/{order_id}/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('不可修改', response.data['error'])
@override_settings( @override_settings(
CELERY_TASK_ALWAYS_EAGER=True, CELERY_TASK_ALWAYS_EAGER=True,
@@ -714,6 +823,50 @@ class SalesReturnOrderAPITestCase(TestCase):
order = business_models.SalesReturnOrder.objects.get(id=order_id) order = business_models.SalesReturnOrder.objects.get(id=order_id)
self.assertEqual(order.status, business_models.SalesReturnStatusEnum.PENDING) self.assertEqual(order.status, business_models.SalesReturnStatusEnum.PENDING)
def test_update_sales_return_order_success(self):
order_id = self._create_sales_return_order(self.strict_payload)
payload = {
'customer': self.customer.id,
'warehouse': self.warehouse_relaxed.id,
'return_date': '2025-12-02',
'items': [
{
'product_id': self.product.id,
'quantity': 40,
'num_of_rolls': 2,
'price': '18.0',
}
],
'remarks': '更新销退',
}
response = self.client.put(f'/api/v1/sales-return-orders/{order_id}/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['remarks'], '更新销退')
def test_update_sales_return_order_rejects_non_pending(self):
order_id = self._create_sales_return_order(self.strict_payload)
self.client.post(
f'/api/v1/sales-return-orders/{order_id}/review/',
{'action': 'approve'},
format='json',
)
payload = {
'customer': self.customer.id,
'warehouse': self.warehouse_relaxed.id,
'return_date': '2025-12-03',
'items': [
{
'product_id': self.product.id,
'quantity': 20,
'num_of_rolls': 1,
'price': '17',
}
],
}
response = self.client.put(f'/api/v1/sales-return-orders/{order_id}/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('不可修改', response.data['error'])
@override_settings( @override_settings(
CELERY_TASK_ALWAYS_EAGER=True, CELERY_TASK_ALWAYS_EAGER=True,

View File

@@ -67,12 +67,16 @@ urlpatterns = [
# 库存查询 API # 库存查询 API
path('inventory/', inventory.InventoryAPIView.as_view(), name='inventory'), path('inventory/', inventory.InventoryAPIView.as_view(), name='inventory'),
path('purchase-orders/', purchase_views.PurchaseOrderView.as_view(), name='purchase_orders'), path('purchase-orders/', purchase_views.PurchaseOrderView.as_view(), name='purchase_orders'),
path('purchase-orders/<int:pk>/', purchase_views.PurchaseOrderDetailView.as_view(), name='purchase_order_detail'),
path('purchase-orders/<int:pk>/review/', purchase_views.PurchaseOrderReviewView.as_view(), name='purchase_order_review'), path('purchase-orders/<int:pk>/review/', purchase_views.PurchaseOrderReviewView.as_view(), name='purchase_order_review'),
path('purchase-return-orders/', purchase_return_views.PurchaseReturnOrderView.as_view(), name='purchase_return_orders'), path('purchase-return-orders/', purchase_return_views.PurchaseReturnOrderView.as_view(), name='purchase_return_orders'),
path('purchase-return-orders/<int:pk>/', purchase_return_views.PurchaseReturnOrderDetailView.as_view(), name='purchase_return_order_detail'),
path('purchase-return-orders/<int:pk>/review/', purchase_return_views.PurchaseReturnOrderReviewView.as_view(), name='purchase_return_order_review'), path('purchase-return-orders/<int:pk>/review/', purchase_return_views.PurchaseReturnOrderReviewView.as_view(), name='purchase_return_order_review'),
path('sales-orders/', sales_views.SalesOrderView.as_view(), name='sales_orders'), path('sales-orders/', sales_views.SalesOrderView.as_view(), name='sales_orders'),
path('sales-orders/<int:pk>/', sales_views.SalesOrderDetailView.as_view(), name='sales_order_detail'),
path('sales-orders/<int:pk>/review/', sales_views.SalesOrderReviewView.as_view(), name='sales_order_review'), path('sales-orders/<int:pk>/review/', sales_views.SalesOrderReviewView.as_view(), name='sales_order_review'),
path('sales-return-orders/', sales_return_views.SalesReturnOrderView.as_view(), name='sales_return_orders'), path('sales-return-orders/', sales_return_views.SalesReturnOrderView.as_view(), name='sales_return_orders'),
path('sales-return-orders/<int:pk>/', sales_return_views.SalesReturnOrderDetailView.as_view(), name='sales_return_order_detail'),
path('sales-return-orders/<int:pk>/review/', sales_return_views.SalesReturnOrderReviewView.as_view(), name='sales_return_order_review'), path('sales-return-orders/<int:pk>/review/', sales_return_views.SalesReturnOrderReviewView.as_view(), name='sales_return_order_review'),
path('payment-orders/', payment_views.PaymentOrderView.as_view(), name='payment_orders'), path('payment-orders/', payment_views.PaymentOrderView.as_view(), name='payment_orders'),
path('payment-orders/<int:pk>/review/', payment_views.PaymentOrderReviewView.as_view(), name='payment_order_review'), path('payment-orders/<int:pk>/review/', payment_views.PaymentOrderReviewView.as_view(), name='payment_order_review'),

View File

@@ -27,6 +27,7 @@ class PurchaseOrderSerializer(serializers.ModelSerializer):
total_amount = serializers.SerializerMethodField(read_only=True) total_amount = serializers.SerializerMethodField(read_only=True)
diff_quantity = serializers.SerializerMethodField(read_only=True) diff_quantity = serializers.SerializerMethodField(read_only=True)
total_quantity = serializers.SerializerMethodField(read_only=True) total_quantity = serializers.SerializerMethodField(read_only=True)
quantity_of_rolls = serializers.SerializerMethodField(read_only=True)
def get_total_amount(self, obj: business_models.PurchaseOrder): def get_total_amount(self, obj: business_models.PurchaseOrder):
return obj.get_total_amount() return obj.get_total_amount()
@@ -37,6 +38,9 @@ class PurchaseOrderSerializer(serializers.ModelSerializer):
def get_total_quantity(self, obj: business_models.PurchaseOrder): def get_total_quantity(self, obj: business_models.PurchaseOrder):
return obj.get_total_quantity() return obj.get_total_quantity()
def get_quantity_of_rolls(self, obj: business_models.PurchaseOrder):
return [item.split_quantity_of_rolls() for item in obj.items.all()]
supplier_name = serializers.CharField(source='supplier.name', read_only=True) supplier_name = serializers.CharField(source='supplier.name', read_only=True)
operator_name = serializers.CharField(source='operator.name', read_only=True) operator_name = serializers.CharField(source='operator.name', read_only=True)
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
@@ -49,6 +53,7 @@ class PurchaseOrderSerializer(serializers.ModelSerializer):
'total_amount', 'diff_quantity', 'total_quantity', 'total_amount', 'diff_quantity', 'total_quantity',
'operator', 'operator_name', 'warehouse', 'warehouse_name', 'operator', 'operator_name', 'warehouse', 'warehouse_name',
'status', 'remarks', 'created_at', 'updated_at', 'items', 'status', 'remarks', 'created_at', 'updated_at', 'items',
'quantity_of_rolls',
] ]
read_only_fields = ['id', 'created_at', 'updated_at', 'items', 'supplier_name', 'operator_name', 'warehouse_name'] read_only_fields = ['id', 'created_at', 'updated_at', 'items', 'supplier_name', 'operator_name', 'warehouse_name']
@@ -135,6 +140,73 @@ class PurchaseOrderView(StockChangeViewMixin, views.APIView):
) )
class PurchaseOrderDetailView(StockChangeViewMixin, views.APIView):
permission_classes = [IsAuthenticated]
def _get_order(self, request, pk: int):
if not self.check_employee_permission(request):
return None, self.permission_error_response('无权限访问')
merchant = request.user.employee.merchant
try:
order = business_models.PurchaseOrder.objects.select_related(
'supplier', 'operator', 'warehouse'
).prefetch_related('items').get(id=pk, merchant=merchant)
return order, None
except business_models.PurchaseOrder.DoesNotExist:
return None, self.not_found_response('采购单不存在')
def put(self, request, pk: int):
return self._update(request, pk)
def patch(self, request, pk: int):
return self._update(request, pk)
def _update(self, request, pk: int):
purchase_order, error_response = self._get_order(request, pk)
if error_response:
return error_response
merchant = request.user.employee.merchant
data = request.data or {}
supplier = purchase_order.supplier
supplier_id = data.get('supplier')
if supplier_id:
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)
warehouse = purchase_order.warehouse
warehouse_id = data.get('warehouse_id') or data.get('warehouse')
if warehouse_id:
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)
order_date = data.get('order_date') or purchase_order.purchase_date
items = data.get('items')
if not isinstance(items, list) or not items:
return Response({'error': 'items 需要为非空数组'}, status=status.HTTP_400_BAD_REQUEST)
remarks = data.get('remarks', purchase_order.remarks)
try:
updated_order = business_services.update_purchase_order(
purchase_order=purchase_order,
supplier=supplier,
order_date=order_date,
warehouse=warehouse,
items=items,
remarks=remarks,
)
except ValueError as exc:
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
serializer = PurchaseOrderSerializer(updated_order)
return Response(serializer.data, status=status.HTTP_200_OK)
class PurchaseOrderReviewSerializer(serializers.Serializer): class PurchaseOrderReviewSerializer(serializers.Serializer):
action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')]) action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')])

View File

@@ -32,6 +32,7 @@ class PurchaseReturnOrderSerializer(serializers.ModelSerializer):
total_amount = serializers.SerializerMethodField(read_only=True) total_amount = serializers.SerializerMethodField(read_only=True)
diff_quantity = serializers.SerializerMethodField(read_only=True) diff_quantity = serializers.SerializerMethodField(read_only=True)
total_quantity = serializers.SerializerMethodField(read_only=True) total_quantity = serializers.SerializerMethodField(read_only=True)
quantity_of_rolls = serializers.SerializerMethodField(read_only=True)
supplier_name = serializers.CharField(source='supplier.name', read_only=True) supplier_name = serializers.CharField(source='supplier.name', read_only=True)
operator_name = serializers.CharField(source='operator.name', read_only=True) operator_name = serializers.CharField(source='operator.name', read_only=True)
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
@@ -46,6 +47,9 @@ class PurchaseReturnOrderSerializer(serializers.ModelSerializer):
def get_total_quantity(self, obj: business_models.PurchaseReturnOrder): def get_total_quantity(self, obj: business_models.PurchaseReturnOrder):
return obj.get_total_quantity() return obj.get_total_quantity()
def get_quantity_of_rolls(self, obj: business_models.PurchaseReturnOrder):
return [item.split_quantity_of_rolls() for item in obj.items.all()]
class Meta: class Meta:
model = business_models.PurchaseReturnOrder model = business_models.PurchaseReturnOrder
fields = [ fields = [
@@ -53,6 +57,7 @@ class PurchaseReturnOrderSerializer(serializers.ModelSerializer):
'total_amount', 'diff_quantity', 'total_quantity', 'total_amount', 'diff_quantity', 'total_quantity',
'operator', 'operator_name', 'warehouse', 'warehouse_name', 'operator', 'operator_name', 'warehouse', 'warehouse_name',
'status', 'remarks', 'created_at', 'updated_at', 'items', 'status', 'remarks', 'created_at', 'updated_at', 'items',
'quantity_of_rolls',
] ]
read_only_fields = [ read_only_fields = [
'id', 'created_at', 'updated_at', 'items', 'id', 'created_at', 'updated_at', 'items',
@@ -142,6 +147,75 @@ class PurchaseReturnOrderView(StockChangeViewMixin, views.APIView):
) )
class PurchaseReturnOrderDetailView(StockChangeViewMixin, views.APIView):
permission_classes = [IsAuthenticated]
def _get_order(self, request, pk: int):
if not self.check_employee_permission(request):
return None, self.permission_error_response('无权限访问')
merchant = request.user.employee.merchant
try:
order = business_models.PurchaseReturnOrder.objects.select_related(
'supplier', 'operator', 'warehouse'
).prefetch_related('items').get(id=pk, merchant=merchant)
return order, None
except business_models.PurchaseReturnOrder.DoesNotExist:
return None, self.not_found_response('采购退货单不存在')
def put(self, request, pk: int):
return self._update(request, pk)
def patch(self, request, pk: int):
return self._update(request, pk)
def _update(self, request, pk: int):
purchase_return_order, error_response = self._get_order(request, pk)
if error_response:
return error_response
merchant = request.user.employee.merchant
data = request.data or {}
supplier = purchase_return_order.supplier
supplier_id = data.get('supplier')
if supplier_id:
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)
warehouse = purchase_return_order.warehouse
warehouse_id = data.get('warehouse_id') or data.get('warehouse')
if warehouse_id:
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)
return_date = data.get('return_date') or purchase_return_order.return_date
items = data.get('items')
if not isinstance(items, list) or not items:
return Response({'error': 'items 需要为非空数组'}, status=status.HTTP_400_BAD_REQUEST)
remarks = data.get('remarks', purchase_return_order.remarks)
purchase_order_id = data.get('purchase_order')
try:
updated_order = business_services.update_purchase_return_order(
purchase_return_order=purchase_return_order,
supplier=supplier,
return_date=return_date,
warehouse=warehouse,
items=items,
remarks=remarks,
purchase_order_id=purchase_order_id,
)
except ValueError as exc:
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
serializer = PurchaseReturnOrderSerializer(updated_order)
return Response(serializer.data, status=status.HTTP_200_OK)
class PurchaseReturnOrderReviewSerializer(serializers.Serializer): class PurchaseReturnOrderReviewSerializer(serializers.Serializer):
action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')]) action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')])

View File

@@ -27,6 +27,7 @@ class SalesOrderSerializer(serializers.ModelSerializer):
total_amount = serializers.SerializerMethodField(read_only=True) total_amount = serializers.SerializerMethodField(read_only=True)
diff_quantity = serializers.SerializerMethodField(read_only=True) diff_quantity = serializers.SerializerMethodField(read_only=True)
total_quantity = serializers.SerializerMethodField(read_only=True) total_quantity = serializers.SerializerMethodField(read_only=True)
quantity_of_rolls = serializers.SerializerMethodField(read_only=True)
def get_total_amount(self, obj: business_models.SalesOrder): def get_total_amount(self, obj: business_models.SalesOrder):
return obj.get_total_amount() return obj.get_total_amount()
@@ -37,6 +38,9 @@ class SalesOrderSerializer(serializers.ModelSerializer):
def get_total_quantity(self, obj: business_models.SalesOrder): def get_total_quantity(self, obj: business_models.SalesOrder):
return obj.get_total_quantity() return obj.get_total_quantity()
def get_quantity_of_rolls(self, obj: business_models.SalesOrder):
return [item.split_quantity_of_rolls() for item in obj.items.all()]
customer_name = serializers.CharField(source='customer.name', read_only=True) customer_name = serializers.CharField(source='customer.name', read_only=True)
operator_name = serializers.CharField(source='operator.name', read_only=True) operator_name = serializers.CharField(source='operator.name', read_only=True)
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
@@ -49,6 +53,7 @@ class SalesOrderSerializer(serializers.ModelSerializer):
'total_amount', 'diff_quantity', 'total_quantity', 'total_amount', 'diff_quantity', 'total_quantity',
'operator', 'operator_name', 'warehouse', 'warehouse_name', 'operator', 'operator_name', 'warehouse', 'warehouse_name',
'status', 'remarks', 'created_at', 'updated_at', 'items', 'status', 'remarks', 'created_at', 'updated_at', 'items',
'quantity_of_rolls',
] ]
read_only_fields = ['id', 'created_at', 'updated_at', 'items', 'customer_name', 'operator_name', 'warehouse_name'] read_only_fields = ['id', 'created_at', 'updated_at', 'items', 'customer_name', 'operator_name', 'warehouse_name']
@@ -135,6 +140,73 @@ class SalesOrderView(StockChangeViewMixin, views.APIView):
) )
class SalesOrderDetailView(StockChangeViewMixin, views.APIView):
permission_classes = [IsAuthenticated]
def _get_order(self, request, pk: int):
if not self.check_employee_permission(request):
return None, self.permission_error_response('无权限访问')
merchant = request.user.employee.merchant
try:
order = business_models.SalesOrder.objects.select_related(
'customer', 'operator', 'warehouse'
).prefetch_related('items').get(id=pk, merchant=merchant)
return order, None
except business_models.SalesOrder.DoesNotExist:
return None, self.not_found_response('销售单不存在')
def put(self, request, pk: int):
return self._update(request, pk)
def patch(self, request, pk: int):
return self._update(request, pk)
def _update(self, request, pk: int):
sales_order, error_response = self._get_order(request, pk)
if error_response:
return error_response
merchant = request.user.employee.merchant
data = request.data or {}
customer = sales_order.customer
customer_id = data.get('customer')
if customer_id:
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)
warehouse = sales_order.warehouse
warehouse_id = data.get('warehouse_id') or data.get('warehouse')
if warehouse_id:
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)
order_date = data.get('order_date') or sales_order.sales_date
items = data.get('items')
if not isinstance(items, list) or not items:
return Response({'error': 'items 需要为非空数组'}, status=status.HTTP_400_BAD_REQUEST)
remarks = data.get('remarks', sales_order.remarks)
try:
updated_order = business_services.update_sales_order(
sales_order=sales_order,
customer=customer,
order_date=order_date,
warehouse=warehouse,
items=items,
remarks=remarks,
)
except ValueError as exc:
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
serializer = SalesOrderSerializer(updated_order)
return Response(serializer.data, status=status.HTTP_200_OK)
class SalesOrderReviewSerializer(serializers.Serializer): class SalesOrderReviewSerializer(serializers.Serializer):
action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')]) action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')])

View File

@@ -31,6 +31,7 @@ class SalesReturnOrderSerializer(serializers.ModelSerializer):
total_amount = serializers.SerializerMethodField(read_only=True) total_amount = serializers.SerializerMethodField(read_only=True)
diff_quantity = serializers.SerializerMethodField(read_only=True) diff_quantity = serializers.SerializerMethodField(read_only=True)
total_quantity = serializers.SerializerMethodField(read_only=True) total_quantity = serializers.SerializerMethodField(read_only=True)
quantity_of_rolls = serializers.SerializerMethodField(read_only=True)
customer_name = serializers.CharField(source='customer.name', read_only=True) customer_name = serializers.CharField(source='customer.name', read_only=True)
operator_name = serializers.CharField(source='operator.name', read_only=True) operator_name = serializers.CharField(source='operator.name', read_only=True)
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
@@ -45,6 +46,9 @@ class SalesReturnOrderSerializer(serializers.ModelSerializer):
def get_total_quantity(self, obj: business_models.SalesReturnOrder): def get_total_quantity(self, obj: business_models.SalesReturnOrder):
return obj.get_total_quantity() return obj.get_total_quantity()
def get_quantity_of_rolls(self, obj: business_models.SalesReturnOrder):
return [item.split_quantity_of_rolls() for item in obj.items.all()]
class Meta: class Meta:
model = business_models.SalesReturnOrder model = business_models.SalesReturnOrder
fields = [ fields = [
@@ -141,6 +145,75 @@ class SalesReturnOrderView(StockChangeViewMixin, views.APIView):
) )
class SalesReturnOrderDetailView(StockChangeViewMixin, views.APIView):
permission_classes = [IsAuthenticated]
def _get_order(self, request, pk: int):
if not self.check_employee_permission(request):
return None, self.permission_error_response('无权限访问')
merchant = request.user.employee.merchant
try:
order = business_models.SalesReturnOrder.objects.select_related(
'customer', 'operator', 'warehouse'
).prefetch_related('items').get(id=pk, merchant=merchant)
return order, None
except business_models.SalesReturnOrder.DoesNotExist:
return None, self.not_found_response('销售退货单不存在')
def put(self, request, pk: int):
return self._update(request, pk)
def patch(self, request, pk: int):
return self._update(request, pk)
def _update(self, request, pk: int):
sales_return_order, error_response = self._get_order(request, pk)
if error_response:
return error_response
merchant = request.user.employee.merchant
data = request.data or {}
customer = sales_return_order.customer
customer_id = data.get('customer')
if customer_id:
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)
warehouse = sales_return_order.warehouse
warehouse_id = data.get('warehouse_id') or data.get('warehouse')
if warehouse_id:
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)
return_date = data.get('return_date') or sales_return_order.return_date
items = data.get('items')
if not isinstance(items, list) or not items:
return Response({'error': 'items 需要为非空数组'}, status=status.HTTP_400_BAD_REQUEST)
remarks = data.get('remarks', sales_return_order.remarks)
sales_order_id = data.get('sales_order')
try:
updated_order = business_services.update_sales_return_order(
sales_return_order=sales_return_order,
customer=customer,
return_date=return_date,
warehouse=warehouse,
items=items,
remarks=remarks,
sales_order_id=sales_order_id,
)
except ValueError as exc:
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
serializer = SalesReturnOrderSerializer(updated_order)
return Response(serializer.data, status=status.HTTP_200_OK)
class SalesReturnOrderReviewSerializer(serializers.Serializer): class SalesReturnOrderReviewSerializer(serializers.Serializer):
action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')]) action = serializers.ChoiceField(choices=[('approve', '审批通过'), ('cancel', '作废')])

View File

@@ -237,6 +237,69 @@ def create_purchase_order(
return purchase_order return purchase_order
def update_purchase_order(
*,
purchase_order: models.PurchaseOrder,
supplier: basic_info_models.Supplier | None = None,
order_date=None,
warehouse: basic_info_models.WareHouse | None = None,
items: List[Dict[str, Any]] | None = None,
remarks: str | None = '',
) -> models.PurchaseOrder:
"""
更新采购订单(仅限审批中状态)。
"""
_ensure_order_pending(purchase_order, models.PurchaseOrderStatusEnum.PENDING, '采购单')
merchant = purchase_order.merchant
new_supplier = supplier or purchase_order.supplier
new_warehouse = warehouse or purchase_order.warehouse
new_order_date = _normalize_order_date(order_date or purchase_order.purchase_date)
remarks = remarks if remarks is not None else purchase_order.remarks
if not items:
raise ValueError('items 需要为非空数组')
normalized_items, _ = _normalize_order_items(
merchant=merchant,
warehouse=new_warehouse,
items=items,
is_outgoing=False,
)
with transaction.atomic():
purchase_order.supplier = new_supplier
purchase_order.purchase_date = new_order_date
purchase_order.warehouse = new_warehouse
purchase_order.remarks = remarks
purchase_order.save(
update_fields=['supplier', 'purchase_date', 'warehouse', 'remarks', 'updated_at']
)
purchase_order.items.all().delete()
bulk_objects = [
models.PurchaseOrderItem(
purchase_order=purchase_order,
product=item_data['product'],
price=item_data['price'],
color=item_data.get('color'),
quantity=item_data['quantity'],
unit=item_data['unit'],
spec=item_data.get('spec'),
empty_diff_percent=item_data['empty_diff_percent'],
quantity_of_rolls=item_data.get('quantity_of_rolls'),
num_of_rolls=item_data['num_of_rolls'],
batch_number=item_data.get('batch_number'),
remarks=item_data.get('remarks'),
)
for item_data in normalized_items
]
models.PurchaseOrderItem.objects.bulk_create(bulk_objects)
purchase_order.refresh_from_db()
return purchase_order
def create_sales_order( def create_sales_order(
*, *,
merchant: basic_info_models.Merchant, merchant: basic_info_models.Merchant,
@@ -297,6 +360,75 @@ def create_sales_order(
return sales_order return sales_order
def update_sales_order(
*,
sales_order: models.SalesOrder,
customer: basic_info_models.Customer | None = None,
order_date=None,
warehouse: basic_info_models.WareHouse | None = None,
operator: basic_info_models.Employee | None = None,
items: List[Dict[str, Any]] | None = None,
remarks: str | None = '',
) -> models.SalesOrder:
"""
更新销售订单(仅限审批中状态)。
"""
_ensure_order_pending(sales_order, models.SalesOrderStatusEnum.PENDING, '销售单')
merchant = sales_order.merchant
new_customer = customer or sales_order.customer
new_warehouse = warehouse or sales_order.warehouse
new_operator = operator or sales_order.operator
new_order_date = _normalize_order_date(order_date or sales_order.sales_date)
remarks = remarks if remarks is not None else sales_order.remarks
if not items:
raise ValueError('items 需要为非空数组')
sales_items, stock_flow_items = _normalize_order_items(
merchant=merchant,
warehouse=new_warehouse,
items=items,
is_outgoing=True,
customer=new_customer,
)
with transaction.atomic():
sales_order.customer = new_customer
sales_order.sales_date = new_order_date
sales_order.warehouse = new_warehouse
sales_order.operator = new_operator
sales_order.remarks = remarks
sales_order.save(
update_fields=['customer', 'sales_date', 'warehouse', 'operator', 'remarks', 'updated_at']
)
sales_order.items.all().delete()
bulk_objects = [
models.SalesOrderItem(
sales_order=sales_order,
product=item_data['product'],
price=item_data['price'],
color=item_data.get('color'),
quantity=item_data['quantity'],
unit=item_data['unit'],
spec=item_data.get('spec'),
empty_diff_percent=item_data['empty_diff_percent'],
quantity_of_rolls=item_data.get('quantity_of_rolls'),
num_of_rolls=item_data['num_of_rolls'],
consume_detail_ids=item_data.get('consume_detail_ids'),
batch_number=item_data.get('batch_number'),
remarks=item_data.get('remarks'),
printing_job=item_data.get('printing_job'),
)
for item_data in sales_items
]
models.SalesOrderItem.objects.bulk_create(bulk_objects)
sales_order.refresh_from_db()
return sales_order
def create_purchase_return_order( def create_purchase_return_order(
*, *,
merchant: basic_info_models.Merchant, merchant: basic_info_models.Merchant,
@@ -369,6 +501,84 @@ def create_purchase_return_order(
return return_order return return_order
def update_purchase_return_order(
*,
purchase_return_order: models.PurchaseReturnOrder,
supplier: basic_info_models.Supplier | None = None,
return_date=None,
warehouse: basic_info_models.WareHouse | None = None,
items: List[Dict[str, Any]] | None = None,
remarks: str | None = '',
purchase_order: models.PurchaseOrder | None = None,
purchase_order_id: int | None = None,
) -> models.PurchaseReturnOrder:
"""
更新采购退货单(仅限审批中状态)。
"""
_ensure_order_pending(purchase_return_order, models.PurchaseReturnStatusEnum.PENDING, '采购退货单')
merchant = purchase_return_order.merchant
new_supplier = supplier or purchase_return_order.supplier
new_warehouse = warehouse or purchase_return_order.warehouse
new_return_date = _normalize_order_date(return_date or purchase_return_order.return_date)
remarks = remarks if remarks is not None else purchase_return_order.remarks
if not items:
raise ValueError('items 需要为非空数组')
resolved_purchase_order = purchase_order or purchase_return_order.purchase_order
if resolved_purchase_order is None and purchase_order_id:
resolved_purchase_order = models.PurchaseOrder.objects.filter(
id=purchase_order_id,
merchant=merchant,
).first()
if resolved_purchase_order is None:
raise ValueError('关联的采购单不存在或不属于当前商户')
if resolved_purchase_order and resolved_purchase_order.supplier_id != new_supplier.id:
raise ValueError('采购退货单的供应商必须与原采购单一致')
normalized_items, _ = _normalize_order_items(
merchant=merchant,
warehouse=new_warehouse,
items=items,
is_outgoing=True,
)
with transaction.atomic():
purchase_return_order.supplier = new_supplier
purchase_return_order.return_date = new_return_date
purchase_return_order.warehouse = new_warehouse
purchase_return_order.remarks = remarks
purchase_return_order.purchase_order = resolved_purchase_order
purchase_return_order.save(
update_fields=['supplier', 'return_date', 'warehouse', 'remarks', 'purchase_order', 'updated_at']
)
purchase_return_order.items.all().delete()
bulk_objects = [
models.PurchaseReturnOrderItem(
purchase_return_order=purchase_return_order,
product=item_data['product'],
price=item_data['price'],
color=item_data.get('color'),
quantity=item_data['quantity'],
unit=item_data['unit'],
spec=item_data.get('spec'),
empty_diff_percent=item_data['empty_diff_percent'],
quantity_of_rolls=item_data.get('quantity_of_rolls'),
num_of_rolls=item_data['num_of_rolls'],
consume_detail_ids=item_data.get('consume_detail_ids'),
batch_number=item_data.get('batch_number'),
remarks=item_data.get('remarks'),
)
for item_data in normalized_items
]
models.PurchaseReturnOrderItem.objects.bulk_create(bulk_objects)
purchase_return_order.refresh_from_db()
return purchase_return_order
def create_sales_return_order( def create_sales_return_order(
*, *,
merchant: basic_info_models.Merchant, merchant: basic_info_models.Merchant,
@@ -441,6 +651,85 @@ def create_sales_return_order(
return return_order return return_order
def update_sales_return_order(
*,
sales_return_order: models.SalesReturnOrder,
customer: basic_info_models.Customer | None = None,
return_date=None,
warehouse: basic_info_models.WareHouse | None = None,
items: List[Dict[str, Any]] | None = None,
remarks: str | None = '',
sales_order: models.SalesOrder | None = None,
sales_order_id: int | None = None,
) -> models.SalesReturnOrder:
"""
更新销售退货单(仅限审批中状态)。
"""
_ensure_order_pending(sales_return_order, models.SalesReturnStatusEnum.PENDING, '销售退货单')
merchant = sales_return_order.merchant
new_customer = customer or sales_return_order.customer
new_warehouse = warehouse or sales_return_order.warehouse
new_return_date = _normalize_order_date(return_date or sales_return_order.return_date)
remarks = remarks if remarks is not None else sales_return_order.remarks
if not items:
raise ValueError('items 需要为非空数组')
resolved_sales_order = sales_order or sales_return_order.sales_order
if resolved_sales_order is None and sales_order_id:
resolved_sales_order = models.SalesOrder.objects.filter(
id=sales_order_id,
merchant=merchant,
).first()
if resolved_sales_order is None:
raise ValueError('关联的销售单不存在或不属于当前商户')
if resolved_sales_order and resolved_sales_order.customer_id != new_customer.id:
raise ValueError('销售退货单的客户必须与原销售单一致')
normalized_items, _ = _normalize_order_items(
merchant=merchant,
warehouse=new_warehouse,
items=items,
is_outgoing=False,
)
with transaction.atomic():
sales_return_order.customer = new_customer
sales_return_order.return_date = new_return_date
sales_return_order.warehouse = new_warehouse
sales_return_order.remarks = remarks
sales_return_order.sales_order = resolved_sales_order
sales_return_order.save(
update_fields=['customer', 'return_date', 'warehouse', 'remarks', 'sales_order', 'updated_at']
)
sales_return_order.items.all().delete()
bulk_objects = [
models.SalesReturnOrderItem(
sales_return_order=sales_return_order,
product=item_data['product'],
price=item_data['price'],
color=item_data.get('color'),
quantity=item_data['quantity'],
unit=item_data['unit'],
spec=item_data.get('spec'),
empty_diff_percent=item_data['empty_diff_percent'],
quantity_of_rolls=item_data.get('quantity_of_rolls'),
num_of_rolls=item_data['num_of_rolls'],
consume_detail_ids=item_data.get('consume_detail_ids'),
batch_number=item_data.get('batch_number'),
remarks=item_data.get('remarks'),
)
for item_data in normalized_items
]
models.SalesReturnOrderItem.objects.bulk_create(bulk_objects)
sales_return_order.refresh_from_db()
return sales_return_order
def create_payment_order( def create_payment_order(
*, *,
merchant: basic_info_models.Merchant, merchant: basic_info_models.Merchant,
@@ -1439,6 +1728,11 @@ def _ensure_non_zero_amount(value, field_name: str) -> Decimal:
return amount return amount
def _ensure_order_pending(order, pending_status, entity_name: str):
if getattr(order, 'status', None) != pending_status:
raise ValueError(f'{entity_name}当前状态不可修改')
def _ensure_non_negative_amount(value, field_name: str) -> Decimal: def _ensure_non_negative_amount(value, field_name: str) -> Decimal:
if value in (None, ''): if value in (None, ''):
return Decimal('0') return Decimal('0')

View File

@@ -279,6 +279,44 @@ class PurchaseOrderServiceTestCase(TestCase):
created_by_id=self.user.id, created_by_id=self.user.id,
) )
def test_update_purchase_order_success(self):
purchase_order = services.create_purchase_order(
merchant=self.merchant,
supplier=self.supplier,
order_date=timezone.now().date(),
warehouse=self.warehouse_relaxed,
operator=self.operator,
items=self.relaxed_items,
created_by=self.user,
)
updated = services.update_purchase_order(
purchase_order=purchase_order,
warehouse=self.warehouse_strict,
items=self.strict_items,
remarks='updated',
)
self.assertEqual(updated.remarks, 'updated')
self.assertEqual(updated.items.count(), 1)
self.assertEqual(updated.items.first().split_quantity_of_rolls(), [10, 5])
def test_update_purchase_order_rejects_non_pending(self):
purchase_order = services.create_purchase_order(
merchant=self.merchant,
supplier=self.supplier,
order_date=timezone.now().date(),
warehouse=self.warehouse_relaxed,
operator=self.operator,
items=self.relaxed_items,
created_by=self.user,
)
purchase_order.status = business_models.PurchaseOrderStatusEnum.APPROVED
purchase_order.save(update_fields=['status'])
with self.assertRaises(ValueError):
services.update_purchase_order(
purchase_order=purchase_order,
items=self.relaxed_items,
)
def test_purchase_order_relaxed_mode_even_split(self): def test_purchase_order_relaxed_mode_even_split(self):
purchase_order = services.create_purchase_order( purchase_order = services.create_purchase_order(
merchant=self.merchant, merchant=self.merchant,
@@ -463,6 +501,45 @@ class SalesOrderServiceTestCase(TestCase):
created_by=self.user, created_by=self.user,
) )
def test_update_sales_order_success(self):
sales_order = services.create_sales_order(
merchant=self.merchant,
customer=self.customer,
order_date=timezone.now().date(),
warehouse=self.warehouse_strict,
operator=self.operator,
items=self.strict_items,
created_by=self.user,
)
updated = services.update_sales_order(
sales_order=sales_order,
warehouse=self.warehouse_relaxed,
items=self.relaxed_items,
remarks='updated',
)
self.assertEqual(updated.remarks, 'updated')
self.assertEqual(updated.items.count(), 1)
item = updated.items.first()
self.assertEqual(item.split_quantity_of_rolls(), [])
def test_update_sales_order_rejects_non_pending(self):
sales_order = services.create_sales_order(
merchant=self.merchant,
customer=self.customer,
order_date=timezone.now().date(),
warehouse=self.warehouse_strict,
operator=self.operator,
items=self.strict_items,
created_by=self.user,
)
sales_order.status = business_models.SalesOrderStatusEnum.APPROVED
sales_order.save(update_fields=['status'])
with self.assertRaises(ValueError):
services.update_sales_order(
sales_order=sales_order,
items=self.strict_items,
)
class PurchaseReturnServiceTestCase(TestCase): class PurchaseReturnServiceTestCase(TestCase):
def setUp(self): def setUp(self):
@@ -581,6 +658,43 @@ class PurchaseReturnServiceTestCase(TestCase):
items=payload, items=payload,
) )
def test_update_purchase_return_order_success(self):
order = services.create_purchase_return_order(
merchant=self.merchant,
supplier=self.supplier,
return_date=timezone.now().date(),
warehouse=self.warehouse_strict,
operator=self.operator,
items=self.strict_items,
created_by=self.user,
)
updated = services.update_purchase_return_order(
purchase_return_order=order,
items=self.strict_items,
remarks='updated',
)
self.assertEqual(updated.remarks, 'updated')
self.assertEqual(updated.items.count(), 1)
self.assertEqual(updated.items.first().split_quantity_of_rolls(), [4, 2])
def test_update_purchase_return_order_rejects_non_pending(self):
order = services.create_purchase_return_order(
merchant=self.merchant,
supplier=self.supplier,
return_date=timezone.now().date(),
warehouse=self.warehouse_strict,
operator=self.operator,
items=self.strict_items,
created_by=self.user,
)
order.status = business_models.PurchaseReturnStatusEnum.APPROVED
order.save(update_fields=['status'])
with self.assertRaises(ValueError):
services.update_purchase_return_order(
purchase_return_order=order,
items=self.strict_items,
)
class SalesReturnServiceTestCase(TestCase): class SalesReturnServiceTestCase(TestCase):
def setUp(self): def setUp(self):
@@ -699,6 +813,43 @@ class SalesReturnServiceTestCase(TestCase):
quantities = sorted([detail.quantity for detail in record.details.order_by('id')], reverse=True) quantities = sorted([detail.quantity for detail in record.details.order_by('id')], reverse=True)
self.assertEqual(quantities, [Decimal('33.34'), Decimal('33.33'), Decimal('33.33')]) self.assertEqual(quantities, [Decimal('33.34'), Decimal('33.33'), Decimal('33.33')])
def test_update_sales_return_order_success(self):
order = services.create_sales_return_order(
merchant=self.merchant,
customer=self.customer,
return_date=timezone.now().date(),
warehouse=self.warehouse_strict,
operator=self.operator,
items=self.strict_items,
created_by=self.user,
)
updated = services.update_sales_return_order(
sales_return_order=order,
items=self.strict_items,
remarks='updated',
)
self.assertEqual(updated.remarks, 'updated')
self.assertEqual(updated.items.count(), 1)
self.assertEqual(updated.items.first().split_quantity_of_rolls(), [6, 2])
def test_update_sales_return_order_rejects_non_pending(self):
order = services.create_sales_return_order(
merchant=self.merchant,
customer=self.customer,
return_date=timezone.now().date(),
warehouse=self.warehouse_strict,
operator=self.operator,
items=self.strict_items,
created_by=self.user,
)
order.status = business_models.SalesReturnStatusEnum.APPROVED
order.save(update_fields=['status'])
with self.assertRaises(ValueError):
services.update_sales_return_order(
sales_return_order=order,
items=self.strict_items,
)
class PaymentReceiptServiceTestCase(TestCase): class PaymentReceiptServiceTestCase(TestCase):
def setUp(self): def setUp(self):
( (

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@
|-----|------|------| |-----|------|------|
| `/purchase-orders/` | GET | 分页列表。 | | `/purchase-orders/` | GET | 分页列表。 |
| `/purchase-orders/` | POST | 创建采购单。 | | `/purchase-orders/` | POST | 创建采购单。 |
| `/purchase-orders/<id>/` | PUT/PATCH | 编辑采购单(仅限审批中)。 |
| `/purchase-orders/<id>/review/` | POST | 审批或作废。 | | `/purchase-orders/<id>/review/` | POST | 审批或作废。 |
### 2.1 创建请求体 ### 2.1 创建请求体
@@ -75,6 +76,7 @@
| API | 方法 | 描述 | | API | 方法 | 描述 |
|-----|------|------| |-----|------|------|
| `/purchase-return-orders/` | GET/POST | 与采购单相同的查询/创建接口,字段改为 `return_date`。 | | `/purchase-return-orders/` | GET/POST | 与采购单相同的查询/创建接口,字段改为 `return_date`。 |
| `/purchase-return-orders/<id>/` | PUT/PATCH | 编辑采购退货单(仅限审批中)。 |
| `/purchase-return-orders/<id>/review/` | POST | 审批或作废,流程与采购单一致。 | | `/purchase-return-orders/<id>/review/` | POST | 审批或作废,流程与采购单一致。 |
- **创建字段**`supplier``warehouse``return_date``items`、可选 `purchase_order``items` 结构沿用采购单;当仓库 `mode=RESTRICT_IN_OUT` 时必须提供 `consume_detail_ids`,指明要冲销的入库明细。 - **创建字段**`supplier``warehouse``return_date``items`、可选 `purchase_order``items` 结构沿用采购单;当仓库 `mode=RESTRICT_IN_OUT` 时必须提供 `consume_detail_ids`,指明要冲销的入库明细。
@@ -90,6 +92,7 @@
|-----|------|------| |-----|------|------|
| `/sales-orders/` | GET | 分页列表。 | | `/sales-orders/` | GET | 分页列表。 |
| `/sales-orders/` | POST | 创建销售单。 | | `/sales-orders/` | POST | 创建销售单。 |
| `/sales-orders/<id>/` | PUT/PATCH | 编辑销售单(仅限审批中)。 |
| `/sales-orders/<id>/review/` | POST | 审批或作废。 | | `/sales-orders/<id>/review/` | POST | 审批或作废。 |
### 3.1 创建请求体 ### 3.1 创建请求体
@@ -137,6 +140,7 @@
| API | 方法 | 描述 | | API | 方法 | 描述 |
|-----|------|------| |-----|------|------|
| `/sales-return-orders/` | GET/POST | 创建 / 列表接口。 | | `/sales-return-orders/` | GET/POST | 创建 / 列表接口。 |
| `/sales-return-orders/<id>/` | PUT/PATCH | 编辑销售退货单(仅限审批中)。 |
| `/sales-return-orders/<id>/review/` | POST | 审批或作废。 | | `/sales-return-orders/<id>/review/` | POST | 审批或作废。 |
- **创建字段**`customer``warehouse``return_date``items`、可选 `sales_order`。因退货为入库动作,严出仓不再需要 `consume_detail_ids` - **创建字段**`customer``warehouse``return_date``items`、可选 `sales_order`。因退货为入库动作,严出仓不再需要 `consume_detail_ids`

58
docs/orders_edit_api.md Normal file
View File

@@ -0,0 +1,58 @@
# 订单编辑 API 概览
用于更新业务订单(销售单、销售退货单、采购单、采购退货单)的共同约束:
- 仅允许 **审批中**`PENDING`)状态编辑;若订单已审批/作废,服务层会抛出错误 `"{实体}当前状态不可修改"`
- 更新接口统一使用 `PUT/PATCH /api/v1/<order-type>/<id>/`,请求体必须包含 `items`,且为非空数组。
- 数量校验与创建逻辑一致:严进仓需 `numbers`,宽进仓需 `quantity + num_of_rolls`,严进严出(出库)需 `consume_detail_ids`
- 订单字段(客户/供应商、仓库、备注、日期等)可被更新,未提供的字段沿用原值。
## SalesOrder 更新
- 路径:`PUT/PATCH /api/v1/sales-orders/<id>/`
- 请求体字段(全部可选,但 `items` 必须存在且非空):
- `customer`:客户 ID
- `warehouse` / `warehouse_id`:仓库 ID
- `order_date``YYYY-MM-DD`
- `remarks`
- `items`:与创建时一致(支持可选 `printing_job`
- 响应:`200 OK`,返回最新销售单详情(同 `GET` 列表项,额外包含 `quantity_of_rolls` 数组)
- 错误:
- `400`:参数缺失或状态不可编辑(`{"error": "销售单当前状态不可修改"}`
- `404`:订单不存在
> 其余订单SalesReturn/Purchase/PurchaseReturn的编辑接口请在实现后按同一规范补充到本文件。
## SalesReturnOrder 更新
- 路径:`PUT/PATCH /api/v1/sales-return-orders/<id>/`
- 请求体字段:
- `customer``warehouse``return_date``remarks`(可选)
- `sales_order`:可重新绑定关联销售单,同样必须属于当前客户
- `items`:与创建规则一致(严进模式需 `numbers`,宽进模式 `quantity/num_of_rolls` 等)
- 响应:`200 OK`,结构同 `GET /sales-return-orders/` + `quantity_of_rolls`
- 错误示例:
- `{"error": "销售退货单当前状态不可修改"}`(状态非 PENDING
- `{"error": "销售退货单的客户必须与原销售单一致"}`(客户不匹配)
## PurchaseOrder 更新
- 路径:`PUT/PATCH /api/v1/purchase-orders/<id>/`
- 请求体字段:
- `supplier``warehouse``order_date``remarks`
- `items`:与创建规则相同(严进模式 `numbers`,宽进 `quantity/num_of_rolls`
- 响应:`200 OK`,返回最新采购单,新增 `quantity_of_rolls` 字段
- 错误示例:
- `{"error": "采购单当前状态不可修改"}`(状态非 PENDING
## PurchaseReturnOrder 更新
- 路径:`PUT/PATCH /api/v1/purchase-return-orders/<id>/`
- 请求体字段:
- `supplier``warehouse``return_date``remarks`
- `purchase_order`:可重新绑定关联采购单,必须属于当前供应商
- `items`:结构同创建(严出仓仍需 `consume_detail_ids`
- 响应:`200 OK`,返回采购退货单详情(含 `quantity_of_rolls`
- 错误示例:
- `{"error": "采购退货单当前状态不可修改"}`
- `{"error": "采购退货单的供应商必须与原采购单一致"}`

View File

@@ -0,0 +1,62 @@
# Printing 模块字段说明2025-12-12
## PrintingJob
来源:`api_v1/views/printing/serializers.py` 中的 `PrintingJobDetailSerializer`
| 字段 | 说明 |
|------|------|
| `id` | 主键 |
| `printing_order` / `printing_order_id` | 关联的印染主订单 ID 与人类可读编号 |
| `product` / `product_name` / `product_code` | 关联产品 |
| `quantity` / `unit` | 生产数量与单位 |
| `size` | 一段尺寸 |
| `pieces` | 件数 |
| `description` | 备注 |
| `work_state` | 业务进展枚举值,详见下表 |
| `work_state_display` | 业务进展的中文描述 |
| `status` / `status_id` | stateflow 状态显示值与下一步节点 ID |
| `is_completed` / `has_started` | 是否完成、是否已开始 |
| `business_object_id` | 关联流程实例 ID |
| `created_at` / `updated_at` | 创建、更新时间戳 |
### `work_state` 枚举
定义于 `printing.models.PrintingJobWorkStateEnum`
| 值 | 标识 |
|----|------|
| `0` | 生产中 |
| `1` | 待送货 |
| `2` | 待开单 |
| `3` | 已完结 |
Serializer 与 Model 完全同步,`work_state` 可在创建/更新接口提交(字段可选),返回时携带 `work_state_display`
## PlateOrder
来源:`api_v1/views/printing/serializers.py` 中的 `PlateOrderDetailSerializer`
PlateOrder 模型当前 **没有 `work_state` 字段**。主要字段如下(按序列化器顺序):
| 字段 | 说明 |
|------|------|
| `id` / `design_code` | 主键与设计编号(空则回退为主键字符串) |
| `plate_type` / `plate_date` / `plate_method` | 开版类型、时间、方式 |
| `plate_image` / `plate_image_url` | JSONField 存储的图片信息及其对外地址 |
| `image_name` / `plate_notes` / `reprint_reason` | 图片名称、注意事项、复版原因 |
| `urgency_level` / `is_invalid` | 紧急程度、是否作废 |
| `customer` / `customer_name` / `customer_phone` / `area` / `default_address` | 客户与地址信息 |
| `salesperson` / `merchandiser` / `designer` 及对应 `*_name` | 关联员工 |
| `style_name` / `fabric` / `fabric_source` / `width` / `production_method` | 款式与面料信息 |
| `is_mark_frame` / `drawing_rating` / `color_matching_rating` / `sample_rating` / `difficulty_rating` | 标记及评分字段 |
| `sample_meter` / `required_sample_meters` | 样品米数及需求 |
| `required_completion_date` / `completion_date` | 要求完成与实际完成时间 |
| `approval_result` / `is_ordered` / `customer_feedback` | 审批结果、下单状态、客户反馈 |
| `process` / `process_name` | 关联流程 ID 与名称 |
| `status` / `status_id` / `is_completed` / `has_started` / `progress_percentage` / `business_object_id` / `last_completed_state` | stateflow 状态相关字段 |
| `print_count` | 打印次数 |
| `created_at` / `updated_at` | 创建、更新时间戳 |
由于 PlateOrder 未包含 `work_state`,前端无需提交该字段;若后续业务需要,可在模型与序列化器中新增字段并同步更新本文档。