forked from erp-dev/erp
feat: added area field into shipments model
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
销售品序列化器(只读)
|
||||
|
||||
@@ -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 字段)。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
|------|------|------|------|
|
||||
| customer | int | 是 | 客户ID |
|
||||
| shipment_date | string | 是 | 出货日期(YYYY-MM-DD) |
|
||||
| area | string | 否 | 出货地区(可空字符串,长度<=30) |
|
||||
| remark | string | 否 | 备注 |
|
||||
| sales_items | array[int] | 否 | 要关联的销售品ID列表 |
|
||||
|
||||
@@ -93,6 +94,7 @@
|
||||
{
|
||||
"customer": 1,
|
||||
"shipment_date": "2026-01-14",
|
||||
"area": "华东",
|
||||
"remark": "备注信息",
|
||||
"sales_items": [1, 2, 3]
|
||||
}
|
||||
@@ -108,6 +110,7 @@
|
||||
"customer": 1,
|
||||
"customer_name": "客户A",
|
||||
"shipment_date": "2026-01-14",
|
||||
"area": "华东",
|
||||
"remark": "备注信息",
|
||||
"items_count": 3,
|
||||
"sales_items": [],
|
||||
@@ -129,6 +132,7 @@
|
||||
| customer | int | 客户ID |
|
||||
| customer_name | string | 客户名称 |
|
||||
| shipment_date | string | 出货日期 |
|
||||
| area | string | 出货地区 |
|
||||
| remark | string | 备注 |
|
||||
| items_count | int | 关联的销售品数量 |
|
||||
| created_by_id | int | 创建人ID |
|
||||
@@ -180,6 +184,7 @@
|
||||
|------|------|------|------|
|
||||
| customer | int | 是 | 客户ID |
|
||||
| shipment_date | string | 是 | 出货日期(YYYY-MM-DD) |
|
||||
| area | string | 否 | 出货地区(可空字符串,长度<=30) |
|
||||
| remark | string | 否 | 备注 |
|
||||
| external_id | string | 是 | 外部订单号(长度<=120) |
|
||||
| external_finished_products | array[object] | 是 | 外部成品表结构数组(至少 1 条) |
|
||||
@@ -198,6 +203,7 @@
|
||||
{
|
||||
"customer": 1,
|
||||
"shipment_date": "2026-01-14",
|
||||
"area": "华南",
|
||||
"remark": "external 备注(可选)",
|
||||
"external_id": "EXT-ORDER-001",
|
||||
"external_finished_products": [
|
||||
@@ -217,6 +223,7 @@
|
||||
"customer": 1,
|
||||
"customer_name": "客户A",
|
||||
"shipment_date": "2026-01-14",
|
||||
"area": "华南",
|
||||
"remark": "external 备注(可选)",
|
||||
"status": 1,
|
||||
"status_display": "待送货",
|
||||
|
||||
17
shipment/migrations/0008_add_area_to_shipment.py
Normal file
17
shipment/migrations/0008_add_area_to_shipment.py
Normal 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='地区'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -128,6 +128,14 @@ class Shipment(ModelBase):
|
||||
help_text='实际出货日期'
|
||||
)
|
||||
|
||||
area = models.CharField(
|
||||
max_length=30,
|
||||
blank=True,
|
||||
default='',
|
||||
verbose_name='地区',
|
||||
help_text='出货地区(可空字符串)',
|
||||
)
|
||||
|
||||
remark = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
|
||||
@@ -49,6 +49,7 @@ def create_shipment(
|
||||
sales_item_ids: List[int],
|
||||
created_by,
|
||||
remark: str = '',
|
||||
area: str = '',
|
||||
) -> Shipment:
|
||||
"""
|
||||
创建出货单并关联销售品
|
||||
@@ -104,6 +105,7 @@ def create_shipment(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
shipment_date=shipment_date,
|
||||
area=(area or '').strip(),
|
||||
remark=remark,
|
||||
created_by=created_by,
|
||||
)
|
||||
@@ -125,6 +127,7 @@ def create_external_shipment(
|
||||
external_finished_products: List[dict],
|
||||
created_by,
|
||||
remark: str = '',
|
||||
area: str = '',
|
||||
) -> Shipment:
|
||||
"""
|
||||
创建出货单(external 版),并批量写入外部成品表并关联到出货单。
|
||||
@@ -160,6 +163,7 @@ def create_external_shipment(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
shipment_date=shipment_date,
|
||||
area=(area or '').strip(),
|
||||
remark=remark,
|
||||
created_by=created_by,
|
||||
external_id=external_id,
|
||||
|
||||
Reference in New Issue
Block a user