forked from erp-dev/erp
feat: added n-to-1 relation between sales_order_item and printing_job model
This commit is contained in:
158
api_v2/tests.py
158
api_v2/tests.py
@@ -1,8 +1,15 @@
|
||||
from decimal import Decimal
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.test import APIClient
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from rest_framework.test import APIClient, APIRequestFactory
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from printing import models as printing_models
|
||||
from business import models as business_models
|
||||
from api_v2.views.printing import PrintingJobByCustomerView
|
||||
|
||||
|
||||
class QuickCreateEmployeeUserAPITest(TestCase):
|
||||
@@ -51,3 +58,152 @@ class QuickCreateEmployeeUserAPITest(TestCase):
|
||||
response = self.client.post(self.url, payload, format='json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('商户不存在', str(response.data))
|
||||
|
||||
|
||||
class PrintingJobByCustomerAPITest(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.client = APIClient()
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='印染商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='客户X',
|
||||
created_by=None,
|
||||
)
|
||||
category = basic_models.ProductCategory.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='品类',
|
||||
product_prefix='FAB',
|
||||
)
|
||||
self.product = basic_models.Product.objects.create(
|
||||
merchant=self.merchant,
|
||||
category=category,
|
||||
name='产品A',
|
||||
human_id='FAB-001',
|
||||
width_size=Decimal('150.00'),
|
||||
color='红色',
|
||||
unit=basic_models.ProductUnitEnum.METER,
|
||||
)
|
||||
self.other_product = basic_models.Product.objects.create(
|
||||
merchant=self.merchant,
|
||||
category=category,
|
||||
name='产品B',
|
||||
human_id='FAB-002',
|
||||
width_size=Decimal('160.00'),
|
||||
color='蓝色',
|
||||
unit=basic_models.ProductUnitEnum.METER,
|
||||
)
|
||||
self.printing_order = printing_models.PrintingOrder.objects.create(
|
||||
customer=self.customer,
|
||||
fabric='棉',
|
||||
width='150cm',
|
||||
)
|
||||
self.printing_order_other = printing_models.PrintingOrder.objects.create(
|
||||
customer=self.customer,
|
||||
fabric='麻',
|
||||
width='160cm',
|
||||
)
|
||||
|
||||
tz = timezone.get_default_timezone()
|
||||
in_range = timezone.make_aware(datetime.datetime(2025, 12, 5, 10, 0, 0), tz)
|
||||
out_range = timezone.make_aware(datetime.datetime(2025, 11, 20, 10, 0, 0), tz)
|
||||
|
||||
self.job_in_range = printing_models.PrintingJob.objects.create(
|
||||
printing_order=self.printing_order,
|
||||
product=self.product,
|
||||
quantity=10,
|
||||
unit='米',
|
||||
)
|
||||
printing_models.PrintingJob.objects.filter(id=self.job_in_range.id).update(created_at=in_range)
|
||||
|
||||
self.job_out_range = printing_models.PrintingJob.objects.create(
|
||||
printing_order=self.printing_order_other,
|
||||
product=self.other_product,
|
||||
quantity=20,
|
||||
unit='米',
|
||||
)
|
||||
printing_models.PrintingJob.objects.filter(id=self.job_out_range.id).update(created_at=out_range)
|
||||
|
||||
# 关联销售单,验证 billed_quantity
|
||||
warehouse = basic_models.WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='仓库A',
|
||||
mode=basic_models.WareHouseModeEnum.UNRESTRICTED,
|
||||
)
|
||||
operator = basic_models.Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='操作员',
|
||||
)
|
||||
sales_order = business_models.SalesOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
sales_date=datetime.date(2025, 12, 5),
|
||||
operator=operator,
|
||||
warehouse=warehouse,
|
||||
)
|
||||
business_models.SalesOrderItem.objects.create(
|
||||
sales_order=sales_order,
|
||||
product=self.product,
|
||||
price=Decimal('10'),
|
||||
quantity=Decimal('12.5'),
|
||||
unit='米',
|
||||
empty_diff_percent=Decimal('0'),
|
||||
num_of_rolls=1,
|
||||
printing_job=self.job_in_range,
|
||||
)
|
||||
business_models.SalesOrderItem.objects.create(
|
||||
sales_order=sales_order,
|
||||
product=self.product,
|
||||
price=Decimal('8'),
|
||||
quantity=Decimal('7.5'),
|
||||
unit='米',
|
||||
empty_diff_percent=Decimal('0'),
|
||||
num_of_rolls=1,
|
||||
printing_job=self.job_in_range,
|
||||
)
|
||||
|
||||
self.view = PrintingJobByCustomerView.as_view()
|
||||
|
||||
def _get(self, params):
|
||||
request = self.factory.get('/api/v2/printing/jobs/', params)
|
||||
return self.view(request)
|
||||
|
||||
def test_basic_date_and_customer_filter(self):
|
||||
resp = self._get({
|
||||
'customer_id': self.customer.id,
|
||||
'date_from': '2025-12-01',
|
||||
'date_to': '2025-12-10',
|
||||
})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.data), 1)
|
||||
self.assertEqual(resp.data[0]['id'], self.job_in_range.id)
|
||||
self.assertEqual(resp.data[0]['billed_quantity'], '20.00')
|
||||
|
||||
def test_filter_by_printing_order(self):
|
||||
resp = self._get({
|
||||
'customer_id': self.customer.id,
|
||||
'date_from': '2025-12-01',
|
||||
'date_to': '2025-12-10',
|
||||
'printing_order': self.printing_order.id,
|
||||
})
|
||||
self.assertEqual(len(resp.data), 1)
|
||||
self.assertEqual(resp.data[0]['printing_order'], self.printing_order.id)
|
||||
|
||||
def test_filter_by_product_fields(self):
|
||||
resp = self._get({
|
||||
'customer_id': self.customer.id,
|
||||
'date_from': '2025-12-01',
|
||||
'date_to': '2025-12-10',
|
||||
'product_id': self.product.id,
|
||||
'product_name': '产品A',
|
||||
'product_human_id': 'FAB-001',
|
||||
'product_width_size': '150',
|
||||
'product_color': '红',
|
||||
})
|
||||
self.assertEqual(len(resp.data), 1)
|
||||
data = resp.data[0]
|
||||
self.assertEqual(data['product'], self.product.id)
|
||||
self.assertEqual(data['billed_quantity'], '20.00')
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from api_v2.views import QuickCreateEmployeeUserView
|
||||
from api_v2.views import QuickCreateEmployeeUserView, PrintingJobByCustomerView
|
||||
|
||||
urlpatterns = [
|
||||
path('users/quick-create/', QuickCreateEmployeeUserView.as_view(), name='api_v2_user_quick_create'),
|
||||
path('printing-jobs/by-customer/', PrintingJobByCustomerView.as_view(), name='api_v2_printing_job_by_customer'),
|
||||
]
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ api_v2 视图包。
|
||||
"""
|
||||
|
||||
from .users import QuickCreateEmployeeUserView
|
||||
from .printing import PrintingJobByCustomerView, PrintingJobV2Serializer
|
||||
|
||||
__all__ = ['QuickCreateEmployeeUserView']
|
||||
__all__ = [
|
||||
'QuickCreateEmployeeUserView',
|
||||
'PrintingJobByCustomerView',
|
||||
'PrintingJobV2Serializer',
|
||||
]
|
||||
|
||||
|
||||
140
api_v2/views/printing.py
Normal file
140
api_v2/views/printing.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers, status, permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from printing import models as printing_models
|
||||
from api_man.serializers import ProductSerializer
|
||||
|
||||
|
||||
class PrintingJobV2Serializer(serializers.ModelSerializer):
|
||||
"""v2 独立的印染任务序列化器,包含开单数量"""
|
||||
|
||||
billed_quantity = serializers.DecimalField(max_digits=18, decimal_places=2, read_only=True)
|
||||
business_object_id = serializers.SerializerMethodField()
|
||||
width = serializers.SerializerMethodField()
|
||||
fabric = serializers.SerializerMethodField()
|
||||
product = ProductSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = printing_models.PrintingJob
|
||||
fields = [
|
||||
'id',
|
||||
'printing_order',
|
||||
'product',
|
||||
'work_state',
|
||||
'quantity',
|
||||
'width',
|
||||
'fabric',
|
||||
'unit',
|
||||
'size',
|
||||
'pieces',
|
||||
'description',
|
||||
'business_object_id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'billed_quantity',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'billed_quantity']
|
||||
|
||||
def get_business_object_id(self, obj):
|
||||
return obj.business_object_id
|
||||
|
||||
def get_width(self, obj: printing_models.PrintingJob) -> float:
|
||||
return obj.printing_order.width
|
||||
|
||||
def get_fabric(self, obj: printing_models.PrintingJob) -> str:
|
||||
return obj.printing_order.fabric
|
||||
|
||||
|
||||
class PrintingJobByCustomerView(APIView):
|
||||
"""
|
||||
按客户与日期范围查询印染任务。
|
||||
|
||||
必填 query 参数:
|
||||
- customer_id: 客户 ID
|
||||
- date_from: 开始日期 (YYYY-MM-DD)
|
||||
- date_to: 结束日期 (YYYY-MM-DD),闭区间,包含 23:59:59
|
||||
可选过滤:
|
||||
- printing_order: 按印染主订单 ID
|
||||
- product_id / product_name / product_human_id / product_width_size / product_color
|
||||
"""
|
||||
|
||||
serializer_class = PrintingJobV2Serializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
qp = request.query_params
|
||||
customer_id = qp.get('customer_id')
|
||||
date_from = qp.get('date_from')
|
||||
date_to = qp.get('date_to')
|
||||
printing_order_id = qp.get('printing_order')
|
||||
product_id = qp.get('product_id')
|
||||
product_name = qp.get('product_name')
|
||||
product_human_id = qp.get('product_human_id')
|
||||
product_width_size = qp.get('product_width_size')
|
||||
product_color = qp.get('product_color')
|
||||
|
||||
if not customer_id:
|
||||
return Response({'detail': 'customer_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not date_from or not date_to:
|
||||
return Response({'detail': 'date_from 与 date_to 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
customer_id_int = int(customer_id)
|
||||
except (TypeError, ValueError):
|
||||
return Response({'detail': 'customer_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
start_date = datetime.datetime.strptime(date_from, '%Y-%m-%d').date()
|
||||
end_date = datetime.datetime.strptime(date_to, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return Response({'detail': '日期格式需为 YYYY-MM-DD'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 闭区间:包含当日 00:00:00 和 23:59:59.999999
|
||||
start_dt = datetime.datetime.combine(start_date, datetime.time.min)
|
||||
end_dt = datetime.datetime.combine(end_date, datetime.time.max)
|
||||
|
||||
if timezone.is_naive(start_dt):
|
||||
start_dt = timezone.make_aware(start_dt, timezone.get_default_timezone())
|
||||
if timezone.is_naive(end_dt):
|
||||
end_dt = timezone.make_aware(end_dt, timezone.get_default_timezone())
|
||||
|
||||
queryset = printing_models.PrintingJob.objects.select_related('printing_order', 'product').filter(
|
||||
printing_order__customer_id=customer_id_int,
|
||||
created_at__gte=start_dt,
|
||||
created_at__lte=end_dt,
|
||||
)
|
||||
|
||||
# 可选过滤:printing_order
|
||||
if printing_order_id:
|
||||
try:
|
||||
queryset = queryset.filter(printing_order_id=int(printing_order_id))
|
||||
except (TypeError, ValueError):
|
||||
return Response({'detail': 'printing_order 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 可选过滤:product
|
||||
if product_id:
|
||||
try:
|
||||
queryset = queryset.filter(product_id=int(product_id))
|
||||
except (TypeError, ValueError):
|
||||
return Response({'detail': 'product_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if product_name:
|
||||
queryset = queryset.filter(product__name__icontains=product_name)
|
||||
if product_human_id:
|
||||
queryset = queryset.filter(product__human_id__icontains=product_human_id)
|
||||
if product_width_size:
|
||||
try:
|
||||
width_decimal = float(product_width_size)
|
||||
except (TypeError, ValueError):
|
||||
return Response({'detail': 'product_width_size 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
queryset = queryset.filter(product__width_size=width_decimal)
|
||||
if product_color:
|
||||
queryset = queryset.filter(product__color__icontains=product_color)
|
||||
|
||||
queryset = queryset.order_by('-created_at')
|
||||
|
||||
serializer = self.serializer_class(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
Reference in New Issue
Block a user