forked from erp-dev/erp
feat: added print_count api, added merchant setting model, added manual mode setting for purchase order
This commit is contained in:
57
api_v1/views/print_count.py
Normal file
57
api_v1/views/print_count.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from api_v1.serializers import PrintCountDeltaSerializer
|
||||
from api_v1.enums import PrintCountObjectType
|
||||
from printing import models as printing_models
|
||||
|
||||
|
||||
class PrintCountDeltaView(APIView):
|
||||
"""通用打印次数递增接口"""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
OBJECT_MODEL_MAP = {
|
||||
PrintCountObjectType.PRINTING_ORDER: printing_models.PrintingOrder,
|
||||
PrintCountObjectType.PLATE_ORDER: printing_models.PlateOrder,
|
||||
}
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = PrintCountDeltaSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
|
||||
model = self.OBJECT_MODEL_MAP.get(data['object_type'])
|
||||
if model is None:
|
||||
return Response(
|
||||
{'detail': '不支持的对象类型'},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
updated = model.objects.filter(id=data['object_id']).update(
|
||||
print_count=F('print_count') + data['delta']
|
||||
)
|
||||
if not updated:
|
||||
return Response(
|
||||
{'detail': '指定对象不存在'},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
fresh_value = model.objects.only('print_count').get(id=data['object_id']).print_count
|
||||
|
||||
return Response(
|
||||
{
|
||||
'object_type': data['object_type'],
|
||||
'object_id': data['object_id'],
|
||||
'delta': data['delta'],
|
||||
'print_count': fresh_value,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
adjust_print_count = PrintCountDeltaView.as_view()
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
- `plate_type`, `urgency_level`, `fabric`, `style_name`: 模糊查询
|
||||
- `is_invalid`, `is_ordered`, `is_mark_frame`: 布尔过滤
|
||||
- `plate_date_from`/`to`: 开版日期范围
|
||||
- `required_completion_date_from`/`to`: 要求完成日期范围
|
||||
- `required_completion_date_from`/`to`: 要求完成时间范围(ISO8601,含具体时间)
|
||||
- `created_date_from`/`to`: 创建日期范围
|
||||
- `search`: 全文搜索 (设计编号, 款式名称, 客户名称, 面料)
|
||||
- `ordering`: 排序字段。
|
||||
|
||||
@@ -2,9 +2,99 @@
|
||||
Printing API 序列化器
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
|
||||
from api_v1.models import UploadedFile
|
||||
from printing import models
|
||||
from .services import PrintingOrderService, PrintingJobService
|
||||
from basic_info.models import Customer, Employee
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def _build_absolute_media_url(url: str | None, request):
|
||||
if not url:
|
||||
return None
|
||||
if isinstance(url, str) and url.startswith(('http://', 'https://')):
|
||||
return url
|
||||
if not isinstance(url, str):
|
||||
return None
|
||||
if request:
|
||||
if url.startswith('/'):
|
||||
return request.build_absolute_uri(url)
|
||||
media_prefix = (settings.MEDIA_URL or '/media/').rstrip('/')
|
||||
return request.build_absolute_uri(f'{media_prefix}/{url.lstrip("/")}')
|
||||
return url
|
||||
|
||||
|
||||
def _serialize_plate_images(raw_value, request):
|
||||
if not raw_value:
|
||||
return []
|
||||
serialized = []
|
||||
if isinstance(raw_value, list):
|
||||
iterable = raw_value
|
||||
elif isinstance(raw_value, dict):
|
||||
iterable = [raw_value]
|
||||
elif isinstance(raw_value, str):
|
||||
iterable = [{'url': raw_value}]
|
||||
else:
|
||||
iterable = []
|
||||
|
||||
for entry in iterable:
|
||||
if isinstance(entry, str):
|
||||
data = {'url': entry}
|
||||
elif isinstance(entry, dict):
|
||||
data = dict(entry)
|
||||
else:
|
||||
continue
|
||||
url = data.get('url') or data.get('path')
|
||||
data['url'] = _build_absolute_media_url(url, request)
|
||||
if 'path' not in data and isinstance(url, str):
|
||||
data['path'] = url
|
||||
serialized.append(data)
|
||||
return serialized
|
||||
|
||||
|
||||
def _build_plate_image_payload(items, request_user):
|
||||
if items is None:
|
||||
return None
|
||||
if not items:
|
||||
return []
|
||||
file_ids = [item['file_id'] for item in items if item.get('file_id') is not None]
|
||||
if not file_ids:
|
||||
return []
|
||||
|
||||
queryset = UploadedFile.objects.filter(id__in=file_ids, is_deleted=False)
|
||||
if request_user and request_user.is_authenticated:
|
||||
queryset = queryset.filter(owner=request_user)
|
||||
|
||||
files_map = {file.id: file for file in queryset}
|
||||
missing = [str(fid) for fid in file_ids if fid not in files_map]
|
||||
if missing:
|
||||
raise serializers.ValidationError({'plate_image': f'以下文件不存在或已删除: {", ".join(missing)}'})
|
||||
|
||||
payload = []
|
||||
for item in items:
|
||||
file = files_map[item['file_id']]
|
||||
name = item.get('name') or file.original_filename or file.path.name
|
||||
payload.append({
|
||||
'file_id': file.id,
|
||||
'name': name,
|
||||
'path': file.path.name,
|
||||
'url': file.file_url,
|
||||
'size': file.file_size,
|
||||
'content_type': file.content_type,
|
||||
'uploaded_at': file.created_at.isoformat(),
|
||||
})
|
||||
return payload
|
||||
|
||||
|
||||
class PlateImageInputSerializer(serializers.Serializer):
|
||||
file_id = serializers.IntegerField(min_value=1, help_text='上传文件的 ID')
|
||||
name = serializers.CharField(
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
help_text='可选的图片名称,默认使用文件原始名称'
|
||||
)
|
||||
|
||||
|
||||
class PrintingOrderListSerializer(serializers.ModelSerializer):
|
||||
@@ -20,10 +110,10 @@ class PrintingOrderListSerializer(serializers.ModelSerializer):
|
||||
'id', 'human_id', 'customer', 'customer_name', 'customer_phone',
|
||||
'fabric', 'width', 'is_urgent', 'area', 'address', 'curve',
|
||||
'is_fabric_received', 'outgoing_date', 'is_invalid', 'new_curve',
|
||||
'process', 'process_name', 'progress', 'position',
|
||||
'process', 'process_name', 'progress', 'position', 'print_count',
|
||||
'created_at', 'updated_at',
|
||||
]
|
||||
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress']
|
||||
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count']
|
||||
|
||||
|
||||
class PrintingOrderDetailSerializer(serializers.ModelSerializer):
|
||||
@@ -42,10 +132,10 @@ class PrintingOrderDetailSerializer(serializers.ModelSerializer):
|
||||
'is_fabric_received', 'craft', 'description', 'outgoing_date',
|
||||
'curve', 'new_curve', 'position',
|
||||
'printing_warn', 'rolling_warn', 'production_warn',
|
||||
'is_invalid', 'process', 'process_name', 'progress',
|
||||
'is_invalid', 'process', 'process_name', 'progress', 'print_count',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress']
|
||||
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count']
|
||||
|
||||
|
||||
class PrintingOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
@@ -214,6 +304,7 @@ class PlateOrderDesignCodeMixin:
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
data['design_code'] = self._normalize_design_code(data.get('design_code'), instance.id)
|
||||
data['plate_image'] = _serialize_plate_images(getattr(instance, 'plate_image', None), self.context.get('request'))
|
||||
return data
|
||||
|
||||
|
||||
@@ -244,7 +335,7 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
|
||||
'sample_rating', 'difficulty_rating',
|
||||
'sample_meter', 'required_sample_meters',
|
||||
'required_completion_date', 'completion_date',
|
||||
'approval_result', 'is_ordered', 'customer_feedback',
|
||||
'approval_result', 'is_ordered', 'customer_feedback', 'print_count',
|
||||
'process', 'process_name',
|
||||
'status', 'status_id', 'is_completed', 'has_started',
|
||||
'progress_percentage', 'business_object_id',
|
||||
@@ -252,17 +343,12 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'status', 'progress_percentage',
|
||||
'created_at', 'updated_at'
|
||||
'created_at', 'updated_at', 'print_count'
|
||||
]
|
||||
|
||||
def get_plate_image_url(self, obj):
|
||||
"""获取图片完整URL"""
|
||||
if obj.plate_image:
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return request.build_absolute_uri(obj.plate_image.url)
|
||||
return obj.plate_image.url
|
||||
return None
|
||||
images = _serialize_plate_images(getattr(obj, 'plate_image', None), self.context.get('request'))
|
||||
return [entry.get('url') for entry in images if entry.get('url')]
|
||||
|
||||
|
||||
def get_process_name(self, obj) ->str | None:
|
||||
@@ -308,7 +394,7 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer
|
||||
'sample_rating', 'difficulty_rating',
|
||||
'sample_meter', 'required_sample_meters',
|
||||
'required_completion_date', 'completion_date',
|
||||
'approval_result', 'is_ordered', 'customer_feedback',
|
||||
'approval_result', 'is_ordered', 'customer_feedback', 'print_count',
|
||||
'process', 'process_name',
|
||||
'status', 'status_id', 'is_completed', 'has_started',
|
||||
'progress_percentage', 'business_object_id',
|
||||
@@ -317,7 +403,7 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer
|
||||
read_only_fields = [
|
||||
'id', 'status', 'status_id', 'is_completed', 'has_started',
|
||||
'progress_percentage', 'business_object_id',
|
||||
'created_at', 'updated_at'
|
||||
'created_at', 'updated_at', 'print_count'
|
||||
]
|
||||
|
||||
def get_business_object_id(self, obj):
|
||||
@@ -325,13 +411,8 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer
|
||||
return obj.business_object.id if obj.business_object else None
|
||||
|
||||
def get_plate_image_url(self, obj):
|
||||
"""获取图片完整URL"""
|
||||
if obj.plate_image:
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return request.build_absolute_uri(obj.plate_image.url)
|
||||
return obj.plate_image.url
|
||||
return None
|
||||
images = _serialize_plate_images(getattr(obj, 'plate_image', None), self.context.get('request'))
|
||||
return [entry.get('url') for entry in images if entry.get('url')]
|
||||
|
||||
def get_process_name(self, obj):
|
||||
"""获取流程名称"""
|
||||
@@ -347,7 +428,13 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer
|
||||
|
||||
class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
"""开版订单创建/更新序列化器"""
|
||||
|
||||
plate_image = PlateImageInputSerializer(
|
||||
many=True,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text="开版图片列表,需提供已上传的 file_id,可选 name 字段"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.PlateOrder
|
||||
fields = [
|
||||
@@ -365,7 +452,10 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
"process"
|
||||
]
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
def validate_plate_image(self, value):
|
||||
return value or []
|
||||
|
||||
def validate_customer(self, value):
|
||||
"""验证客户是否存在"""
|
||||
if not value:
|
||||
@@ -415,18 +505,18 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
"""创建开版订单"""
|
||||
user = self.context["request"].user
|
||||
|
||||
# 创建 PlateOrder,save 方法会自动创建 BusinessObject
|
||||
plate_order = models.PlateOrder.objects.create(**validated_data)
|
||||
|
||||
return plate_order
|
||||
plate_images = validated_data.pop('plate_image', None)
|
||||
if plate_images is not None:
|
||||
validated_data['plate_image'] = _build_plate_image_payload(plate_images, self._request_user)
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""更新开版订单"""
|
||||
# 直接更新字段,save 方法会自动处理 BusinessObject 的创建
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
return instance
|
||||
plate_images = validated_data.pop('plate_image', None)
|
||||
if plate_images is not None:
|
||||
validated_data['plate_image'] = _build_plate_image_payload(plate_images, self._request_user)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
@property
|
||||
def _request_user(self):
|
||||
request = self.context.get('request')
|
||||
return getattr(request, 'user', None)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
PlateOrder API 测试
|
||||
"""
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
@@ -9,6 +10,7 @@ from django.contrib.auth.models import Permission
|
||||
from basic_info import models as basic_models
|
||||
from printing import models as printing_models
|
||||
from stateflow import models as stateflow_models
|
||||
from api_v1 import models as api_models
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -95,6 +97,18 @@ class PlateOrderAPITestCase(TestCase):
|
||||
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
|
||||
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
|
||||
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
|
||||
self.upload_file_primary = self._create_uploaded_file('plate-main.jpg')
|
||||
self.upload_file_secondary = self._create_uploaded_file('plate-secondary.jpg')
|
||||
|
||||
def _create_uploaded_file(self, filename):
|
||||
file = SimpleUploadedFile(filename, b'test-image-content', content_type='image/jpeg')
|
||||
return api_models.UploadedFile.objects.create(
|
||||
owner=self.user,
|
||||
path=file,
|
||||
original_filename=filename,
|
||||
file_size=file.size,
|
||||
content_type='image/jpeg',
|
||||
)
|
||||
|
||||
def test_create_plate_order(self):
|
||||
"""测试创建开版订单"""
|
||||
@@ -109,6 +123,9 @@ class PlateOrderAPITestCase(TestCase):
|
||||
'salesperson': self.salesperson.id,
|
||||
'merchandiser': self.merchandiser.id,
|
||||
'image_name': 'sample.png',
|
||||
'plate_image': [
|
||||
{'file_id': self.upload_file_primary.id, 'name': '主图'}
|
||||
],
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='json')
|
||||
@@ -116,6 +133,8 @@ class PlateOrderAPITestCase(TestCase):
|
||||
self.assertEqual(response.data['design_code'], 'DESIGN001')
|
||||
self.assertEqual(response.data['style_name'], '测试款式')
|
||||
self.assertEqual(response.data['image_name'], 'sample.png')
|
||||
self.assertEqual(len(response.data['plate_image']), 1)
|
||||
self.assertEqual(response.data['plate_image'][0]['file_id'], self.upload_file_primary.id)
|
||||
|
||||
# 验证数据库中创建了记录
|
||||
self.assertTrue(printing_models.PlateOrder.objects.filter(design_code='DESIGN001').exists())
|
||||
@@ -145,11 +164,22 @@ class PlateOrderAPITestCase(TestCase):
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='json')
|
||||
# CharField 不会验证内容,所以应该成功
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_create_plate_order_with_invalid_plate_image_file(self):
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'design_code': 'DESIGN_IMG',
|
||||
'plate_type': '圆网',
|
||||
'plate_image': [{'file_id': 999}],
|
||||
}
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('plate_image', response.data)
|
||||
|
||||
def test_list_plate_orders(self):
|
||||
"""测试获取开版订单列表"""
|
||||
# 创建测试数据
|
||||
printing_models.PlateOrder.objects.create(
|
||||
order_one = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN001',
|
||||
plate_type='圆网',
|
||||
@@ -158,7 +188,17 @@ class PlateOrderAPITestCase(TestCase):
|
||||
salesperson=self.salesperson,
|
||||
image_name='first.png',
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
order_one.plate_image = [
|
||||
{
|
||||
'file_id': self.upload_file_primary.id,
|
||||
'name': 'A 面',
|
||||
'url': self.upload_file_primary.file_url,
|
||||
'path': self.upload_file_primary.path.name,
|
||||
}
|
||||
]
|
||||
order_one.save(update_fields=['plate_image'])
|
||||
|
||||
order_two = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN002',
|
||||
plate_type='平网',
|
||||
@@ -167,6 +207,15 @@ class PlateOrderAPITestCase(TestCase):
|
||||
merchandiser=self.merchandiser,
|
||||
image_name='second.png',
|
||||
)
|
||||
order_two.plate_image = [
|
||||
{
|
||||
'file_id': self.upload_file_secondary.id,
|
||||
'name': 'B 面',
|
||||
'url': self.upload_file_secondary.file_url,
|
||||
'path': self.upload_file_secondary.path.name,
|
||||
}
|
||||
]
|
||||
order_two.save(update_fields=['plate_image'])
|
||||
|
||||
response = self.client.get('/api/v1/plate-orders/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -174,11 +223,13 @@ class PlateOrderAPITestCase(TestCase):
|
||||
if isinstance(response.data, dict):
|
||||
self.assertEqual(response.data['count'], 2)
|
||||
collection = response.data.get('results') or response.data.get('data') or []
|
||||
if collection:
|
||||
self.assertIn('image_name', collection[0])
|
||||
else:
|
||||
self.assertEqual(len(response.data), 2)
|
||||
self.assertIn('image_name', response.data[0])
|
||||
collection = response.data
|
||||
self.assertEqual(len(collection), 2)
|
||||
|
||||
if collection:
|
||||
self.assertIn('plate_image', collection[0])
|
||||
self.assertIsInstance(collection[0]['plate_image'], list)
|
||||
|
||||
def test_retrieve_plate_order(self):
|
||||
"""测试获取单个开版订单详情"""
|
||||
@@ -193,6 +244,15 @@ class PlateOrderAPITestCase(TestCase):
|
||||
merchandiser=self.merchandiser,
|
||||
image_name='detail.png',
|
||||
)
|
||||
plate_order.plate_image = [
|
||||
{
|
||||
'file_id': self.upload_file_primary.id,
|
||||
'name': '详情图',
|
||||
'url': self.upload_file_primary.file_url,
|
||||
'path': self.upload_file_primary.path.name,
|
||||
}
|
||||
]
|
||||
plate_order.save(update_fields=['plate_image'])
|
||||
|
||||
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -202,6 +262,8 @@ class PlateOrderAPITestCase(TestCase):
|
||||
self.assertIn('customer_name', response.data)
|
||||
self.assertIn('salesperson_name', response.data)
|
||||
self.assertEqual(response.data['image_name'], 'detail.png')
|
||||
self.assertIsInstance(response.data['plate_image'], list)
|
||||
self.assertEqual(response.data['plate_image'][0]['file_id'], self.upload_file_primary.id)
|
||||
|
||||
def test_design_code_fallback_in_detail(self):
|
||||
"""design_code 为空时返回主键ID"""
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
"""
|
||||
测试 PlateOrder 文件上传功能
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from basic_info import models as basic_models
|
||||
from printing import models as printing_models
|
||||
from stateflow import models as stateflow_models
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class PlateOrderFileUploadTestCase(TestCase):
|
||||
"""测试 PlateOrder 文件上传(PATCH 请求)"""
|
||||
|
||||
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',
|
||||
password='testpass123',
|
||||
email='test@example.com'
|
||||
)
|
||||
|
||||
# 创建员工并关联商户
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
sys_user=self.user,
|
||||
merchant=self.merchant,
|
||||
name='测试员工',
|
||||
mobile='13800138000',
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE
|
||||
)
|
||||
|
||||
# 创建客户
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='测试客户',
|
||||
mobile='13900139000',
|
||||
area='测试地区'
|
||||
)
|
||||
|
||||
# 认证用户
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
# 给用户添加所有 PlateOrder 权限
|
||||
perms = Permission.objects.filter(
|
||||
content_type__app_label='printing',
|
||||
content_type__model='plateorder'
|
||||
)
|
||||
self.user.user_permissions.add(*perms)
|
||||
|
||||
def test_patch_with_file_upload(self):
|
||||
"""测试 PATCH 请求上传文件(问题场景)"""
|
||||
# 创建开版订单
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN001',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
fabric='棉布',
|
||||
)
|
||||
|
||||
# 创建一个测试图片文件
|
||||
image_content = b'fake image content for testing'
|
||||
image_file = SimpleUploadedFile(
|
||||
"test_plate_image.jpg",
|
||||
image_content,
|
||||
content_type="image/jpeg"
|
||||
)
|
||||
|
||||
# PATCH 请求,同时更新文字字段和上传文件
|
||||
data = {
|
||||
'urgency_level': '加急',
|
||||
'plate_image': image_file,
|
||||
'is_mark_frame': True,
|
||||
}
|
||||
|
||||
response = self.client.patch(
|
||||
f'/api/v1/plate-orders/{plate_order.id}/',
|
||||
data,
|
||||
format='multipart' # 重要:使用 multipart 格式
|
||||
)
|
||||
|
||||
# 验证响应
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK,
|
||||
f"Expected 200, got {response.status_code}: {response.data}")
|
||||
self.assertEqual(response.data['urgency_level'], '加急')
|
||||
self.assertEqual(response.data['is_mark_frame'], True)
|
||||
|
||||
# 验证文件已上传
|
||||
plate_order.refresh_from_db()
|
||||
self.assertTrue(plate_order.plate_image)
|
||||
self.assertIn('test_plate_image', plate_order.plate_image.name)
|
||||
|
||||
def test_patch_without_file(self):
|
||||
"""测试 PATCH 请求不上传文件(正常场景)"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN002',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式2',
|
||||
)
|
||||
|
||||
# 纯 JSON 数据
|
||||
data = {
|
||||
'urgency_level': '特急',
|
||||
'is_mark_frame': False,
|
||||
}
|
||||
|
||||
response = self.client.patch(
|
||||
f'/api/v1/plate-orders/{plate_order.id}/',
|
||||
data,
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['urgency_level'], '特急')
|
||||
self.assertEqual(response.data['is_mark_frame'], False)
|
||||
|
||||
def test_patch_only_file(self):
|
||||
"""测试 PATCH 请求仅上传文件"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN003',
|
||||
plate_type='平网',
|
||||
style_name='测试款式3',
|
||||
urgency_level='正常',
|
||||
)
|
||||
|
||||
# 创建测试文件
|
||||
file_content = b'another fake image'
|
||||
image_file = SimpleUploadedFile(
|
||||
"plate_design.png",
|
||||
file_content,
|
||||
content_type="image/png"
|
||||
)
|
||||
|
||||
data = {
|
||||
'plate_image': image_file,
|
||||
}
|
||||
|
||||
response = self.client.patch(
|
||||
f'/api/v1/plate-orders/{plate_order.id}/',
|
||||
data,
|
||||
format='multipart'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# 验证其他字段未被改变
|
||||
plate_order.refresh_from_db()
|
||||
self.assertEqual(plate_order.urgency_level, '正常')
|
||||
self.assertEqual(plate_order.design_code, 'DESIGN003')
|
||||
self.assertTrue(plate_order.plate_image)
|
||||
|
||||
def test_create_with_file(self):
|
||||
"""测试 POST 创建时上传文件"""
|
||||
file_content = b'initial image'
|
||||
image_file = SimpleUploadedFile(
|
||||
"initial_plate.jpg",
|
||||
file_content,
|
||||
content_type="image/jpeg"
|
||||
)
|
||||
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'design_code': 'DESIGN_NEW',
|
||||
'plate_type': '圆网',
|
||||
'style_name': '新款式',
|
||||
'plate_image': image_file,
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
'/api/v1/plate-orders/',
|
||||
data,
|
||||
format='multipart'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertIn('plate_image', response.data)
|
||||
|
||||
# 验证数据库
|
||||
plate_order = printing_models.PlateOrder.objects.get(design_code='DESIGN_NEW')
|
||||
self.assertTrue(plate_order.plate_image)
|
||||
|
||||
@@ -10,6 +10,7 @@ from rest_framework.permissions import DjangoModelPermissions
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters import rest_framework as django_filters
|
||||
from django_filters import IsoDateTimeFilter
|
||||
from printing import models
|
||||
from basic_info.models import MerchantTypeEnum
|
||||
|
||||
@@ -526,8 +527,8 @@ class PlateOrderFilterSet(django_filters.FilterSet):
|
||||
style_name = django_filters.CharFilter(lookup_expr='icontains')
|
||||
plate_date_from = django_filters.DateFilter(field_name='plate_date', lookup_expr='gte')
|
||||
plate_date_to = django_filters.DateFilter(field_name='plate_date', lookup_expr='lte')
|
||||
required_completion_date_from = django_filters.DateFilter(field_name='required_completion_date', lookup_expr='gte')
|
||||
required_completion_date_to = django_filters.DateFilter(field_name='required_completion_date', lookup_expr='lte')
|
||||
required_completion_date_from = IsoDateTimeFilter(field_name='required_completion_date', lookup_expr='gte')
|
||||
required_completion_date_to = IsoDateTimeFilter(field_name='required_completion_date', lookup_expr='lte')
|
||||
created_date_from = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
|
||||
created_date_to = django_filters.DateFilter(field_name='created_at', lookup_expr='lte')
|
||||
|
||||
@@ -574,8 +575,8 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
||||
- style_name: 款式名称(模糊查询)
|
||||
- plate_date_from: 开版日期起始
|
||||
- plate_date_to: 开版日期结束
|
||||
- required_completion_date_from: 要求完成日期起始
|
||||
- required_completion_date_to: 要求完成日期结束
|
||||
- required_completion_date_from: 要求完成时间起始(含时分秒)
|
||||
- required_completion_date_to: 要求完成时间结束
|
||||
- created_date_from: 创建日期起始
|
||||
- created_date_to: 创建日期结束
|
||||
- search: 全文搜索(设计编号、款式名称、客户名称、面料)
|
||||
|
||||
@@ -1,18 +1,61 @@
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from rest_framework import status, views
|
||||
from rest_framework import status, views, serializers, pagination
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from business import services as business_services
|
||||
from business import models as business_models
|
||||
from .stock_change_views.mixins import StockChangeViewMixin
|
||||
|
||||
|
||||
class PurchaseOrderItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = business_models.PurchaseOrderItem
|
||||
fields = [
|
||||
'id', 'product', 'price', 'color', 'quantity', 'unit',
|
||||
'empty_diff_percent', 'quantity_of_rolls', 'num_of_rolls',
|
||||
'batch_number', 'remarks', 'created_at', 'updated_at',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
class PurchaseOrderSerializer(serializers.ModelSerializer):
|
||||
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
||||
operator_name = serializers.CharField(source='operator.name', read_only=True)
|
||||
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||
items = PurchaseOrderItemSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = business_models.PurchaseOrder
|
||||
fields = [
|
||||
'id', 'supplier', 'supplier_name', 'purchase_date', 'kind',
|
||||
'operator', 'operator_name', 'warehouse', 'warehouse_name',
|
||||
'status', 'remarks', 'created_at', 'updated_at', 'items',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'items', 'supplier_name', 'operator_name', 'warehouse_name']
|
||||
|
||||
|
||||
class PurchaseOrderPagination(pagination.LimitOffsetPagination):
|
||||
default_limit = 20
|
||||
max_limit = 100
|
||||
|
||||
|
||||
class PurchaseOrderView(StockChangeViewMixin, views.APIView):
|
||||
"""创建采购订单并触发入库任务"""
|
||||
"""采购订单查询与创建"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = PurchaseOrderPagination
|
||||
|
||||
def get(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
return self.permission_error_response('无权限访问')
|
||||
|
||||
merchant = request.user.employee.merchant
|
||||
queryset = business_models.PurchaseOrder.objects.filter(merchant=merchant).prefetch_related('items', 'supplier', 'operator', 'warehouse')
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset.order_by('-created_at'), request, view=self)
|
||||
serializer = PurchaseOrderSerializer(page, many=True)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
if not self.check_employee_permission(request):
|
||||
@@ -22,9 +65,8 @@ class PurchaseOrderView(StockChangeViewMixin, views.APIView):
|
||||
data = request.data or {}
|
||||
|
||||
supplier_id = data.get('supplier')
|
||||
warehouse_id = data.get('warehouse')
|
||||
warehouse_id = data.get('warehouse_id') or data.get('warehouse')
|
||||
order_date = data.get('order_date')
|
||||
total_amount = data.get('total_amount')
|
||||
items = data.get('items', [])
|
||||
remarks = data.get('remarks', '')
|
||||
|
||||
@@ -32,6 +74,10 @@ class PurchaseOrderView(StockChangeViewMixin, views.APIView):
|
||||
return Response({'error': '缺少供应商 ID'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not warehouse_id:
|
||||
return Response({'error': '缺少仓库 ID'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not order_date:
|
||||
return Response({'error': '缺少 order_date'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not isinstance(items, list) or not items:
|
||||
return Response({'error': 'items 需要为非空数组'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
supplier = basic_models.Supplier.objects.get(id=supplier_id, merchant=merchant)
|
||||
@@ -39,22 +85,19 @@ class PurchaseOrderView(StockChangeViewMixin, views.APIView):
|
||||
return Response({'error': f'供应商 {supplier_id} 不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
basic_models.WareHouse.objects.get(id=warehouse_id, merchant=merchant)
|
||||
warehouse = basic_models.WareHouse.objects.get(id=warehouse_id, merchant=merchant)
|
||||
except basic_models.WareHouse.DoesNotExist:
|
||||
return Response({'error': f'仓库 {warehouse_id} 不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
total_amount_decimal = Decimal(str(total_amount))
|
||||
except (InvalidOperation, TypeError):
|
||||
return Response({'error': 'total_amount 必须为合法数值'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
operator = request.user.employee
|
||||
|
||||
try:
|
||||
purchase_order = business_services.create_purchase_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=order_date,
|
||||
total_amount=total_amount_decimal,
|
||||
warehouse_id=warehouse_id,
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=items,
|
||||
remarks=remarks,
|
||||
created_by=request.user,
|
||||
|
||||
Reference in New Issue
Block a user