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 model = Shipment
fields = [ fields = [
'id', 'merchant_id', 'merchant_name', 'id', 'merchant_id', 'merchant_name',
'customer', 'customer_name', 'shipment_date', 'remark', 'customer', 'customer_name', 'shipment_date', 'area', 'remark',
'status', 'status_display', 'status', 'status_display',
'external_id', 'external_id',
'cancelled_at', 'cancelled_by_id', 'cancelled_by_name', 'cancelled_at', 'cancelled_by_id', 'cancelled_by_name',
@@ -114,6 +114,7 @@ class ShipmentCreateNormalSerializer(serializers.Serializer):
""" """
customer = serializers.IntegerField(help_text='客户ID') customer = serializers.IntegerField(help_text='客户ID')
shipment_date = serializers.DateField(help_text='出货日期') 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='备注') remark = serializers.CharField(required=False, default='', allow_blank=True, help_text='备注')
sales_items = serializers.ListField( sales_items = serializers.ListField(
child=serializers.IntegerField(), child=serializers.IntegerField(),
@@ -144,6 +145,7 @@ class ShipmentCreateExternalSerializer(serializers.Serializer):
""" """
customer = serializers.IntegerField(help_text='客户ID') customer = serializers.IntegerField(help_text='客户ID')
shipment_date = serializers.DateField(help_text='出货日期') 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='备注') remark = serializers.CharField(required=False, default='', allow_blank=True, help_text='备注')
external_id = serializers.CharField(max_length=120, help_text='外部订单号(必填)') external_id = serializers.CharField(max_length=120, help_text='外部订单号(必填)')
external_finished_products = serializers.ListField( external_finished_products = serializers.ListField(
@@ -164,6 +166,19 @@ class ShipmentCreateExternalSerializer(serializers.Serializer):
return value 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): class SalesItemSerializer(serializers.Serializer):
""" """
销售品序列化器(只读) 销售品序列化器(只读)

View File

@@ -348,6 +348,7 @@ class ShipmentCreateAPITestCase(TestCase):
data = { data = {
'customer': self.customer.id, 'customer': self.customer.id,
'shipment_date': '2026-01-14', 'shipment_date': '2026-01-14',
'area': '华东',
'remark': '测试备注', 'remark': '测试备注',
'sales_items': [self.sales_item1.id, self.sales_item2.id] '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'], self.customer.id)
self.assertEqual(result['customer_name'], self.customer.name) self.assertEqual(result['customer_name'], self.customer.name)
self.assertEqual(result['shipment_date'], '2026-01-14') self.assertEqual(result['shipment_date'], '2026-01-14')
self.assertEqual(result.get('area', ''), '华东')
self.assertEqual(result['remark'], '测试备注') self.assertEqual(result['remark'], '测试备注')
self.assertEqual(result['items_count'], 2) self.assertEqual(result['items_count'], 2)
self.assertEqual(result['created_by_id'], self.user.id) self.assertEqual(result['created_by_id'], self.user.id)
@@ -488,6 +490,7 @@ class ShipmentExternalCreateAPITestCase(TestCase):
data = { data = {
'customer': self.customer.id, 'customer': self.customer.id,
'shipment_date': '2026-01-14', 'shipment_date': '2026-01-14',
'area': '华南',
'remark': 'external 备注', 'remark': 'external 备注',
'external_id': 'EXT-ORDER-001', 'external_id': 'EXT-ORDER-001',
'external_finished_products': [ 'external_finished_products': [
@@ -502,6 +505,7 @@ class ShipmentExternalCreateAPITestCase(TestCase):
self.assertEqual(result['customer'], self.customer.id) self.assertEqual(result['customer'], self.customer.id)
self.assertEqual(result['external_id'], 'EXT-ORDER-001') self.assertEqual(result['external_id'], 'EXT-ORDER-001')
self.assertEqual(result.get('area', ''), '华南')
self.assertEqual(result['items_count'], 0) self.assertEqual(result['items_count'], 0)
self.assertEqual(result['external_finished_products_count'], 2) self.assertEqual(result['external_finished_products_count'], 2)
@@ -618,6 +622,7 @@ class ShipmentQueryAPITestCase(TestCase):
shipment_date='2026-01-15', shipment_date='2026-01-15',
created_by=self.user1, created_by=self.user1,
remark='s1', remark='s1',
area='A1',
) )
self.shipment2 = shipment_models.Shipment.objects.create( self.shipment2 = shipment_models.Shipment.objects.create(
merchant=self.merchant2, merchant=self.merchant2,
@@ -625,6 +630,7 @@ class ShipmentQueryAPITestCase(TestCase):
shipment_date='2026-01-15', shipment_date='2026-01-15',
created_by=self.user2, created_by=self.user2,
remark='s2', remark='s2',
area='B1',
) )
self.client.force_authenticate(user=self.user1) 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) 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.assertIn('sales_items', item)
self.assertIsInstance(item['sales_items'], list) self.assertIsInstance(item['sales_items'], list)
self.assertIn('external_finished_products', item) self.assertIn('external_finished_products', item)
@@ -653,11 +661,30 @@ class ShipmentQueryAPITestCase(TestCase):
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
result = resp.json() result = resp.json()
self.assertEqual(result['id'], self.shipment1.id) self.assertEqual(result['id'], self.shipment1.id)
self.assertIn('area', result)
self.assertEqual(result['area'], 'A1')
self.assertIn('sales_items', result) self.assertIn('sales_items', result)
self.assertIsInstance(result['sales_items'], list) self.assertIsInstance(result['sales_items'], list)
self.assertIn('external_finished_products', result) self.assertIn('external_finished_products', result)
self.assertIsInstance(result['external_finished_products'], list) 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): def test_list_shipments_includes_external_finished_product_remark(self):
""" """
Shipments list 需要附带 external_finished_products 明细数据(包含 remark 字段)。 Shipments list 需要附带 external_finished_products 明细数据(包含 remark 字段)。

View File

@@ -16,6 +16,7 @@ from .serializers import (
ShipmentSerializer, ShipmentSerializer,
ShipmentCreateNormalSerializer, ShipmentCreateNormalSerializer,
ShipmentCreateExternalSerializer, ShipmentCreateExternalSerializer,
ShipmentUpdateSerializer,
) )
@@ -122,6 +123,7 @@ class ShipmentListCreateView(ListModelMixin, GenericAPIView):
sales_item_ids=data['sales_items'], sales_item_ids=data['sales_items'],
created_by=request.user, created_by=request.user,
remark=data.get('remark', ''), remark=data.get('remark', ''),
area=data.get('area', ''),
) )
except ValueError as e: except ValueError as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) 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): def get(self, request, pk: int):
return self.retrieve(request, pk=pk) 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): class ShipmentExternalCreateView(APIView):
""" """
@@ -191,6 +234,7 @@ class ShipmentExternalCreateView(APIView):
external_finished_products=data['external_finished_products'], external_finished_products=data['external_finished_products'],
created_by=request.user, created_by=request.user,
remark=data.get('remark', ''), remark=data.get('remark', ''),
area=data.get('area', ''),
) )
except ValueError as e: except ValueError as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -84,6 +84,7 @@
|------|------|------|------| |------|------|------|------|
| customer | int | 是 | 客户ID | | customer | int | 是 | 客户ID |
| shipment_date | string | 是 | 出货日期YYYY-MM-DD | | shipment_date | string | 是 | 出货日期YYYY-MM-DD |
| area | string | 否 | 出货地区(可空字符串,长度<=30 |
| remark | string | 否 | 备注 | | remark | string | 否 | 备注 |
| sales_items | array[int] | 否 | 要关联的销售品ID列表 | | sales_items | array[int] | 否 | 要关联的销售品ID列表 |
@@ -93,6 +94,7 @@
{ {
"customer": 1, "customer": 1,
"shipment_date": "2026-01-14", "shipment_date": "2026-01-14",
"area": "华东",
"remark": "备注信息", "remark": "备注信息",
"sales_items": [1, 2, 3] "sales_items": [1, 2, 3]
} }
@@ -108,6 +110,7 @@
"customer": 1, "customer": 1,
"customer_name": "客户A", "customer_name": "客户A",
"shipment_date": "2026-01-14", "shipment_date": "2026-01-14",
"area": "华东",
"remark": "备注信息", "remark": "备注信息",
"items_count": 3, "items_count": 3,
"sales_items": [], "sales_items": [],
@@ -129,6 +132,7 @@
| customer | int | 客户ID | | customer | int | 客户ID |
| customer_name | string | 客户名称 | | customer_name | string | 客户名称 |
| shipment_date | string | 出货日期 | | shipment_date | string | 出货日期 |
| area | string | 出货地区 |
| remark | string | 备注 | | remark | string | 备注 |
| items_count | int | 关联的销售品数量 | | items_count | int | 关联的销售品数量 |
| created_by_id | int | 创建人ID | | created_by_id | int | 创建人ID |
@@ -180,6 +184,7 @@
|------|------|------|------| |------|------|------|------|
| customer | int | 是 | 客户ID | | customer | int | 是 | 客户ID |
| shipment_date | string | 是 | 出货日期YYYY-MM-DD | | shipment_date | string | 是 | 出货日期YYYY-MM-DD |
| area | string | 否 | 出货地区(可空字符串,长度<=30 |
| remark | string | 否 | 备注 | | remark | string | 否 | 备注 |
| external_id | string | 是 | 外部订单号(长度<=120 | | external_id | string | 是 | 外部订单号(长度<=120 |
| external_finished_products | array[object] | 是 | 外部成品表结构数组(至少 1 条) | | external_finished_products | array[object] | 是 | 外部成品表结构数组(至少 1 条) |
@@ -198,6 +203,7 @@
{ {
"customer": 1, "customer": 1,
"shipment_date": "2026-01-14", "shipment_date": "2026-01-14",
"area": "华南",
"remark": "external 备注(可选)", "remark": "external 备注(可选)",
"external_id": "EXT-ORDER-001", "external_id": "EXT-ORDER-001",
"external_finished_products": [ "external_finished_products": [
@@ -217,6 +223,7 @@
"customer": 1, "customer": 1,
"customer_name": "客户A", "customer_name": "客户A",
"shipment_date": "2026-01-14", "shipment_date": "2026-01-14",
"area": "华南",
"remark": "external 备注(可选)", "remark": "external 备注(可选)",
"status": 1, "status": 1,
"status_display": "待送货", "status_display": "待送货",

View File

@@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shipment', '0007_add_remark_to_external_finished_product'),
]
operations = [
migrations.AddField(
model_name='shipment',
name='area',
field=models.CharField(blank=True, default='', help_text='出货地区(可空字符串)', max_length=30, verbose_name='地区'),
),
]

View File

@@ -128,6 +128,14 @@ class Shipment(ModelBase):
help_text='实际出货日期' help_text='实际出货日期'
) )
area = models.CharField(
max_length=30,
blank=True,
default='',
verbose_name='地区',
help_text='出货地区(可空字符串)',
)
remark = models.TextField( remark = models.TextField(
blank=True, blank=True,
default='', default='',

View File

@@ -49,6 +49,7 @@ def create_shipment(
sales_item_ids: List[int], sales_item_ids: List[int],
created_by, created_by,
remark: str = '', remark: str = '',
area: str = '',
) -> Shipment: ) -> Shipment:
""" """
创建出货单并关联销售品 创建出货单并关联销售品
@@ -104,6 +105,7 @@ def create_shipment(
merchant=merchant, merchant=merchant,
customer=customer, customer=customer,
shipment_date=shipment_date, shipment_date=shipment_date,
area=(area or '').strip(),
remark=remark, remark=remark,
created_by=created_by, created_by=created_by,
) )
@@ -125,6 +127,7 @@ def create_external_shipment(
external_finished_products: List[dict], external_finished_products: List[dict],
created_by, created_by,
remark: str = '', remark: str = '',
area: str = '',
) -> Shipment: ) -> Shipment:
""" """
创建出货单external 版),并批量写入外部成品表并关联到出货单。 创建出货单external 版),并批量写入外部成品表并关联到出货单。
@@ -160,6 +163,7 @@ def create_external_shipment(
merchant=merchant, merchant=merchant,
customer=customer, customer=customer,
shipment_date=shipment_date, shipment_date=shipment_date,
area=(area or '').strip(),
remark=remark, remark=remark,
created_by=created_by, created_by=created_by,
external_id=external_id, external_id=external_id,