forked from erp-dev/erp
fix: printing api problem
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
Printing API 序列化器
|
Printing API 序列化器
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import empty
|
from rest_framework.fields import empty
|
||||||
|
|
||||||
@@ -149,8 +150,8 @@ class PlateImageInputSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
class PrintingOrderListSerializer(serializers.ModelSerializer):
|
class PrintingOrderListSerializer(serializers.ModelSerializer):
|
||||||
"""印染订单列表序列化器"""
|
"""印染订单列表序列化器"""
|
||||||
customer_name = serializers.CharField(source='customer.name', read_only=True)
|
customer_name = serializers.SerializerMethodField()
|
||||||
customer_phone = serializers.CharField(source='customer.mobile', read_only=True)
|
customer_phone = serializers.SerializerMethodField()
|
||||||
process_name = serializers.CharField(source='process.name', read_only=True)
|
process_name = serializers.CharField(source='process.name', read_only=True)
|
||||||
progress = serializers.IntegerField(read_only=True)
|
progress = serializers.IntegerField(read_only=True)
|
||||||
jobs_status_summary = serializers.SerializerMethodField()
|
jobs_status_summary = serializers.SerializerMethodField()
|
||||||
@@ -177,6 +178,24 @@ class PrintingOrderListSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count', 'merchant_id']
|
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count', 'merchant_id']
|
||||||
|
|
||||||
|
def get_customer_name(self, obj):
|
||||||
|
if hasattr(obj, 'customer_name'):
|
||||||
|
return obj.customer_name
|
||||||
|
try:
|
||||||
|
customer = getattr(obj, 'customer', None)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return None
|
||||||
|
return getattr(customer, 'name', None)
|
||||||
|
|
||||||
|
def get_customer_phone(self, obj):
|
||||||
|
if hasattr(obj, 'customer_phone'):
|
||||||
|
return obj.customer_phone
|
||||||
|
try:
|
||||||
|
customer = getattr(obj, 'customer', None)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return None
|
||||||
|
return getattr(customer, 'mobile', None)
|
||||||
|
|
||||||
def get_jobs_status_summary(self, obj):
|
def get_jobs_status_summary(self, obj):
|
||||||
"""
|
"""
|
||||||
返回订单下所有 PrintingJob 按当前状态分组的数量汇总
|
返回订单下所有 PrintingJob 按当前状态分组的数量汇总
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ PrintingOrder API 测试
|
|||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.db import connection
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
@@ -170,6 +171,57 @@ class PrintingOrderAPITestCase(TestCase):
|
|||||||
# 分页响应格式
|
# 分页响应格式
|
||||||
self.assertEqual(response.data['count'], 2)
|
self.assertEqual(response.data['count'], 2)
|
||||||
|
|
||||||
|
def test_list_pagination_keeps_rows_with_dangling_customer_reference(self):
|
||||||
|
"""历史脏数据 customer_id 悬空时,列表分页不应被 INNER JOIN 推空。"""
|
||||||
|
base = timezone.now()
|
||||||
|
dangling = printing_models.PrintingOrder.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='悬空客户订单',
|
||||||
|
width='150cm',
|
||||||
|
)
|
||||||
|
middle = printing_models.PrintingOrder.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='中间订单',
|
||||||
|
width='150cm',
|
||||||
|
)
|
||||||
|
oldest = printing_models.PrintingOrder.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='最旧订单',
|
||||||
|
width='150cm',
|
||||||
|
)
|
||||||
|
printing_models.PrintingOrder.objects.filter(id=dangling.id).update(
|
||||||
|
created_at=base + timezone.timedelta(minutes=2),
|
||||||
|
)
|
||||||
|
printing_models.PrintingOrder.objects.filter(id=middle.id).update(
|
||||||
|
created_at=base + timezone.timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
printing_models.PrintingOrder.objects.filter(id=oldest.id).update(created_at=base)
|
||||||
|
|
||||||
|
missing_customer_id = 99999999
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
'UPDATE printing_printingorder SET customer_id = %s WHERE id = %s',
|
||||||
|
[missing_customer_id, dangling.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.client.get(
|
||||||
|
'/api/v1/printing-orders/?limit=1&offset=2&ordering=-created_at',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['count'], 3)
|
||||||
|
self.assertEqual(len(response.data['results']), 1)
|
||||||
|
self.assertEqual(response.data['results'][0]['id'], oldest.id)
|
||||||
|
finally:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
'UPDATE printing_printingorder SET customer_id = %s WHERE id = %s',
|
||||||
|
[self.customer.id, dangling.id],
|
||||||
|
)
|
||||||
|
|
||||||
def test_list_printing_orders_default_excludes_invalid(self):
|
def test_list_printing_orders_default_excludes_invalid(self):
|
||||||
"""默认不返回作废订单;显式传 is_invalid=true 时可查询作废订单"""
|
"""默认不返回作废订单;显式传 is_invalid=true 时可查询作废订单"""
|
||||||
printing_models.PrintingOrder.objects.create(
|
printing_models.PrintingOrder.objects.create(
|
||||||
@@ -443,6 +495,68 @@ class PrintingOrderAPITestCase(TestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertIn('已经标记', response.data['detail'])
|
self.assertIn('已经标记', response.data['detail'])
|
||||||
|
|
||||||
|
def _create_product_for_job(self, name='测试产品'):
|
||||||
|
category = basic_models.ProductCategory.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
name=f'{name}类别',
|
||||||
|
product_prefix='JOB',
|
||||||
|
)
|
||||||
|
return basic_models.Product.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
category=category,
|
||||||
|
name=name,
|
||||||
|
unit=basic_models.ProductUnitEnum.METER,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_printing_order_ids_returns_job_ids(self):
|
||||||
|
"""测试获取订单下所有 PrintingJob ID。"""
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm',
|
||||||
|
)
|
||||||
|
product = self._create_product_for_job()
|
||||||
|
job1 = printing_models.PrintingJob.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
printing_order=order,
|
||||||
|
product=product,
|
||||||
|
quantity=10,
|
||||||
|
unit='米',
|
||||||
|
)
|
||||||
|
job2 = printing_models.PrintingJob.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
printing_order=order,
|
||||||
|
product=product,
|
||||||
|
quantity=20,
|
||||||
|
unit='米',
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/v1/printing-orders/{order.id}/ids/')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, {'ids': [job1.id, job2.id]})
|
||||||
|
|
||||||
|
def test_printing_order_ids_returns_empty_list_when_no_jobs(self):
|
||||||
|
"""订单存在但没有任务时返回空数组。"""
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm',
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/v1/printing-orders/{order.id}/ids/')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, {'ids': []})
|
||||||
|
|
||||||
|
def test_printing_order_ids_returns_404_when_order_missing(self):
|
||||||
|
"""订单不存在时返回 404。"""
|
||||||
|
response = self.client.get('/api/v1/printing-orders/99999999/ids/')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
def test_filter_by_customer(self):
|
def test_filter_by_customer(self):
|
||||||
"""测试按客户过滤"""
|
"""测试按客户过滤"""
|
||||||
customer2 = basic_models.Customer.objects.create(
|
customer2 = basic_models.Customer.objects.create(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from datetime import datetime, time, timedelta
|
|||||||
|
|
||||||
from flower.viewsets import LimitedModelViewSet
|
from flower.viewsets import LimitedModelViewSet
|
||||||
from printing import models
|
from printing import models
|
||||||
|
from basic_info import models as basic_models
|
||||||
from basic_info.models import MerchantTypeEnum
|
from basic_info.models import MerchantTypeEnum
|
||||||
|
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
@@ -301,8 +302,16 @@ class PrintingOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet):
|
|||||||
):
|
):
|
||||||
queryset = queryset.filter(is_invalid=False)
|
queryset = queryset.filter(is_invalid=False)
|
||||||
|
|
||||||
if self.action in ["list", "retrieve", "by_external_order_id"]:
|
if self.action == "list":
|
||||||
|
customer_qs = basic_models.Customer.objects.filter(id=OuterRef("customer_id"))
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
customer_name=Subquery(customer_qs.values("name")[:1]),
|
||||||
|
customer_phone=Subquery(customer_qs.values("mobile")[:1]),
|
||||||
|
)
|
||||||
|
elif self.action in ["retrieve", "by_external_order_id"]:
|
||||||
queryset = queryset.select_related("customer")
|
queryset = queryset.select_related("customer")
|
||||||
|
|
||||||
|
if self.action in ["list", "retrieve", "by_external_order_id"]:
|
||||||
# 预取 printing_jobs 及其 business_object,用于状态汇总统计
|
# 预取 printing_jobs 及其 business_object,用于状态汇总统计
|
||||||
# 同时预取 state_logs 和 process 相关数据以减少 job.status 属性调用时的 N+1 查询
|
# 同时预取 state_logs 和 process 相关数据以减少 job.status 属性调用时的 N+1 查询
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
@@ -350,6 +359,15 @@ class PrintingOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet):
|
|||||||
serializer = self.get_serializer(printing_order)
|
serializer = self.get_serializer(printing_order)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=["get"], url_path="ids")
|
||||||
|
def ids(self, request, pk=None):
|
||||||
|
"""返回该印染订单下所有 PrintingJob ID。"""
|
||||||
|
printing_order = self.get_object()
|
||||||
|
ids = list(
|
||||||
|
printing_order.printing_jobs.order_by("id").values_list("id", flat=True)
|
||||||
|
)
|
||||||
|
return Response({"ids": ids})
|
||||||
|
|
||||||
@action(detail=True, methods=["post"])
|
@action(detail=True, methods=["post"])
|
||||||
def invalidate(self, request, pk=None):
|
def invalidate(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user