forked from erp-dev/erp
fix: added merchant_id in models of shipment module
This commit is contained in:
@@ -3,6 +3,6 @@ Shipment API 模块
|
||||
|
||||
提供出货单和销售品相关的 API 接口
|
||||
"""
|
||||
from .views import SalesItemByPrintingOrderView, ShipmentCreateView
|
||||
from .views import SalesItemByPrintingOrderView, ShipmentCreateView, ShipmentExternalCreateView
|
||||
|
||||
__all__ = ['SalesItemByPrintingOrderView', 'ShipmentCreateView']
|
||||
__all__ = ['SalesItemByPrintingOrderView', 'ShipmentCreateView', 'ShipmentExternalCreateView']
|
||||
|
||||
@@ -14,12 +14,23 @@ class ShipmentSerializer(serializers.ModelSerializer):
|
||||
created_by_id = serializers.IntegerField(source='created_by.id', read_only=True, allow_null=True)
|
||||
created_by_name = serializers.SerializerMethodField()
|
||||
items_count = serializers.SerializerMethodField()
|
||||
cancelled_by_id = serializers.IntegerField(source='cancelled_by.id', read_only=True, allow_null=True)
|
||||
cancelled_by_name = serializers.SerializerMethodField()
|
||||
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
||||
external_finished_products_count = serializers.SerializerMethodField()
|
||||
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True)
|
||||
merchant_name = serializers.CharField(source='merchant.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Shipment
|
||||
fields = [
|
||||
'id', 'customer', 'customer_name', 'shipment_date', 'remark',
|
||||
'id', 'merchant_id', 'merchant_name',
|
||||
'customer', 'customer_name', 'shipment_date', 'remark',
|
||||
'status', 'status_display',
|
||||
'external_id',
|
||||
'cancelled_at', 'cancelled_by_id', 'cancelled_by_name',
|
||||
'items_count', 'created_by_id', 'created_by_name',
|
||||
'external_finished_products_count',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
@@ -35,10 +46,21 @@ class ShipmentSerializer(serializers.ModelSerializer):
|
||||
def get_items_count(self, obj):
|
||||
return obj.items.count()
|
||||
|
||||
def get_cancelled_by_name(self, obj):
|
||||
if obj.cancelled_by:
|
||||
employee = getattr(obj.cancelled_by, 'employee', None)
|
||||
if employee:
|
||||
return employee.name
|
||||
return obj.cancelled_by.username
|
||||
return None
|
||||
|
||||
class ShipmentCreateSerializer(serializers.Serializer):
|
||||
def get_external_finished_products_count(self, obj):
|
||||
return obj.external_finished_products.count()
|
||||
|
||||
|
||||
class ShipmentCreateNormalSerializer(serializers.Serializer):
|
||||
"""
|
||||
出货单创建序列化器
|
||||
出货单创建序列化器(普通版)
|
||||
"""
|
||||
customer = serializers.IntegerField(help_text='客户ID')
|
||||
shipment_date = serializers.DateField(help_text='出货日期')
|
||||
@@ -54,6 +76,42 @@ class ShipmentCreateSerializer(serializers.Serializer):
|
||||
# 去重
|
||||
return list(set(value)) if value else []
|
||||
|
||||
class ExternalFinishedProductInputSerializer(serializers.Serializer):
|
||||
"""外部成品表写入结构(external create 专用)"""
|
||||
style_name = serializers.CharField(max_length=200)
|
||||
num_of_rolls = serializers.IntegerField(min_value=0)
|
||||
|
||||
|
||||
class ShipmentCreateExternalSerializer(serializers.Serializer):
|
||||
"""
|
||||
出货单创建序列化器(external 版)
|
||||
|
||||
特点:
|
||||
- 不绑定任何 SalesItem
|
||||
- 必须提供 external_id
|
||||
- 同时写入 ExternalFinishedProduct 列表并关联到 Shipment
|
||||
"""
|
||||
customer = serializers.IntegerField(help_text='客户ID')
|
||||
shipment_date = serializers.DateField(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(
|
||||
child=ExternalFinishedProductInputSerializer(),
|
||||
required=True,
|
||||
help_text='外部成品表结构数组(必填)'
|
||||
)
|
||||
|
||||
def validate_external_id(self, value):
|
||||
value = (value or '').strip()
|
||||
if not value:
|
||||
raise serializers.ValidationError('external_id 不能为空')
|
||||
return value
|
||||
|
||||
def validate_external_finished_products(self, value):
|
||||
if not value:
|
||||
raise serializers.ValidationError('external_finished_products 不能为空')
|
||||
return value
|
||||
|
||||
|
||||
class SalesItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
|
||||
@@ -106,6 +106,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
|
||||
|
||||
# 创建出货单
|
||||
self.shipment = shipment_models.Shipment.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
shipment_date='2026-01-14',
|
||||
created_by=self.user,
|
||||
@@ -113,6 +114,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
|
||||
|
||||
# 创建销售品 - 未关联出货单
|
||||
self.sales_item1 = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='销售品1',
|
||||
quantity=Decimal('50.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
@@ -121,6 +123,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
|
||||
)
|
||||
|
||||
self.sales_item2 = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='销售品2',
|
||||
quantity=Decimal('30.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
@@ -132,6 +135,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
|
||||
|
||||
# 创建销售品 - 已关联出货单
|
||||
self.sales_item3 = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='销售品3(已出货)',
|
||||
quantity=Decimal('100.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
@@ -142,6 +146,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
|
||||
|
||||
# 创建与该订单无关的销售品
|
||||
self.sales_item_other = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='其它销售品',
|
||||
quantity=Decimal('999.00'),
|
||||
unit=shipment_models.UnitChoices.PIECE,
|
||||
@@ -304,6 +309,7 @@ class ShipmentCreateAPITestCase(TestCase):
|
||||
|
||||
# 创建销售品(未关联出货单)
|
||||
self.sales_item1 = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='销售品1',
|
||||
quantity=Decimal('50.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
@@ -311,6 +317,7 @@ class ShipmentCreateAPITestCase(TestCase):
|
||||
)
|
||||
|
||||
self.sales_item2 = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='销售品2',
|
||||
quantity=Decimal('30.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
@@ -319,11 +326,13 @@ class ShipmentCreateAPITestCase(TestCase):
|
||||
|
||||
# 创建已关联出货单的销售品
|
||||
self.existing_shipment = shipment_models.Shipment.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
shipment_date='2026-01-13',
|
||||
created_by=self.user,
|
||||
)
|
||||
self.sales_item_shipped = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='销售品3(已出货)',
|
||||
quantity=Decimal('100.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
@@ -426,3 +435,121 @@ class ShipmentCreateAPITestCase(TestCase):
|
||||
response = self.client.post('/api/v1/shipment/shipments/', data, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class ShipmentExternalCreateAPITestCase(TestCase):
|
||||
"""测试创建出货单 external 版 API"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# 创建商户
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='测试印花厂',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY
|
||||
)
|
||||
|
||||
# 创建用户
|
||||
self.user = User.objects.create_user(
|
||||
username='testuser_ext',
|
||||
password='testpass123',
|
||||
email='test_ext@example.com'
|
||||
)
|
||||
|
||||
# 创建员工并关联商户
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
sys_user=self.user,
|
||||
merchant=self.merchant,
|
||||
name='测试员工Ext',
|
||||
mobile='13800138002',
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE
|
||||
)
|
||||
|
||||
# 创建客户(需要同 merchant)
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='测试客户Ext',
|
||||
mobile='13900139002',
|
||||
area='测试地区Ext'
|
||||
)
|
||||
|
||||
# 创建一个销售品(用于验证 external 版不会绑定任何销售品)
|
||||
self.sales_item = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='销售品-不应被绑定',
|
||||
quantity=Decimal('10.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_create_external_shipment_success(self):
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'shipment_date': '2026-01-14',
|
||||
'remark': 'external 备注',
|
||||
'external_id': 'EXT-ORDER-001',
|
||||
'external_finished_products': [
|
||||
{'style_name': '款式A', 'num_of_rolls': 2},
|
||||
{'style_name': '款式B', 'num_of_rolls': 5},
|
||||
]
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/shipment/shipments/external/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
result = response.json()
|
||||
|
||||
self.assertEqual(result['customer'], self.customer.id)
|
||||
self.assertEqual(result['external_id'], 'EXT-ORDER-001')
|
||||
self.assertEqual(result['items_count'], 0)
|
||||
self.assertEqual(result['external_finished_products_count'], 2)
|
||||
|
||||
shipment_id = result['id']
|
||||
|
||||
# 验证外部成品表写入并关联
|
||||
self.assertEqual(
|
||||
shipment_models.ExternalFinishedProduct.objects.filter(shipment_id=shipment_id).count(),
|
||||
2
|
||||
)
|
||||
|
||||
# 验证不会绑定任何销售品
|
||||
self.sales_item.refresh_from_db()
|
||||
self.assertIsNone(self.sales_item.shipment_id)
|
||||
|
||||
def test_create_external_shipment_external_id_required(self):
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'shipment_date': '2026-01-14',
|
||||
'external_id': ' ',
|
||||
'external_finished_products': [
|
||||
{'style_name': '款式A', 'num_of_rolls': 1},
|
||||
]
|
||||
}
|
||||
response = self.client.post('/api/v1/shipment/shipments/external/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('external_id', response.json())
|
||||
|
||||
def test_create_external_shipment_products_required(self):
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'shipment_date': '2026-01-14',
|
||||
'external_id': 'EXT-ORDER-002',
|
||||
'external_finished_products': []
|
||||
}
|
||||
response = self.client.post('/api/v1/shipment/shipments/external/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('external_finished_products', response.json())
|
||||
|
||||
def test_create_external_shipment_unauthenticated(self):
|
||||
self.client.logout()
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'shipment_date': '2026-01-14',
|
||||
'external_id': 'EXT-ORDER-003',
|
||||
'external_finished_products': [
|
||||
{'style_name': '款式A', 'num_of_rolls': 1},
|
||||
]
|
||||
}
|
||||
response = self.client.post('/api/v1/shipment/shipments/external/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -6,7 +6,12 @@ from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from .serializers import SalesItemSerializer, ShipmentSerializer, ShipmentCreateSerializer
|
||||
from .serializers import (
|
||||
SalesItemSerializer,
|
||||
ShipmentSerializer,
|
||||
ShipmentCreateNormalSerializer,
|
||||
ShipmentCreateExternalSerializer,
|
||||
)
|
||||
|
||||
|
||||
class ShipmentCreateView(APIView):
|
||||
@@ -41,7 +46,7 @@ class ShipmentCreateView(APIView):
|
||||
|
||||
def post(self, request):
|
||||
# 验证请求数据
|
||||
serializer = ShipmentCreateSerializer(data=request.data)
|
||||
serializer = ShipmentCreateNormalSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -65,6 +70,43 @@ class ShipmentCreateView(APIView):
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class ShipmentExternalCreateView(APIView):
|
||||
"""
|
||||
创建出货单(external 版)
|
||||
|
||||
POST /api/v1/shipment/shipments/external/
|
||||
|
||||
特点:
|
||||
- external_id 必填
|
||||
- external_finished_products 必填(数组)
|
||||
- 不绑定任何销售品
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = ShipmentCreateExternalSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
data = serializer.validated_data
|
||||
|
||||
from shipment.services import create_external_shipment
|
||||
try:
|
||||
shipment = create_external_shipment(
|
||||
customer_id=data['customer'],
|
||||
shipment_date=data['shipment_date'],
|
||||
external_id=data['external_id'],
|
||||
external_finished_products=data['external_finished_products'],
|
||||
created_by=request.user,
|
||||
remark=data.get('remark', ''),
|
||||
)
|
||||
except ValueError as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
response_serializer = ShipmentSerializer(shipment)
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class SalesItemByPrintingOrderView(APIView):
|
||||
"""
|
||||
通过生产订单查询销售品
|
||||
|
||||
Reference in New Issue
Block a user