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
|
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):
|
||||||
"""
|
"""
|
||||||
销售品序列化器(只读)
|
销售品序列化器(只读)
|
||||||
|
|||||||
@@ -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 字段)。
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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": "待送货",
|
||||||
|
|||||||
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='地区'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -127,6 +127,14 @@ class Shipment(ModelBase):
|
|||||||
verbose_name='出货日期',
|
verbose_name='出货日期',
|
||||||
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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user