1
0
forked from erp-dev/erp

feat: added area field into shipments model

This commit is contained in:
2026-01-16 17:58:17 +08:00
parent 2e0b7dc335
commit c73014f69c
7 changed files with 123 additions and 1 deletions

View File

@@ -27,7 +27,7 @@ class ShipmentSerializer(serializers.ModelSerializer):
model = Shipment
fields = [
'id', 'merchant_id', 'merchant_name',
'customer', 'customer_name', 'shipment_date', 'remark',
'customer', 'customer_name', 'shipment_date', 'area', 'remark',
'status', 'status_display',
'external_id',
'cancelled_at', 'cancelled_by_id', 'cancelled_by_name',
@@ -114,6 +114,7 @@ class ShipmentCreateNormalSerializer(serializers.Serializer):
"""
customer = serializers.IntegerField(help_text='客户ID')
shipment_date = serializers.DateField(help_text='出货日期')
area = serializers.CharField(max_length=30, required=False, default='', allow_blank=True, help_text='出货地区(可选)')
remark = serializers.CharField(required=False, default='', allow_blank=True, help_text='备注')
sales_items = serializers.ListField(
child=serializers.IntegerField(),
@@ -144,6 +145,7 @@ class ShipmentCreateExternalSerializer(serializers.Serializer):
"""
customer = serializers.IntegerField(help_text='客户ID')
shipment_date = serializers.DateField(help_text='出货日期')
area = serializers.CharField(max_length=30, required=False, default='', allow_blank=True, help_text='出货地区(可选)')
remark = serializers.CharField(required=False, default='', allow_blank=True, help_text='备注')
external_id = serializers.CharField(max_length=120, help_text='外部订单号(必填)')
external_finished_products = serializers.ListField(
@@ -164,6 +166,19 @@ class ShipmentCreateExternalSerializer(serializers.Serializer):
return value
class ShipmentUpdateSerializer(serializers.Serializer):
"""
出货单更新序列化器PATCH/PUT
说明:本次新增字段 area需要保证更新入口可写入并回显。
"""
customer = serializers.IntegerField(required=False, help_text='客户ID可选')
shipment_date = serializers.DateField(required=False, help_text='出货日期(可选)')
area = serializers.CharField(max_length=30, required=False, allow_blank=True, help_text='出货地区(可选)')
remark = serializers.CharField(required=False, allow_blank=True, help_text='备注(可选)')
external_id = serializers.CharField(max_length=120, required=False, allow_blank=True, allow_null=True, help_text='外部订单号(可选)')
class SalesItemSerializer(serializers.Serializer):
"""
销售品序列化器(只读)

View File

@@ -348,6 +348,7 @@ class ShipmentCreateAPITestCase(TestCase):
data = {
'customer': self.customer.id,
'shipment_date': '2026-01-14',
'area': '华东',
'remark': '测试备注',
'sales_items': [self.sales_item1.id, self.sales_item2.id]
}
@@ -362,6 +363,7 @@ class ShipmentCreateAPITestCase(TestCase):
self.assertEqual(result['customer'], self.customer.id)
self.assertEqual(result['customer_name'], self.customer.name)
self.assertEqual(result['shipment_date'], '2026-01-14')
self.assertEqual(result.get('area', ''), '华东')
self.assertEqual(result['remark'], '测试备注')
self.assertEqual(result['items_count'], 2)
self.assertEqual(result['created_by_id'], self.user.id)
@@ -488,6 +490,7 @@ class ShipmentExternalCreateAPITestCase(TestCase):
data = {
'customer': self.customer.id,
'shipment_date': '2026-01-14',
'area': '华南',
'remark': 'external 备注',
'external_id': 'EXT-ORDER-001',
'external_finished_products': [
@@ -502,6 +505,7 @@ class ShipmentExternalCreateAPITestCase(TestCase):
self.assertEqual(result['customer'], self.customer.id)
self.assertEqual(result['external_id'], 'EXT-ORDER-001')
self.assertEqual(result.get('area', ''), '华南')
self.assertEqual(result['items_count'], 0)
self.assertEqual(result['external_finished_products_count'], 2)
@@ -618,6 +622,7 @@ class ShipmentQueryAPITestCase(TestCase):
shipment_date='2026-01-15',
created_by=self.user1,
remark='s1',
area='A1',
)
self.shipment2 = shipment_models.Shipment.objects.create(
merchant=self.merchant2,
@@ -625,6 +630,7 @@ class ShipmentQueryAPITestCase(TestCase):
shipment_date='2026-01-15',
created_by=self.user2,
remark='s2',
area='B1',
)
self.client.force_authenticate(user=self.user1)
@@ -643,6 +649,8 @@ class ShipmentQueryAPITestCase(TestCase):
# 关键:列表序列化器必须稳定输出明细数组字段(即使为空)
item = next(it for it in data['results'] if it['id'] == self.shipment1.id)
self.assertIn('area', item)
self.assertEqual(item['area'], 'A1')
self.assertIn('sales_items', item)
self.assertIsInstance(item['sales_items'], list)
self.assertIn('external_finished_products', item)
@@ -653,11 +661,30 @@ class ShipmentQueryAPITestCase(TestCase):
self.assertEqual(resp.status_code, status.HTTP_200_OK)
result = resp.json()
self.assertEqual(result['id'], self.shipment1.id)
self.assertIn('area', result)
self.assertEqual(result['area'], 'A1')
self.assertIn('sales_items', result)
self.assertIsInstance(result['sales_items'], list)
self.assertIn('external_finished_products', result)
self.assertIsInstance(result['external_finished_products'], list)
def test_patch_shipment_area_success(self):
"""
新增字段 area支持更新PATCH并回显。
"""
resp = self.client.patch(
f'/api/v1/shipment/shipments/{self.shipment1.id}/',
data={'area': '更新地区'},
format='json'
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
result = resp.json()
self.assertEqual(result['id'], self.shipment1.id)
self.assertEqual(result.get('area', ''), '更新地区')
self.shipment1.refresh_from_db()
self.assertEqual(getattr(self.shipment1, 'area', ''), '更新地区')
def test_list_shipments_includes_external_finished_product_remark(self):
"""
Shipments list 需要附带 external_finished_products 明细数据(包含 remark 字段)。

View File

@@ -16,6 +16,7 @@ from .serializers import (
ShipmentSerializer,
ShipmentCreateNormalSerializer,
ShipmentCreateExternalSerializer,
ShipmentUpdateSerializer,
)
@@ -122,6 +123,7 @@ class ShipmentListCreateView(ListModelMixin, GenericAPIView):
sales_item_ids=data['sales_items'],
created_by=request.user,
remark=data.get('remark', ''),
area=data.get('area', ''),
)
except ValueError as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -161,6 +163,47 @@ class ShipmentDetailView(RetrieveModelMixin, GenericAPIView):
def get(self, request, pk: int):
return self.retrieve(request, pk=pk)
def patch(self, request, pk: int):
"""
更新出货单(部分更新)
PATCH /api/v1/shipment/shipments/<id>/
"""
shipment = self.get_object()
serializer = ShipmentUpdateSerializer(data=request.data, partial=True)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
data = serializer.validated_data
# customer 变更需校验 merchant 一致
if 'customer' in data:
from basic_info.models import Customer
try:
customer = Customer.objects.get(id=data['customer'])
except Customer.DoesNotExist:
return Response({'detail': f'客户 {data["customer"]} 不存在'}, status=status.HTTP_400_BAD_REQUEST)
if customer.merchant_id != shipment.merchant_id:
return Response({'detail': '无权限绑定该客户'}, status=status.HTTP_400_BAD_REQUEST)
shipment.customer = customer
if 'shipment_date' in data:
shipment.shipment_date = data['shipment_date']
if 'area' in data:
shipment.area = (data.get('area') or '').strip()
if 'remark' in data:
shipment.remark = data.get('remark') or ''
if 'external_id' in data:
shipment.external_id = (data.get('external_id') or '').strip() or None
shipment.save()
return Response(ShipmentSerializer(shipment).data, status=status.HTTP_200_OK)
def put(self, request, pk: int):
"""
更新出货单(全量更新,当前实现允许缺省字段,等同于 PATCH
PUT /api/v1/shipment/shipments/<id>/
"""
return self.patch(request, pk=pk)
class ShipmentExternalCreateView(APIView):
"""
@@ -191,6 +234,7 @@ class ShipmentExternalCreateView(APIView):
external_finished_products=data['external_finished_products'],
created_by=request.user,
remark=data.get('remark', ''),
area=data.get('area', ''),
)
except ValueError as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)