1
0
forked from erp-dev/erp

fix: printing api problem

This commit is contained in:
2026-06-28 21:39:53 +08:00
parent cb6589c884
commit cc4607725c
4 changed files with 154 additions and 3 deletions

View File

@@ -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 按当前状态分组的数量汇总

View File

@@ -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(

View File

@@ -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.