diff --git a/api_v1/admin.py b/api_v1/admin.py index 046820e..2cca964 100644 --- a/api_v1/admin.py +++ b/api_v1/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin +from django.contrib.admin import action from api_v1.models import UploadedFile +from .tasks import backup_database @admin.register(UploadedFile) @@ -10,7 +12,12 @@ class UploadedFileAdmin(admin.ModelAdmin): readonly_fields = ['created_at', 'file_size', 'content_type'] date_hierarchy = 'created_at' ordering = ['-created_at'] + actions = ['backup_database'] def get_queryset(self, request): # 在管理界面显示所有文件,包括已删除的 return super().get_queryset(request).select_related('owner') + + @action(description='备份数据库') + def backup_database(self, request, queryset): + backup_database.delay() diff --git a/api_v1/enums.py b/api_v1/enums.py new file mode 100644 index 0000000..f1e4dde --- /dev/null +++ b/api_v1/enums.py @@ -0,0 +1,11 @@ +from django.db import models + + +class PrintCountObjectType(models.TextChoices): + PRINTING_ORDER = 'printing_order', '印染订单' + PLATE_ORDER = 'plate_order', '开版订单' + + @classmethod + def values(cls): + return [choice.value for choice in cls] + diff --git a/api_v1/serializers.py b/api_v1/serializers.py index 72d855a..4a3adbf 100644 --- a/api_v1/serializers.py +++ b/api_v1/serializers.py @@ -4,6 +4,7 @@ from typing import List, Dict, Any from stock import models as stock_models from basic_info import models as basic_info_models from . import models as api_models +from .enums import PrintCountObjectType class ProductStockChangeSerializer(serializers.Serializer): @@ -193,4 +194,25 @@ class FileUploadSerializer(serializers.Serializer): if value.size > max_size: raise serializers.ValidationError(f'文件大小不能超过 {max_size // (1024*1024)}MB') - return value \ No newline at end of file + return value + + +class PrintCountDeltaSerializer(serializers.Serializer): + """打印次数增量请求""" + + object_type = serializers.ChoiceField(choices=PrintCountObjectType.choices) + object_id = serializers.IntegerField(min_value=1) + delta = serializers.CharField(required=False, allow_null=True, allow_blank=True) + + def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]: + raw_delta = attrs.get('delta') + try: + delta_value = int(raw_delta) + except (TypeError, ValueError): + delta_value = 1 + + if delta_value < 1: + delta_value = 1 + + attrs['delta'] = delta_value + return attrs \ No newline at end of file diff --git a/api_v1/test_print_count_delta.py b/api_v1/test_print_count_delta.py new file mode 100644 index 0000000..0a13306 --- /dev/null +++ b/api_v1/test_print_count_delta.py @@ -0,0 +1,91 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from basic_info.models import Merchant, MerchantTypeEnum, Customer +from printing import models as printing_models + + +class PrintCountDeltaAPITestCase(TestCase): + """打印次数增量接口测试""" + + url = '/api/v1/print-count/delta/' + + def setUp(self): + self.client = APIClient() + self.user = get_user_model().objects.create_user(username='tester', password='test123') + self.client.force_authenticate(self.user) + + self.merchant = Merchant.objects.create(name='测试商户', type=MerchantTypeEnum.FACTORY) + self.customer = Customer.objects.create( + merchant=self.merchant, + name='客户甲', + mobile='13900000000', + created_by=None, + ) + + self.printing_order = printing_models.PrintingOrder.objects.create( + customer=self.customer, + fabric='棉布', + width='150cm', + ) + self.plate_order = printing_models.PlateOrder.objects.create( + customer=self.customer, + design_code='DES-001', + ) + + def test_increment_printing_order_with_delta(self): + response = self.client.post( + self.url, + { + 'object_type': 'printing_order', + 'object_id': self.printing_order.id, + 'delta': 3, + }, + format='json', + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.printing_order.refresh_from_db() + self.assertEqual(self.printing_order.print_count, 3) + self.assertEqual(response.data['print_count'], 3) + self.assertEqual(response.data['delta'], 3) + + def test_delta_defaults_to_one_when_invalid(self): + response = self.client.post( + self.url, + { + 'object_type': 'plate_order', + 'object_id': self.plate_order.id, + 'delta': 'invalid', + }, + format='json', + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.plate_order.refresh_from_db() + self.assertEqual(self.plate_order.print_count, 1) + self.assertEqual(response.data['delta'], 1) + + def test_invalid_object_type_returns_400(self): + response = self.client.post( + self.url, + { + 'object_type': 'unknown_type', + 'object_id': self.printing_order.id, + }, + format='json', + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('object_type', response.data) + + def test_not_found_returns_404(self): + response = self.client.post( + self.url, + { + 'object_type': 'printing_order', + 'object_id': 999999, + }, + format='json', + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + diff --git a/api_v1/tests.py b/api_v1/tests.py index d116a98..23dbdc8 100644 --- a/api_v1/tests.py +++ b/api_v1/tests.py @@ -139,11 +139,16 @@ class PurchaseOrderAPITestCase(TestCase): def setUp(self): self.merchant = Merchant.objects.create(name='PO商户', type=MerchantTypeEnum.FACTORY) self.supplier = Supplier.objects.create(merchant=self.merchant, name='供应商A') - self.warehouse = WareHouse.objects.create( + self.warehouse_strict = WareHouse.objects.create( merchant=self.merchant, - name='主仓', + name='严进仓', mode=WareHouseModeEnum.RESTRICT_IN, ) + self.warehouse_relaxed = WareHouse.objects.create( + merchant=self.merchant, + name='宽进仓', + mode=WareHouseModeEnum.UNRESTRICTED, + ) category = ProductCategory.objects.create( merchant=self.merchant, name='品类', @@ -164,39 +169,66 @@ class PurchaseOrderAPITestCase(TestCase): ) self.client = APIClient() self.client.force_authenticate(user=self.user) - self.payload = { + self.strict_payload = { 'supplier': self.supplier.id, - 'warehouse': self.warehouse.id, + 'warehouse': self.warehouse_strict.id, 'order_date': '2025-11-26', - 'total_amount': '1500.00', 'items': [ { 'product_id': self.product.id, - 'quantities': ['10.0', '5.0'], + 'numbers': [10, 5], + 'price': '12.5', + 'unit': '米', } ], 'remarks': '接口测试', } + self.relaxed_payload = { + 'supplier': self.supplier.id, + 'warehouse': self.warehouse_relaxed.id, + 'order_date': '2025-11-26', + 'items': [ + { + 'product_id': self.product.id, + 'quantity': 120, + 'num_of_rolls': 3, + 'price': '10.5', + } + ], + } - def test_create_purchase_order_success(self): + def test_create_purchase_order_success_strict(self): with patch('business.services.create_purchase_order_stock_entries.delay') as mock_delay: - response = self.client.post('/api/v1/purchase-orders/', self.payload, format='json') + response = self.client.post('/api/v1/purchase-orders/', self.strict_payload, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertIn('id', response.data) mock_delay.assert_called_once() def test_create_purchase_order_invalid_supplier(self): - payload = {**self.payload, 'supplier': 999} + payload = {**self.strict_payload, 'supplier': 999} response = self.client.post('/api/v1/purchase-orders/', payload, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn('不存在', response.data['error']) def test_create_purchase_order_unauthenticated(self): self.client.force_authenticate(user=None) - response = self.client.post('/api/v1/purchase-orders/', self.payload, format='json') + response = self.client.post('/api/v1/purchase-orders/', self.strict_payload, format='json') self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + def test_create_purchase_order_relaxed_mode(self): + with patch('business.services.create_purchase_order_stock_entries.delay') as mock_delay: + response = self.client.post('/api/v1/purchase-orders/', self.relaxed_payload, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + mock_delay.assert_called_once() + + def test_mode_mismatch_raises(self): + payload = {**self.relaxed_payload} + payload['warehouse'] = self.warehouse_strict.id # 严进仓却传宽进参数 + response = self.client.post('/api/v1/purchase-orders/', payload, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('严进模式', response.data['error']) + @override_settings( CELERY_TASK_ALWAYS_EAGER=True, diff --git a/api_v1/urls.py b/api_v1/urls.py index fc5eb29..3810f90 100644 --- a/api_v1/urls.py +++ b/api_v1/urls.py @@ -9,6 +9,7 @@ from .views import ( product_image, stateflow, users, + print_count, ) from .views.stock_change_views.snapshot import StockSnapshotListView from .views.printing.views import PrintingOrderViewSet, PrintingJobViewSet, PlateOrderViewSet @@ -57,6 +58,7 @@ urlpatterns = [ path('inventory/', inventory.InventoryAPIView.as_view(), name='inventory'), path('purchase-orders/', purchase_order.PurchaseOrderView.as_view(), name='purchase_orders'), path('health/', healthy.HealthCheckView.as_view(), name='health_check'), + path('print-count/delta/', print_count.adjust_print_count, name='print_count_delta'), # 产品图片上传 API path('products//image/', product_image.ProductImageUploadView.as_view(), name='product_image_upload'), diff --git a/api_v1/views/print_count.py b/api_v1/views/print_count.py new file mode 100644 index 0000000..d38a5f7 --- /dev/null +++ b/api_v1/views/print_count.py @@ -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() + diff --git a/api_v1/views/printing/API.md b/api_v1/views/printing/API.md index 0836550..86a339d 100644 --- a/api_v1/views/printing/API.md +++ b/api_v1/views/printing/API.md @@ -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`: 排序字段。 diff --git a/api_v1/views/printing/serializers.py b/api_v1/views/printing/serializers.py index 74cfcf6..5c30514 100644 --- a/api_v1/views/printing/serializers.py +++ b/api_v1/views/printing/serializers.py @@ -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) diff --git a/api_v1/views/printing/test_plate_order_api.py b/api_v1/views/printing/test_plate_order_api.py index f9dc9ec..9382170 100644 --- a/api_v1/views/printing/test_plate_order_api.py +++ b/api_v1/views/printing/test_plate_order_api.py @@ -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""" diff --git a/api_v1/views/printing/test_plate_order_file_upload.py b/api_v1/views/printing/test_plate_order_file_upload.py deleted file mode 100644 index 2d9935c..0000000 --- a/api_v1/views/printing/test_plate_order_file_upload.py +++ /dev/null @@ -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) - diff --git a/api_v1/views/printing/views.py b/api_v1/views/printing/views.py index 215a969..fece15f 100644 --- a/api_v1/views/printing/views.py +++ b/api_v1/views/printing/views.py @@ -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: 全文搜索(设计编号、款式名称、客户名称、面料) diff --git a/api_v1/views/purchase_order.py b/api_v1/views/purchase_order.py index b8a20eb..2d8fefa 100644 --- a/api_v1/views/purchase_order.py +++ b/api_v1/views/purchase_order.py @@ -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, diff --git a/basic_info/admin.py b/basic_info/admin.py index 1778c81..90b549a 100644 --- a/basic_info/admin.py +++ b/basic_info/admin.py @@ -38,6 +38,21 @@ class QuickInputAdmin(admin.ModelAdmin): list_filter = ('group',) +@admin.register(models.MerchantSetting) +class MerchantSettingAdmin(admin.ModelAdmin): + list_display = ( + 'merchant', 'key', 'value', + 'type', 'val_str', 'val_int', + 'val_float', 'val_bool', 'description', + ) + list_filter = ('merchant', 'type') + search_fields = ('merchant__name', 'key', 'description') + + @admin.display(description='设置值') + def _value(self, obj): + return obj.value() + + @admin.register(models.UserProfile) class UserProfileAdmin(AdminBase): list_display = ('user', 'merchant') diff --git a/basic_info/migrations/0014_merchantsetting.py b/basic_info/migrations/0014_merchantsetting.py new file mode 100644 index 0000000..ba4820d --- /dev/null +++ b/basic_info/migrations/0014_merchantsetting.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.7 on 2025-11-28 06:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0013_alter_customer_options'), + ] + + operations = [ + migrations.CreateModel( + name='MerchantSetting', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('key', models.CharField(max_length=100, verbose_name='设置项')), + ('val', models.CharField(max_length=100, verbose_name='设置值')), + ('description', models.TextField(blank=True, null=True, verbose_name='备注')), + ('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='settings', to='basic_info.merchant', verbose_name='所属商户')), + ], + options={ + 'verbose_name': '商户设置', + 'verbose_name_plural': '商户设置', + }, + ), + ] diff --git a/basic_info/migrations/0015_remove_merchantsetting_val_merchantsetting_val_bool_and_more.py b/basic_info/migrations/0015_remove_merchantsetting_val_merchantsetting_val_bool_and_more.py new file mode 100644 index 0000000..0f36872 --- /dev/null +++ b/basic_info/migrations/0015_remove_merchantsetting_val_merchantsetting_val_bool_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.7 on 2025-11-28 06:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0014_merchantsetting'), + ] + + operations = [ + migrations.RemoveField( + model_name='merchantsetting', + name='val', + ), + migrations.AddField( + model_name='merchantsetting', + name='val_bool', + field=models.BooleanField(blank=True, null=True, verbose_name='设置值(布尔值)'), + ), + migrations.AddField( + model_name='merchantsetting', + name='val_float', + field=models.FloatField(blank=True, null=True, verbose_name='设置值(浮点数)'), + ), + migrations.AddField( + model_name='merchantsetting', + name='val_int', + field=models.IntegerField(blank=True, null=True, verbose_name='设置值(整数)'), + ), + migrations.AddField( + model_name='merchantsetting', + name='val_str', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='设置值(字符串)'), + ), + ] diff --git a/basic_info/migrations/0016_merchantsetting_type.py b/basic_info/migrations/0016_merchantsetting_type.py new file mode 100644 index 0000000..b223906 --- /dev/null +++ b/basic_info/migrations/0016_merchantsetting_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-28 06:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0015_remove_merchantsetting_val_merchantsetting_val_bool_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='merchantsetting', + name='type', + field=models.CharField(choices=[('str', '字符串'), ('int', '整数'), ('float', '浮点数'), ('bool', '布尔值')], default='str', max_length=20, verbose_name='设置类型'), + ), + ] diff --git a/basic_info/models.py b/basic_info/models.py index 068ca93..19bc05b 100644 --- a/basic_info/models.py +++ b/basic_info/models.py @@ -452,3 +452,52 @@ class VehicleTransportRecord(ModelBase): class Meta: verbose_name = '司机车次' verbose_name_plural = '司机车次' + + +class MerchantSettingTypeEnum(models.TextChoices): + """商户设置类型枚举""" + STR = 'str', '字符串' + INT = 'int', '整数' + FLOAT = 'float', '浮点数' + BOOL = 'bool', '布尔值' + + +class MerchantSetting(ModelBase): + id = models.BigAutoField(primary_key=True) + merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='settings', verbose_name='所属商户') + key = models.CharField(max_length=100, verbose_name='设置项') + type = models.CharField( + max_length=20, + choices=MerchantSettingTypeEnum.choices, + default=MerchantSettingTypeEnum.STR, + verbose_name='设置类型', + ) + val_str = models.CharField(max_length=100, null=True, blank=True, verbose_name='设置值(字符串)') + val_int = models.IntegerField(null=True, blank=True, verbose_name='设置值(整数)') + val_float = models.FloatField(null=True, blank=True, verbose_name='设置值(浮点数)') + val_bool = models.BooleanField(null=True, blank=True, verbose_name='设置值(布尔值)') + description = models.TextField(blank=True, null=True, verbose_name='备注') + + @property + def value(self): + if self.type == MerchantSettingTypeEnum.STR: + return self.val_str + elif self.type == MerchantSettingTypeEnum.INT: + return self.val_int + elif self.type == MerchantSettingTypeEnum.FLOAT: + return self.val_float + elif self.type == MerchantSettingTypeEnum.BOOL: + return self.val_bool + raise ValueError(f'Invalid setting type: {self.type}') + + def __str__(self): + return f'{self.merchant.name} - {self.key}' + + class Meta: + verbose_name = '商户设置' + verbose_name_plural = '商户设置' + + +class MerchantSettingKeyEnum(models.TextChoices): + """商户设置项枚举""" + AUTO_CREATE_STOCK_CHANGE_TASKS = 'auto_create_stock_change_tasks', '自动创建出入库记录任务' diff --git a/basic_info/services.py b/basic_info/services.py index 192c167..35b953d 100644 --- a/basic_info/services.py +++ b/basic_info/services.py @@ -73,3 +73,9 @@ class DataVisibilityService: return False return basic_models.WareHouse.objects.filter(id=warehouse_id).filter(merchant=emp.merchant).exists() + + +class MerchantSettingService: + @staticmethod + def get_setting(merchant: basic_models.Merchant, key: basic_models.MerchantSettingKeyEnum): + return basic_models.MerchantSetting.objects.get(merchant=merchant, key=key) diff --git a/business/services.py b/business/services.py index 24d0950..993ebcb 100644 --- a/business/services.py +++ b/business/services.py @@ -1,21 +1,25 @@ from __future__ import annotations -from typing import Any, Dict, List - -from django.db import transaction - +import logging from datetime import date, datetime -from decimal import Decimal -from typing import Any, Dict, List +from decimal import Decimal, InvalidOperation +from typing import Any, Dict, List, Tuple +from django.contrib.auth import get_user_model from django.db import transaction from basic_info import models as basic_info_models +from stock import models as stock_models +from stock.services import StockFlowService +from basic_info.services import MerchantSettingService from . import models from .tasks import create_purchase_order_stock_entries +logger = logging.getLogger(__name__) + + def _normalize_order_date(value) -> date: if isinstance(value, date): return value @@ -34,8 +38,8 @@ def create_purchase_order( merchant: basic_info_models.Merchant, supplier: basic_info_models.Supplier, order_date, - total_amount, - warehouse_id: int, + warehouse: basic_info_models.WareHouse, + operator: basic_info_models.Employee, items: List[Dict[str, Any]], remarks: str | None = '', created_by=None, @@ -47,40 +51,190 @@ def create_purchase_order( merchant: 采购单所属商户 supplier: 供应商 order_date: 订单日期 (date) - total_amount: 总金额 - warehouse_id: 入库仓库ID - items: 产品明细,格式示例: - [ - {'product_id': 1, 'quantities': ['10.00', '5.00']}, - {'product_id': 2, 'quantities': ['3.50']}, - ] - 该结构会被传递给 StockFlowService,需满足其模式要求。 + warehouse: 入库仓库实例 + operator: 经办人 + items: 产品明细,字段会根据仓库模式校验 remarks: 备注 created_by: 创建者用户(可选,用于 stock 记录中的 created_by) """ if not items: raise ValueError('items 不能为空') - if not warehouse_id: - raise ValueError('warehouse_id 不能为空') normalized_date = _normalize_order_date(order_date) - total_amount = Decimal(str(total_amount)) + purchase_items, stock_flow_items = _normalize_purchase_items( + merchant=merchant, + warehouse=warehouse, + items=items, + ) with transaction.atomic(): purchase_order = models.PurchaseOrder.objects.create( merchant=merchant, supplier=supplier, - order_date=normalized_date, - total_amount=total_amount, + purchase_date=normalized_date, + operator=operator, + warehouse=warehouse, remarks=remarks, ) + bulk_objects = [ + models.PurchaseOrderItem( + purchase_order=purchase_order, + product=item_data['product'], + price=item_data['price'], + color=item_data.get('color'), + quantity=item_data['quantity'], + unit=item_data['unit'], + empty_diff_percent=item_data['empty_diff_percent'], + quantity_of_rolls=item_data.get('quantity_of_rolls'), + num_of_rolls=item_data['num_of_rolls'], + batch_number=item_data.get('batch_number'), + remarks=item_data.get('remarks'), + ) + for item_data in purchase_items + ] + models.PurchaseOrderItem.objects.bulk_create(bulk_objects) created_by_id = getattr(created_by, 'id', None) - create_purchase_order_stock_entries.delay( - purchase_order_id=purchase_order.id, - warehouse_id=warehouse_id, - items=items, - created_by_id=created_by_id, - ) + if MerchantSettingService.get_setting(merchant, basic_info_models.MerchantSettingKeyEnum.AUTO_CREATE_STOCK_CHANGE_TASKS).value is True: + logger.info('自动创建出入库记录任务已开启,创建入库记录任务') + create_purchase_order_stock_entries.delay( + purchase_order_id=purchase_order.id, + warehouse_id=warehouse.id, + items=stock_flow_items, + created_by_id=created_by_id, + ) return purchase_order + +def _normalize_purchase_items( + *, + merchant: basic_info_models.Merchant, + warehouse: basic_info_models.WareHouse, + items: List[Dict[str, Any]], +) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: + """ + 根据仓库模式校验采购明细,并返回 + - purchase_items: 用于创建 PurchaseOrderItem + - stock_flow_items: 传递给 StockFlowService 的 items 结构 + """ + purchase_items: List[Dict[str, Any]] = [] + stock_flow_items: List[Dict[str, Any]] = [] + + warehouse_mode = warehouse.mode + + for index, raw_item in enumerate(items): + product_id = raw_item.get('product_id') + if not product_id: + raise ValueError(f'items[{index}].product_id 不能为空') + try: + product = basic_info_models.Product.objects.get(id=product_id, merchant=merchant) + except basic_info_models.Product.DoesNotExist as exc: + raise ValueError(f'产品 {product_id} 不存在或不属于当前商户') from exc + + price = _to_decimal(raw_item.get('price', '0'), f'items[{index}].price') + empty_diff_percent = _to_decimal(raw_item.get('empty_diff_percent', '0'), f'items[{index}].empty_diff_percent') + color = raw_item.get('color') + batch_number = raw_item.get('batch_number') + remarks = raw_item.get('remarks') + unit = raw_item.get('unit') or product.get_unit_display() or '米' + + if warehouse_mode == basic_info_models.WareHouseModeEnum.UNRESTRICTED: + if 'numbers' in raw_item and raw_item['numbers']: + raise ValueError(f'仓库为宽进模式,items[{index}] 不应提供 numbers') + quantity = _to_positive_int(raw_item.get('quantity'), f'items[{index}].quantity') + num_of_rolls = _to_positive_int(raw_item.get('num_of_rolls'), f'items[{index}].num_of_rolls') + quantity_of_rolls = None + stock_flow_items.append({ + 'product_id': product.id, + 'value': str(quantity), + 'num_of_rolls': num_of_rolls, + }) + else: + numbers = raw_item.get('numbers') + if not numbers or not isinstance(numbers, list): + raise ValueError(f'仓库为严进模式,items[{index}] 需要提供 numbers 数组') + normalized_numbers = [ + str(_to_positive_int(value, f'items[{index}].numbers[{pos}]')) + for pos, value in enumerate(numbers) + ] + num_of_rolls = len(normalized_numbers) + quantity = sum(int(val) for val in normalized_numbers) + quantity_of_rolls = ','.join(normalized_numbers) + stock_flow_items.append({ + 'product_id': product.id, + 'quantities': normalized_numbers, + }) + + purchase_items.append({ + 'product': product, + 'price': price, + 'color': color, + 'quantity': quantity, + 'unit': unit, + 'empty_diff_percent': empty_diff_percent, + 'quantity_of_rolls': quantity_of_rolls, + 'num_of_rolls': num_of_rolls, + 'batch_number': batch_number, + 'remarks': remarks, + }) + + return purchase_items, stock_flow_items + + +def create_purchase_order_stock_entries_sync( + *, + purchase_order_id: int, + warehouse_id: int, + items: List[Dict[str, Any]], + created_by_id: int | None = None, +) -> Dict[str, Any]: + """ + 根据采购单生成入库记录。 + """ + try: + purchase_order = models.PurchaseOrder.objects.select_related('merchant').get(id=purchase_order_id) + except models.PurchaseOrder.DoesNotExist: + logger.error('PurchaseOrder %s 不存在,无法创建入库单', purchase_order_id) + return {'error': 'purchase_order_not_found', 'purchase_order_id': purchase_order_id} + + merchant = purchase_order.merchant + + created_by = None + if created_by_id: + UserModel = get_user_model() + created_by = UserModel.objects.filter(id=created_by_id).first() + + service = StockFlowService(merchant=merchant, created_by=created_by) + record, details, created_count = service.stock_in( + warehouse_id=warehouse_id, + source_type=stock_models.StockChangeSourceEnum.PURCHASE, + source_id=purchase_order.id, + items=items, + ) + + payload = { + 'purchase_order_id': purchase_order.id, + 'stock_change_record_id': getattr(record, 'id', None), + 'created_details_count': created_count, + } + logger.info('采购单 %s 入库任务完成: %s', purchase_order.id, payload) + return payload + + +def _to_decimal(value, field_name: str) -> Decimal: + try: + return Decimal(str(value)) + except (InvalidOperation, TypeError) as exc: + raise ValueError(f'{field_name} 必须是合法数值') from exc + + +def _to_positive_int(value, field_name: str) -> int: + if value is None: + raise ValueError(f'{field_name} 不能为空') + decimal_value = _to_decimal(value, field_name) + if decimal_value <= 0: + raise ValueError(f'{field_name} 必须大于 0') + if decimal_value != decimal_value.to_integral_value(): + raise ValueError(f'{field_name} 必须为整数') + return int(decimal_value) + diff --git a/business/tasks.py b/business/tasks.py index 39c1b17..05f4591 100644 --- a/business/tasks.py +++ b/business/tasks.py @@ -1,15 +1,8 @@ -import logging from typing import Any, Dict, List from celery import shared_task -from django.contrib.auth import get_user_model -from business import models as business_models -from stock import models as stock_models -from stock.services import StockFlowService - - -logger = logging.getLogger(__name__) +from business import services as business_services @shared_task(bind=True) @@ -20,37 +13,13 @@ def create_purchase_order_stock_entries( warehouse_id: int, items: List[Dict[str, Any]], created_by_id: int | None = None, -): - """ - 为采购单创建入库记录。仅支持入库(StockFlowService.stock_in)。 - """ - try: - purchase_order = business_models.PurchaseOrder.objects.select_related('merchant').get(id=purchase_order_id) - except business_models.PurchaseOrder.DoesNotExist: - logger.error('PurchaseOrder %s 不存在,无法创建入库单', purchase_order_id) - return {'error': 'purchase_order_not_found', 'purchase_order_id': purchase_order_id} - - merchant = purchase_order.merchant - - created_by = None - if created_by_id: - UserModel = get_user_model() - created_by = UserModel.objects.filter(id=created_by_id).first() - - service = StockFlowService(merchant=merchant, created_by=created_by) - record, details, created_count = service.stock_in( +) -> Dict[str, Any]: + payload = business_services.create_purchase_order_stock_entries_sync( + purchase_order_id=purchase_order_id, warehouse_id=warehouse_id, - source_type=stock_models.StockChangeSourceEnum.PURCHASE, - source_id=purchase_order.id, items=items, + created_by_id=created_by_id, ) - - payload = { - 'task_id': self.request.id, - 'purchase_order_id': purchase_order.id, - 'stock_change_record_id': getattr(record, 'id', None), - 'created_details_count': created_count, - } - logger.info('采购单 %s 入库任务完成: %s', purchase_order.id, payload) + payload['task_id'] = self.request.id return payload diff --git a/business/tests.py b/business/tests.py index cd0db94..4a92723 100644 --- a/business/tests.py +++ b/business/tests.py @@ -1,4 +1,3 @@ -from decimal import Decimal from typing import List, Dict, Any from django.contrib.auth import get_user_model @@ -20,11 +19,16 @@ def create_basic_fixtures(): merchant=merchant, name='测试供应商', ) - warehouse = basic_models.WareHouse.objects.create( + warehouse_strict = basic_models.WareHouse.objects.create( merchant=merchant, - name='主仓', + name='严进仓', mode=basic_models.WareHouseModeEnum.RESTRICT_IN, ) + warehouse_relaxed = basic_models.WareHouse.objects.create( + merchant=merchant, + name='宽进仓', + mode=basic_models.WareHouseModeEnum.UNRESTRICTED, + ) category = basic_models.ProductCategory.objects.create( merchant=merchant, name='面料', @@ -37,36 +41,56 @@ def create_basic_fixtures(): human_id='FAB-001', unit=basic_models.ProductUnitEnum.METER, ) - return merchant, supplier, warehouse, product + operator = basic_models.Employee.objects.create( + merchant=merchant, + name='经办人', + status=basic_models.EmployeeStatusEnum.ACTIVE, + ) + return merchant, supplier, warehouse_strict, warehouse_relaxed, product, operator class PurchaseOrderServiceTestCase(TestCase): def setUp(self): - self.merchant, self.supplier, self.warehouse, self.product = create_basic_fixtures() + ( + self.merchant, + self.supplier, + self.warehouse_strict, + self.warehouse_relaxed, + self.product, + self.operator, + ) = create_basic_fixtures() User = get_user_model() self.user = User.objects.create_user(username='creator', password='pass123') - self.items: List[Dict[str, Any]] = [ - {'product_id': self.product.id, 'quantities': ['10.00', '5.00']}, + self.strict_items: List[Dict[str, Any]] = [ + {'product_id': self.product.id, 'numbers': [10, 5], 'price': '12.5', 'unit': '米'} + ] + self.relaxed_items: List[Dict[str, Any]] = [ + {'product_id': self.product.id, 'quantity': 120, 'num_of_rolls': 3, 'price': '10.0', 'unit': '米'} ] - def test_create_purchase_order_triggers_task(self): + def test_create_purchase_order_triggers_task_strict(self): with patch('business.services.create_purchase_order_stock_entries.delay') as mock_delay: purchase_order = services.create_purchase_order( merchant=self.merchant, supplier=self.supplier, order_date=timezone.now().date(), - total_amount=Decimal('100.00'), - warehouse_id=self.warehouse.id, - items=self.items, + warehouse=self.warehouse_strict, + operator=self.operator, + items=self.strict_items, remarks='自动化测试', created_by=self.user, ) self.assertIsInstance(purchase_order, business_models.PurchaseOrder) + self.assertEqual(purchase_order.warehouse, self.warehouse_strict) + self.assertEqual(purchase_order.operator, self.operator) + self.assertEqual(purchase_order.items.count(), 1) + item = purchase_order.items.first() + self.assertEqual(item.quantity_of_rolls, '10,5') mock_delay.assert_called_once_with( purchase_order_id=purchase_order.id, - warehouse_id=self.warehouse.id, - items=self.items, + warehouse_id=self.warehouse_strict.id, + items=[{'product_id': self.product.id, 'quantities': ['10', '5']}], created_by_id=self.user.id, ) @@ -76,11 +100,75 @@ class PurchaseOrderServiceTestCase(TestCase): merchant=self.merchant, supplier=self.supplier, order_date=timezone.now().date(), - total_amount=Decimal('50.00'), - warehouse_id=self.warehouse.id, + warehouse=self.warehouse_strict, + operator=self.operator, items=[], ) + def test_create_purchase_order_relaxed_items(self): + with patch('business.services.create_purchase_order_stock_entries.delay') as mock_delay: + purchase_order = services.create_purchase_order( + merchant=self.merchant, + supplier=self.supplier, + order_date=timezone.now().date(), + warehouse=self.warehouse_relaxed, + operator=self.operator, + items=self.relaxed_items, + created_by=self.user, + ) + + self.assertEqual(purchase_order.items.first().quantity_of_rolls, None) + mock_delay.assert_called_once_with( + purchase_order_id=purchase_order.id, + warehouse_id=self.warehouse_relaxed.id, + items=[{'product_id': self.product.id, 'value': '120', 'num_of_rolls': 3}], + created_by_id=self.user.id, + ) + + +class PurchaseOrderStockServiceTestCase(TestCase): + def setUp(self): + ( + self.merchant, + self.supplier, + self.warehouse_strict, + self.warehouse_relaxed, + self.product, + self.operator, + ) = create_basic_fixtures() + User = get_user_model() + self.user = User.objects.create_user(username='svc-user', password='pass123') + self.purchase_order = business_models.PurchaseOrder.objects.create( + merchant=self.merchant, + supplier=self.supplier, + purchase_date=timezone.now().date(), + operator=self.operator, + warehouse=self.warehouse_strict, + ) + self.items = [{'product_id': self.product.id, 'quantities': ['8']}] + + def test_service_calls_stock_flow_service(self): + with patch('business.services.StockFlowService') as mock_flow_cls: + mock_instance = mock_flow_cls.return_value + mock_instance.stock_in.return_value = (MagicMock(id=321), [], 2) + + payload = services.create_purchase_order_stock_entries_sync( + purchase_order_id=self.purchase_order.id, + warehouse_id=self.warehouse_strict.id, + items=self.items, + created_by_id=self.user.id, + ) + + mock_flow_cls.assert_called_once_with(merchant=self.merchant, created_by=self.user) + mock_instance.stock_in.assert_called_once_with( + warehouse_id=self.warehouse_strict.id, + source_type=stock_models.StockChangeSourceEnum.PURCHASE, + source_id=self.purchase_order.id, + items=[{'product_id': self.product.id, 'quantities': ['8']}], + ) + self.assertEqual(payload['purchase_order_id'], self.purchase_order.id) + self.assertEqual(payload['stock_change_record_id'], 321) + @override_settings( CELERY_TASK_ALWAYS_EAGER=True, @@ -88,39 +176,30 @@ class PurchaseOrderServiceTestCase(TestCase): ) class PurchaseOrderStockTaskTestCase(TestCase): def setUp(self): - self.merchant, self.supplier, self.warehouse, self.product = create_basic_fixtures() - User = get_user_model() - self.user = User.objects.create_user(username='task-user', password='pass123') - self.purchase_order = business_models.PurchaseOrder.objects.create( - merchant=self.merchant, - supplier=self.supplier, - order_date=timezone.now().date(), - total_amount=Decimal('100.00'), - ) - self.items = [{'product_id': self.product.id, 'quantities': ['8.00']}] + self.purchase_order_id = 123 + self.warehouse_id = 456 + self.items = [{'product_id': 1, 'quantities': ['5']}] - def test_task_calls_stock_flow_service(self): - with patch('business.tasks.StockFlowService') as mock_flow_cls: - mock_instance = mock_flow_cls.return_value - mock_instance.stock_in.return_value = (MagicMock(id=321), [], 2) + def test_task_delegates_to_service(self): + with patch('business.tasks.business_services.create_purchase_order_stock_entries_sync') as mock_sync: + mock_sync.return_value = {'purchase_order_id': self.purchase_order_id} async_result = tasks.create_purchase_order_stock_entries.delay( - purchase_order_id=self.purchase_order.id, - warehouse_id=self.warehouse.id, + purchase_order_id=self.purchase_order_id, + warehouse_id=self.warehouse_id, items=self.items, - created_by_id=self.user.id, + created_by_id=999, ) payload = async_result.get(timeout=5) - mock_flow_cls.assert_called_once_with(merchant=self.merchant, created_by=self.user) - mock_instance.stock_in.assert_called_once_with( - warehouse_id=self.warehouse.id, - source_type=stock_models.StockChangeSourceEnum.PURCHASE, - source_id=self.purchase_order.id, + mock_sync.assert_called_once_with( + purchase_order_id=self.purchase_order_id, + warehouse_id=self.warehouse_id, items=self.items, + created_by_id=999, ) - self.assertEqual(payload['purchase_order_id'], self.purchase_order.id) - self.assertEqual(payload['stock_change_record_id'], 321) + self.assertEqual(payload['purchase_order_id'], self.purchase_order_id) + self.assertIn('task_id', payload) from django.test import TestCase # Create your tests here. diff --git a/data-bak/db-backup-20251126-124636.sql b/data-bak/db-backup-20251128-062859.sql similarity index 85% rename from data-bak/db-backup-20251126-124636.sql rename to data-bak/db-backup-20251128-062859.sql index 2dcca32..9293227 100644 --- a/data-bak/db-backup-20251126-124636.sql +++ b/data-bak/db-backup-20251128-062859.sql @@ -2,7 +2,7 @@ -- PostgreSQL database dump -- -\restrict lAaLqLBC97fkSperdxYl5fjzjf6qhnkvpVyPWEkZyQ37Mu4fIXbcWZBIFk58Sg7 +\restrict 7vxIoMQ6fViE6Ury13lyTyPcnDejMchx1YmoWE9b14f1V4IdfU6SzZ7uNzJes6K -- Dumped from database version 16.10 -- Dumped by pg_dump version 17.6 (Debian 17.6-0+deb13u1) @@ -456,6 +456,41 @@ ALTER TABLE public.basic_info_merchant ALTER COLUMN id ADD GENERATED BY DEFAULT ); +-- +-- Name: basic_info_merchantsetting; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.basic_info_merchantsetting ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + id bigint NOT NULL, + key character varying(100) NOT NULL, + description text, + merchant_id bigint NOT NULL, + val_bool boolean, + val_float double precision, + val_int integer, + val_str character varying(100), + type character varying(20) NOT NULL +); + + +ALTER TABLE public.basic_info_merchantsetting OWNER TO postgres; + +-- +-- Name: basic_info_merchantsetting_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.basic_info_merchantsetting ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.basic_info_merchantsetting_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + -- -- Name: basic_info_product; Type: TABLE; Schema: public; Owner: postgres -- @@ -748,16 +783,59 @@ CREATE TABLE public.business_purchaseorder ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, id bigint NOT NULL, - order_date date NOT NULL, - total_amount numeric(15,2) NOT NULL, remarks text, merchant_id bigint NOT NULL, - supplier_id bigint NOT NULL + supplier_id bigint NOT NULL, + kind integer NOT NULL, + operator_id bigint NOT NULL, + purchase_date date NOT NULL, + status integer NOT NULL, + warehouse_id bigint NOT NULL ); ALTER TABLE public.business_purchaseorder OWNER TO postgres; +-- +-- Name: business_purchaseorderitem; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.business_purchaseorderitem ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + id bigint NOT NULL, + price numeric(15,2) NOT NULL, + color character varying(50), + quantity integer NOT NULL, + unit character varying(50) NOT NULL, + empty_diff_percent numeric(15,2) NOT NULL, + quantity_of_rolls character varying(255), + num_of_rolls integer NOT NULL, + batch_number character varying(100), + remarks text, + product_id bigint NOT NULL, + purchase_order_id bigint NOT NULL, + CONSTRAINT business_purchaseorderitem_num_of_rolls_check CHECK ((num_of_rolls >= 0)), + CONSTRAINT business_purchaseorderitem_quantity_check CHECK ((quantity >= 0)) +); + + +ALTER TABLE public.business_purchaseorderitem OWNER TO postgres; + +-- +-- Name: business_purchaseorderitem_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.business_purchaseorderitem ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.business_purchaseorderitem_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + -- -- Name: django_admin_log; Type: TABLE; Schema: public; Owner: postgres -- @@ -871,7 +949,7 @@ CREATE TABLE public.printing_plateorder ( plate_type character varying(20), plate_date timestamp with time zone, plate_method character varying(50), - plate_image character varying(100), + plate_image jsonb NOT NULL, plate_notes text, reprint_reason text, urgency_level character varying(20) NOT NULL, @@ -882,7 +960,7 @@ CREATE TABLE public.printing_plateorder ( color_matching_rating character varying(20), sample_rating character varying(20), difficulty_rating character varying(20), - required_completion_date date, + required_completion_date timestamp with time zone, completion_date timestamp with time zone, fabric character varying(100), width character varying(50), @@ -900,7 +978,10 @@ CREATE TABLE public.printing_plateorder ( is_invalid boolean NOT NULL, process integer NOT NULL, fabric_source character varying(100), - designer_id bigint + designer_id bigint, + image_name text, + print_count integer NOT NULL, + CONSTRAINT printing_plateorder_print_count_check CHECK ((print_count >= 0)) ); @@ -983,7 +1064,9 @@ CREATE TABLE public.printing_printingorder ( production_warn text, customer_id bigint NOT NULL, is_invalid boolean NOT NULL, - process_id bigint + process_id bigint, + print_count integer NOT NULL, + CONSTRAINT printing_printingorder_print_count_check CHECK ((print_count >= 0)) ); @@ -1436,6 +1519,15 @@ ALTER TABLE public.stock_stocksnapshot ALTER COLUMN id ADD GENERATED BY DEFAULT COPY public.api_uploaded_file (id, created_at, updated_at, path, is_deleted, original_filename, file_size, content_type, owner_id) FROM stdin; 1 2025-11-21 02:46:27.837198+00 2025-11-21 02:46:27.837209+00 uploads/2025/11/21/21aed50ea13146c68d20b4ae3e1b3079.png f debug_group_4.png 159627 image/png 2 +2 2025-11-28 01:20:36.190015+00 2025-11-28 01:20:36.190028+00 uploads/2025/11/28/3f6310da200d4eed95fbf83e3a91c5d3.png f image.png 20215 image/png 2 +3 2025-11-28 01:32:56.717249+00 2025-11-28 01:32:56.717258+00 uploads/2025/11/28/669cb9d758ec4d1d812221634c0a8f0c.png f image.png 9562 image/png 2 +4 2025-11-28 01:33:05.201488+00 2025-11-28 01:33:05.201499+00 uploads/2025/11/28/7ceb226e55ff49f1ae19288bceb30043.png f image.png 27946 image/png 2 +5 2025-11-28 01:33:07.382898+00 2025-11-28 01:33:07.382907+00 uploads/2025/11/28/d8476a09bc65428186de1fff70acc8cb.png f image.png 19360 image/png 2 +6 2025-11-28 01:33:10.519571+00 2025-11-28 01:33:10.519581+00 uploads/2025/11/28/114c7bdecf9547dd8d55c19d814fcbe5.png f image.png 25338 image/png 2 +7 2025-11-28 02:45:21.799563+00 2025-11-28 02:45:21.799574+00 uploads/2025/11/28/ccaba46c20a249f1ba70cc00a4abe75a.png f image.png 17641 image/png 2 +8 2025-11-28 02:51:44.90851+00 2025-11-28 02:51:44.908523+00 uploads/2025/11/28/427aa1b8af0c44a3ba2ef930585e5f0e.png f image.png 29584 image/png 2 +9 2025-11-28 02:53:41.308123+00 2025-11-28 02:53:41.308132+00 uploads/2025/11/28/ebb3a627a5ce4eaaabc6fc4d9cd15bc7.png f image.png 55897 image/png 2 +10 2025-11-28 02:58:51.677179+00 2025-11-28 02:58:51.677189+00 uploads/2025/11/28/afaea93815684e2c8c3ea17124a702ed.png f image.png 35906 image/png 2 \. @@ -1617,6 +1709,14 @@ COPY public.auth_permission (id, name, content_type_id, codename) FROM stdin; 155 Can change 采购单 38 change_purchaseorder 156 Can delete 采购单 38 delete_purchaseorder 157 Can view 采购单 38 view_purchaseorder +158 Can add 采购单明细 39 add_purchaseorderitem +159 Can change 采购单明细 39 change_purchaseorderitem +160 Can delete 采购单明细 39 delete_purchaseorderitem +161 Can view 采购单明细 39 view_purchaseorderitem +162 Can add 商户设置 40 add_merchantsetting +163 Can change 商户设置 40 change_merchantsetting +164 Can delete 商户设置 40 delete_merchantsetting +165 Can view 商户设置 40 view_merchantsetting \. @@ -1625,12 +1725,12 @@ COPY public.auth_permission (id, name, content_type_id, codename) FROM stdin; -- COPY public.auth_user (id, password, last_login, is_superuser, username, first_name, last_name, email, is_staff, is_active, date_joined) FROM stdin; -2 pbkdf2_sha256$1000000$FQEiwQ12oi0PN5Eiy8yBj8$5K56mG8hMRXmQDNAJaeacM1f7ILZ0cF/6Ad6wTI69QA= 2025-11-26 02:43:48.977068+00 f jimi f t 2025-11-18 05:59:00+00 3 pbkdf2_sha256$1000000$mlevMIoViCC3iCxkVWY7N7$Hs9ug78RJIMHoORM5k4YzX52IkUubzlzOzXpMDAG5/k= 2025-11-23 12:57:16.773509+00 f 映雪 f t 2025-11-19 08:12:00+00 5 pbkdf2_sha256$1000000$6QzgwlV3m9j08p5c6TimM6$dYqey1tpxL2fIuJj+qKVl2z5Dc41BrvHuZlQShmgIM4= \N f testuser_5599 testuser_5599@example.com f t 2025-11-24 05:45:22.629483+00 6 pbkdf2_sha256$1000000$Gc8ss3NEXAAO5mK1nQ51l2$/DOLtcYC+F+dG1LVSuvWvyrLVNR9qViopLL9ntjqojo= \N f apitest2 f t 2025-11-24 06:08:19.098505+00 7 pbkdf2_sha256$1000000$gXVjPP0SD9cgTNxOOsTPbG$/dtOlfY+1WsASF/pW+EtompccUEORJMTMyhL9YebZxo= \N f apitest666 f t 2025-11-24 06:16:49.620831+00 -1 pbkdf2_sha256$1000000$NyR8RyN6CWu3xEJP4rXclZ$vKH5DuApHqxxHaxiWd/b6Ek0BTozUZ+ZaJXcxtfRkiA= 2025-11-25 07:53:54.390262+00 t admin t t 2025-11-18 05:58:19.271303+00 +1 pbkdf2_sha256$1000000$NyR8RyN6CWu3xEJP4rXclZ$vKH5DuApHqxxHaxiWd/b6Ek0BTozUZ+ZaJXcxtfRkiA= 2025-11-28 06:05:13.233786+00 t admin t t 2025-11-18 05:58:19.271303+00 +2 pbkdf2_sha256$1000000$FQEiwQ12oi0PN5Eiy8yBj8$5K56mG8hMRXmQDNAJaeacM1f7ILZ0cF/6Ad6wTI69QA= 2025-11-28 06:15:55.446346+00 f jimi f t 2025-11-18 05:59:00+00 \. @@ -1765,6 +1865,15 @@ COPY public.basic_info_merchant (created_at, updated_at, id, name, type, area, e \. +-- +-- Data for Name: basic_info_merchantsetting; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.basic_info_merchantsetting (created_at, updated_at, id, key, description, merchant_id, val_bool, val_float, val_int, val_str, type) FROM stdin; +2025-11-28 06:03:02.924489+00 2025-11-28 06:20:38.985597+00 1 auto_create_stock_change_tasks 1 f \N \N \N bool +\. + + -- -- Data for Name: basic_info_product; Type: TABLE DATA; Schema: public; Owner: postgres -- @@ -1898,6 +2007,17 @@ COPY public.business_object (id, created_at, updated_at, name, description, proc 28 2025-11-22 02:39:05.939841+00 2025-11-22 02:39:05.939847+00 2 \N \N 29 2025-11-23 13:11:03.642075+00 2025-11-23 13:11:03.642082+00 2 \N \N 30 2025-11-24 11:33:15.711916+00 2025-11-24 11:33:15.711922+00 2 \N \N +31 2025-11-27 07:23:06.022961+00 2025-11-27 07:23:06.022966+00 2 \N \N +32 2025-11-27 08:08:49.410063+00 2025-11-27 08:08:49.41007+00 2 \N \N +33 2025-11-27 08:09:00.364914+00 2025-11-27 08:09:00.364921+00 2 \N \N +34 2025-11-27 09:01:25.14368+00 2025-11-27 09:01:25.143685+00 2 \N \N +35 2025-11-27 09:04:52.038687+00 2025-11-27 09:04:52.038695+00 2 \N \N +36 2025-11-28 01:32:58.420878+00 2025-11-28 01:32:58.420883+00 2 \N \N +37 2025-11-28 01:33:14.216478+00 2025-11-28 01:33:14.216483+00 2 \N \N +38 2025-11-28 01:43:59.359293+00 2025-11-28 01:43:59.359298+00 2 \N \N +39 2025-11-28 02:39:41.390748+00 2025-11-28 02:39:41.390755+00 2 \N \N +40 2025-11-28 02:57:18.735261+00 2025-11-28 02:57:18.735268+00 2 \N \N +41 2025-11-28 02:58:57.702444+00 2025-11-28 02:58:57.70245+00 2 \N \N \. @@ -1905,7 +2025,32 @@ COPY public.business_object (id, created_at, updated_at, name, description, proc -- Data for Name: business_purchaseorder; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.business_purchaseorder (created_at, updated_at, id, order_date, total_amount, remarks, merchant_id, supplier_id) FROM stdin; +COPY public.business_purchaseorder (created_at, updated_at, id, remarks, merchant_id, supplier_id, kind, operator_id, purchase_date, status, warehouse_id) FROM stdin; +2025-11-27 08:13:33.430682+00 2025-11-27 08:13:33.430691+00 4 1 1 1 1 2025-11-27 2 2 +2025-11-27 08:32:59.523809+00 2025-11-27 08:32:59.523816+00 5 1 1 1 1 2025-11-27 1 4 +2025-11-28 03:45:15.401776+00 2025-11-28 03:45:15.401787+00 6 1 1 1 1 2025-11-28 1 2 +2025-11-28 03:47:11.102159+00 2025-11-28 03:47:11.102166+00 7 1 1 1 1 2025-11-28 1 3 +2025-11-28 03:48:02.675851+00 2025-11-28 03:48:02.675859+00 8 oooo 1 1 1 1 2025-11-28 1 4 +2025-11-28 03:49:04.580215+00 2025-11-28 03:49:04.580224+00 9 1 1 1 1 2025-11-28 1 2 +2025-11-28 06:18:30.813402+00 2025-11-28 06:18:30.813413+00 10 1 1 1 1 2025-11-28 1 3 +2025-11-28 06:18:54.718044+00 2025-11-28 06:18:54.718052+00 11 1 1 1 1 2025-11-28 1 4 +2025-11-28 06:20:58.855488+00 2025-11-28 06:20:58.855501+00 12 1 1 1 1 2025-11-28 1 2 +\. + + +-- +-- Data for Name: business_purchaseorderitem; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.business_purchaseorderitem (created_at, updated_at, id, price, color, quantity, unit, empty_diff_percent, quantity_of_rolls, num_of_rolls, batch_number, remarks, product_id, purchase_order_id) FROM stdin; +2025-11-27 08:14:29.091796+00 2025-11-27 08:14:29.091805+00 1 555.00 红色 25 米 3.34 200 1 1 1 4 +2025-11-27 08:35:24.029424+00 2025-11-27 08:48:57.608408+00 2 25.00 \N 100 米 5.00 \N 1 WWOODDP1 1 5 +2025-11-28 03:45:15.402492+00 2025-11-28 03:45:15.402496+00 3 0.00 \N 195 米 0.00 97,98 2 \N \N 1 6 +2025-11-28 03:47:11.102785+00 2025-11-28 03:47:11.10279+00 4 0.00 \N 195 米 0.00 97,98 2 \N \N 1 7 +2025-11-28 03:49:04.580819+00 2025-11-28 03:49:04.580824+00 5 0.00 \N 195 米 0.00 97,98 2 \N \N 1 9 +2025-11-28 06:18:30.814105+00 2025-11-28 06:18:30.814109+00 6 0.00 \N 195 米 0.00 97,98 2 \N \N 1 10 +2025-11-28 06:18:54.718646+00 2025-11-28 06:18:54.71865+00 7 0.00 \N 386 米 0.00 97,98,97,94 4 \N \N 1 11 +2025-11-28 06:20:58.85678+00 2025-11-28 06:20:58.856787+00 8 0.00 \N 195 米 0.00 97,98 2 \N \N 2 12 \. @@ -2013,6 +2158,23 @@ COPY public.django_admin_log (id, action_time, object_id, object_repr, action_fl 97 2025-11-25 10:12:01.371549+00 11 出入库记录 11 - 仓库:中大 1 [{"added": {}}] 20 1 98 2025-11-25 10:12:41.75256+00 27 库存变动明细 27 - 记录ID: 11 1 [{"added": {}}] 21 1 99 2025-11-25 10:12:59.643169+00 27 库存变动明细 27 - 记录ID: 11 2 [{"changed": {"fields": ["\\u6240\\u6d88\\u8017\\u7684\\u5165\\u5e93\\u660e\\u7ec6"]}}] 21 1 +100 2025-11-27 08:08:36.5688+00 13 出入库记录 13 - 仓库:中大 3 20 1 +101 2025-11-27 08:09:43.222937+00 3 采购订单 3 - 测试 3 38 1 +102 2025-11-27 08:09:43.222962+00 2 采购订单 2 - 测试 3 38 1 +103 2025-11-27 08:09:43.222973+00 1 采购订单 1 - 测试 3 38 1 +104 2025-11-27 08:13:33.431382+00 4 采购订单 4 - 测试 1 [{"added": {}}] 38 1 +105 2025-11-27 08:14:29.09261+00 1 PurchaseOrderItem object (1) 1 [{"added": {}}] 39 1 +106 2025-11-27 08:32:59.524449+00 5 采购订单 5 - 测试 1 [{"added": {}}] 38 1 +107 2025-11-27 08:35:24.030068+00 2 PurchaseOrderItem object (2) 1 [{"added": {}}] 39 1 +108 2025-11-27 08:35:32.980517+00 2 PurchaseOrderItem object (2) 2 [{"changed": {"fields": ["\\u7a7a\\u5dee\\u767e\\u5206\\u6bd4"]}}] 39 1 +109 2025-11-27 08:48:57.608904+00 2 PurchaseOrderItem object (2) 2 [{"changed": {"fields": ["\\u6279\\u6b21\\u53f7"]}}] 39 1 +110 2025-11-28 03:48:02.676475+00 8 采购订单 8 - 测试 1 [{"added": {}}] 38 1 +111 2025-11-28 06:03:02.925051+00 1 瑞彩印花 - auto_create_stock_tasks 1 [{"added": {}}] 40 1 +112 2025-11-28 06:03:10.044445+00 1 瑞彩印花 - auto_create_stock_tasks 2 [{"changed": {"fields": ["\\u8bbe\\u7f6e\\u503c"]}}] 40 1 +113 2025-11-28 06:04:43.402849+00 1 瑞彩印花 - auto_create_stock_tasks 2 [{"changed": {"fields": ["\\u8bbe\\u7f6e\\u503c(\\u5e03\\u5c14\\u503c)"]}}] 40 1 +114 2025-11-28 06:09:27.417148+00 1 瑞彩印花 - auto_create_stock_tasks 2 [{"changed": {"fields": ["\\u8bbe\\u7f6e\\u7c7b\\u578b"]}}] 40 1 +115 2025-11-28 06:12:01.751881+00 1 瑞彩印花 - auto_create_stock_change_tasks 2 [{"changed": {"fields": ["\\u8bbe\\u7f6e\\u9879"]}}] 40 1 +116 2025-11-28 06:20:38.986097+00 1 瑞彩印花 - auto_create_stock_change_tasks 2 [{"changed": {"fields": ["\\u8bbe\\u7f6e\\u503c(\\u5e03\\u5c14\\u503c)"]}}] 40 1 \. @@ -2059,6 +2221,8 @@ COPY public.django_content_type (id, app_label, model) FROM stdin; 36 basic_info employeetype 37 basic_info userprofile 38 business purchaseorder +39 business purchaseorderitem +40 basic_info merchantsetting \. @@ -2143,6 +2307,21 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; 74 stock 0005_stockchangedetail_consume_fields 2025-11-24 09:06:44.307805+00 75 business 0001_initial 2025-11-25 06:08:12.742536+00 76 stock 0006_remove_purchaseorder 2025-11-25 06:08:12.745336+00 +77 printing 0016_plateorder_image_name 2025-11-27 06:53:13.195655+00 +78 business 0002_remove_purchaseorder_order_date_purchaseorder_kind_and_more 2025-11-27 07:29:19.575686+00 +79 printing 0017_alter_plateorder_image_name 2025-11-27 07:29:19.582348+00 +80 business 0003_purchaseorder_status 2025-11-27 07:34:05.728213+00 +81 business 0004_remove_purchaseorder_total_amount_and_more 2025-11-27 08:08:14.545561+00 +82 business 0005_alter_purchaseorder_operator_and_more 2025-11-27 08:10:45.002847+00 +83 business 0006_remove_purchaseorderitem_total_amount 2025-11-27 08:18:28.948263+00 +84 business 0007_alter_purchaseorderitem_batch_number 2025-11-27 08:45:44.548428+00 +85 printing 0018_plateorder_plate_image_to_jsonfield 2025-11-28 01:31:22.225727+00 +86 printing 0019_alter_plateorder_plate_image 2025-11-28 01:31:22.259774+00 +87 printing 0020_alter_plateorder_required_completion_date 2025-11-28 02:57:43.677396+00 +88 printing 0021_plateorder_print_count_printingorder_print_count 2025-11-28 03:29:26.824386+00 +89 basic_info 0014_merchantsetting 2025-11-28 06:00:48.6332+00 +90 basic_info 0015_remove_merchantsetting_val_merchantsetting_val_bool_and_more 2025-11-28 06:04:30.020931+00 +91 basic_info 0016_merchantsetting_type 2025-11-28 06:06:20.05325+00 \. @@ -2151,9 +2330,11 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; -- COPY public.django_session (session_key, session_data, expire_date) FROM stdin; +7yutj5xzsu9xicbn3bi2ziy5zzoamenm .eJzNm0tz4ygQgP9KyudMohcCzXHve9zTeivFM9ZElrR6ZGp2av77ArJjmWAbIU3Jl-BCTQNf8-gG8nPzgvtu99K3vHnJ2ebrJtw8jvMIpm-8VB_YN1y-Vk-0KrsmJ09K5OnwtX36s2K8-OMge6Zgh9udLM1RSlOUkpSnNEshQwTDOAGCxykNMxHFGYAxAjSNSZRBmXIex0GGUxRwFkZAKd3zsm-lrr9_bjcl3vPt5uvDdrPd9gkPsUxAhEOZpDhSCYAAbjePUiKXbR5kBW4eBP5C84YWfPi4Vy1v1edParOQQpkgFjGlLwbAQV_fFMO3Z8z2eflM-jYveds-131DJQxeNYw3z4MwZuwvN3kpeShDGo4Zbfo9sbfaF8avx4fZAH79oz7znKmvYRBEbkplM9OAywRymi7GOO_4fipnXeZeWE_BYpKPzYzQUmfCBRk1nyUqgTgg8-bNORVARKY6RtPQw7Rthzsuiur7h8Eq8o3T7rJhL5aYYlY_LlbEfixM8wGLbsC4GiQogrqKmEaqtQAGszjrXzVuZE3dtYXqYolVOPuyMDmnFt3jhslEhKnKjLNoFue6qahafxwAH0VXIevbe5MsvKkbRUhZDQYkmz-CnQfuPVB177lJFU3SLdsewlhlBiBZYvSWcheYMIK1-J3xnsLEpJ9ZaoIR0TNEbrmnepFIqbEuyUxCVAKEl2NpDOOiev1YghtOpTfjPAMsRVex0e8gZ1gsDCbWu6iN1K-JthkVuTub-FshNCdSYtvSY21rGSyq2QmJ7gxMApcI4Yp3eq42JTHXfQjE7DCkbvKyy8vXjx_fKnLZ0FZpdxv70rkJ2peIaeP4Zk2I4GjrHVh_4ncjsL4gfwfE3TmYjK3TRgSBnrgJWpBxIdciZ8An4XXoehIw6QIzw3acAaBQEQdEid6YWJLobSpzcWGvxtBYHQeAhGoOFA4LeuQSmn86HsFtTl_yUlTKD2M97ag0kBz_P64cj1wuM8GmnmyscbQfD9OEtijkXPc0E95mPYXxPbCdoMpga4tFEq7nM-BZvCTb77jhu6pvrwQfVumV-PoxMPnaoo1EZGTQrZQCkHidn55QtX1dF_nVA2qL8DpYp3bd4BnZYgEQC6S3oRgPjq72fknkc3Y5IvXOd7mU6xpctnXVdLeigNtFV0HuS8ckbzuMBglQuo8HeUFCtY0TNlrvIZq5__F9XVQ_OO9-1G5Lx1mBdZgvwsW0gNWVOa9pqbX6iHAS77tg7btSR9bYRy1QUlsU63seQnRcJVI-j-6eq-ursmt5p51vF8hmmXVYe_IwWVtjoHPdS43kI7dJkO-CrvdItt47DRHV0nRp33bV3tH1-BBeh64nAZOu7bYJCK7W9DSGWrfQTiMIU6_HBydg__Y5fcvLuncbvSPxdQh7UjAJ2-I9CCJkt57KTPWVAPV77HECqB6_yKhO5IXbxjeWX4X4MlRM_raY8LDWgyxYdP1g_D2nXP90AT4SX4W3LwWTsC0qPHjmSKB0Se_5GIq4Os9j-XUYe3IwGMe2SDFLhNaNtEOOWPSx385iTHD5himtekc3Yyy_CmNfDiZj804otG6OwxkKINpPh6kOR_X97bw7oVDj0PvL6aBm5tVfRd-Gv8oNfOW3L_7sBSYY1ROOjfM8IKZtrfHQqLW-ePPynZfd1YNsU3AdnM59NclZo5uzhoGQ6Ic56pBx1jAVDef_XX3NYYquznJK702y1sjmXHfMdBJh7RjNuhv-NLsZ73BeTFgODgXugbknF9MCtxf4QxQAw9RnVx0hbEtct7vq85Za9kWxLkznLpr4oJlhO9NDBCc60Xc7IEFUR1VoOMZNLM1vVZ3tLucF-4KL7ubeOY4b7B1QoY3dQurfBHTkc3kenETcR_4Cnb4RIF3rqGkW6yMxTi1vzdqjkvYLrV6vIHttqr6-wWyQWR2aU0cNZElgMrSGkQKBYRnSsWmUqfcqKBBsni-Y8EC5PomI9HsEgKDOFD6rPq7zl_fwua-LCjPOrh8G2IQnmM-ThvW-0o-AacXIzJAu_ubX_2MEg3k:1vOrxp:x8zwg7R0fMTGw-7dJv41hwLHzqn09JEBgXZ_mjqJeDM 2025-12-12 06:28:17.957326+00 pbsoimfkt3qhp2caalbpzzfmnoia4ary .eJzNmktz4ygQgP9Kyuck1guB5rj3Pe5pPZXiaSuRJa0emcpO5b8vIMeRCbER8pZ8CQ40DXzNoxv0e_WE-2731Le8ecrZ6scqXN2P8wimL7xUBewZl9vqkVZl1-TkUYk8Hkrbxz8rxos_DrInCna43cnaHKU0RSlJeUqzFDJEMIwTIHic0jATUZwBGCNA05hEGZQp53EcZDhFAWdhBJTSPS_7Vur6-_dmVeI936x-3G1Wm02fcEFkAiIcyiRliUogDshmdS8lctnnQVbg5k7gB5o3tOBD4V71vFXFFrUh_lQLiMhkgmgaOqjtm2IoW2O2z8t12-GOi6L6tSZ9m5e8bSvyzGm3HsQxY3-51pCyh1qk4ZjRpt-T7wbgx-X9_u5aLN5_qmKeM1UaBkFk0Q0YB0pbBHUTMY1UbwEMZnHWv2rcyJY63rhwNmoswtmXhck5tuged0wmIkxVZpxFszjXTUXl5HQB_CG6CFnf0Ztkk4u6UYSU1WBAsvkz2Hni3gJV95GbVMEk3bLvIYxVZgCSa8zeUp4CE2awFr8x3lOYmPRTS0swInqFBMGo00ik1NiXZCYhKgECgPnzvai2xy244bRqmPMKsFRdxEb_BznTYnBiu1e1kfo10TajKjdnE38rIDMjtB3psba1dHrV6oREDwYmQTrPOz1Vm5KY6zEEQv2XBuo_yKlLI6a16yYvu7zcHn88V-R7Q1ul3W3sS-ciaF8ihknD4GJLMpaJdKbX2vrCT66Rc_7qN_I3QNydg8nYumxEEOiFm6ArMi7kXuQM-FN4GbqeBEy6kblLZbbGoFARB0SJPphYkuhjKnNxYc_G0Bgq7QnVHCgcNvTIJTQ3rUhwm9OnvBSV8sNYTzsqDSTn_9v3xjxTZ4JNPdlY42g_HqZNbVHIqe5pJrzMegrjW2A7QZXB1haLJFyvZ8Cz-Jpsf-GG76q-PRN8WKUX4uvHwORrizYSkZFBt1IKQOLjuIxQtX1dF_m5bd4mvAzWqUM3edpiARALpI-hGA-OrvZ-SeRzdzki9cp3uZTrGly2ddV0l6KAy1UXQe5LxySPbLoToHR_XORda6fg-7qo3rjbRnEUXoauJwGTrtVNUctDaotieE26e97QHS7dDrmj8DJ0PQkYdCNrXDP4mNemS_u2q_aOm_FReBm6ngRMuvaIhquNPo2h1i30MQrC1CeiGQH7p8_pS17WvdvsHYkvQ9iTgknY9pKECNExfxZcdf4y_ppTrn-6EB6JL0LYl4JJ2PaGdDgrkUDpKH6BaGY89-EcvNVuB9xYfhnGnhxMxrY4LkuE1o0SfTvJouN-P4sxweULprTqHY-5sfwijH05mIyB6VjYJvYhqgFE-y4w1Q6iflGZd0sbahx6f_sMnWZexlf0Zfir3JAtv3wVb68wwaiecGyc5wExbWuNfUa99cWbl6-87M5eLZmCy-B0HqtJzhq7nHQMhEQ_lauwf9Y0FQ3n_559XzVFF2c5ZfQmWWvccqo7ZjqJsA5DZ73WfFndjHc4LyZsB4cKt8Dck4thgdga25y2NHihMEx9TtURwrbEdburvh6pZV8Uy8J0HqKJzxa8ZCEdvMmIbbzfYQZuda_i5_bSO4xNeJE5OnnoJk_zASay3YAighOd6NtrkCCqoyQ0XFQlln63qs12l_OCPeCiu-iLQBCh8x6U-mLTbjn1QedaFX9vr08RdytdYdC2x37HgZp2sjnjcvexfE3TfihpH2i1PYNs21R9fYHZILM4NKeBmshM3zq2B40IDNu6_kwpytSLPAoEm_u2GChXMhGRfnEFCOpM4XOK4jp_eg3XfV1UmHEm8uKMv2ITnmA-TxrWFxk_AqbRoJmRvv9cvf8He7uCiw:1vLuTn:9mpos00LpzQqh1_HZUVfRUBLxHwEHsQ2fAFkpQ7KweM 2025-12-04 02:33:03.066594+00 -h4nbxaigtax9cnxud2hb4sd0uexilmw7 .eJzNmklz6ygQgP9KyucsWkCgd5z7HOc0mUqxxkpkSaMlrzKv8t8HUBaZYBshv5IvwUFNA1-Lphv0a_NAhn77MHSifSj45scm3lxP6yhhz6LSD_gTqR7rW1ZXfVvQWy1y-_60u_2z5qL84112T8GWdFvVWuCMZTijmchYniGOKUEpgFKkGYtzmaQ5RCmGLEtpkiNVCpGmUU4yHAkeJ1Ar3Ylq6JSuv3_dbyqyE_ebH1f3m_v7AYiYqAImJFZFRhJdQATR_eZaSRRqzKOsJO2VJDesaFkpxoc7PfJOP_6mNo8ZUgXmCdf6Ugg99A1tOT67I3xXVHd06IpKdN1dM7RMwRB1y0V7NwoTzv_yk1eS721oKwhn7bCj7lGHwni7vloM4O0f_VgUXD-NoyixK2JHL0BIOhkwB7pAJKLLrLfPAVKZ66mwLA4wYteTXsiy_vlpnpo-CdYfNuPBFnMMGcbFiTiMhW0-4NANuYBaW4JMFylL9GghihZxNr8a0qqe-mPL5WCLVTiHsrA5Q4fu6cBUIeNMV6Z5sohz09ZMexsPwB-iq5ANnb1NNjupGydYWw1FNF_-Bnu_uJdA1X_mNlU0S7cae4xSXRlBcI63t1K7wIw32IhfGO85TGz62NETSqhZIWrL_eoXy4xZfklVUqoLKIPCG-s1LuvHTxfcCqZiF-8V4Gi6io1-BznbYvnMfs9qI_1rpm0mTS7OJsFWiCPbLKlrS0-NrVXKolcnomYyCETZsuh0X21GU2HmEEn9Xxbp_5BgPp3Y1m7aouqL6vHzx1NNDxvaKe1v41A6J0GHErFtnJzsSWWiiV9240X7RHp3QP4CiPtzsBk7l42MIrNwAT4j41L5Im_AX8Lr0A0kYNMFdoUrh4ZI6owDYWA2Jg6A2aZynxD2aA5N9AEABMxwYGh06IlPav7tIIR0BXsoKlnrOIwPrGfKQOr9fz1yGHK4zQybBrJx5tFhPGwTurKQfd3zTHia9RzGl8B2hiqLrSsXAcKsZyjy9Jxsf5JWbOuhO5J8OKVX4hvGwObryjaAzOmoWyuFEIQELhNU3dA0ZXH0mNQhvA7WuVO3ebpyAZhKbLahlIyBrol-aRJydjkh9SK2hZLrW1J1Td32p7KA001XQR5KxyKfRC7dAGrdHwd5EWDGxoBP_D3CC_c_sWvK-lWI_rXxcx17DdZhfhYutgWcocx-T-fy1R8IZ_G-CNahnjpx5j7aQSltSYrOSXcn9GVV5RdmfAqvQzeQgE3XmfWMUf656bKh6-ud53b4KbwO3UACNl3nTZMU2s9kKTK6pQlkYJwFXct-Aft3KNhzUTWD39s7EV-HcCAFm7DrjgnBBLutpyszc0zNwq7BvwDqzwJUpiGL0s8ZT-VXIX4eKjZ_Vw6IKTWnXnl0Vv_BxUvBhPnpA3wivgrvUAo2YVcm-B4tYomzc0Z0H-Gxb0A3lV-HcSAHm7ErG8yBNLqxCRIxTz7320WMKameCWP14BlmTOVXYRzKwWac2ymjy3W_5_WQmtgRZSZFMneKy-4pYoPD7C9fhwcLr6Nq9jz-1WHgozh9GeVuMMOogXBcnJcBsUyZOjOgyWhD8RbVi6j6o4ertuA6OL3napNzZjd7A4MxNR-L6IOvRa-pbIX47-gXBrbo6iznzN4m68xs9nWn3BQJMYHRovvKb6ubi54U5Qx38N7gEpgHcrEt4Mx-9nsaswAUZyG76gRhV5Gm29bft9RqKMt1YXpP0cYH7QrXSR-mBJjC3DdAgJnJqvB4tAgcw-90n922ECW_IWV_cu-c5g3uCejUxm0h_QG1yXwOr4MvEf83_wyTPpEgHZuobRZXgK5Wi-P7p-5DSXfD6scjyB7bemhOMBtlVofmNVEbGbYrnGmkxHB0QyY3TXL9DQWOJF8WCwIR6dAHyMTckUOMTKUM8fqkKR5e4ruhKWvCBT9-GOASnmG-QBrOO7QwApbRgP3lUapC_M3b_4NBlrY:1vNpQw:JJKhKn0v0UW9V6_rJqP1yuhP01rBLH7J3y9JvZbBFK8 2025-12-09 09:34:02.767355+00 +0qv1nnfydmil7knzisuz8cbfc9a92fqv .eJzNmklz6ygQgP9KyucsWkCgd5z7HOc0mUqxxkpkSaMlrzKv8t8HUBaZYBshv5IvwUFNA1-Lphv0a_NAhn77MHSifSj45scm3lxP6yhhz6LSD_gTqR7rW1ZXfVvQWy1y-_60u_2z5qL84112T8GWdFvVWuCMZTijmchYniGOKUEpgFKkGYtzmaQ5RCmGLEtpkiNVCpGmUU4yHAkeJ1Ar3Ylq6JSuv3_dbyqyE_ebH1f3m_v7AYiYqAImJFZFRhJdQATR_eZaSRRqzKOsJO2VJDesaFkpxoc7PfJOP_6mNo8ZUgXmCdf6Ugg99A1tOT67I3xXVHd06IpKdN1dM7RMwRB1y0V7NwoTzv_yk1eS721oKwhn7bCj7lGHwni7vloM4O0f_VgUXD-NoyixK2JHL0BIOhkwB7pAJKLLrLfPAVKZ66mwLA4wYteTXsiy_vlpnpo-CdYfNuPBFnMMGcbFiTiMhW0-4NANuYBaW4JMFylL9GghihZxNr8a0qqe-mPL5WCLVTiHsrA5Q4fu6cBUIeNMV6Z5sohz09ZMexsPwB-iq5ANnb1NNjupGydYWw1FNF_-Bnu_uJdA1X_mNlU0S7cae4xSXRlBcI63t1K7wIw32IhfGO85TGz62NETSqhZIWrL_eoXy4xZfklVUqoLKIPCG-s1LuvHTxfcCqZiF-8V4Gi6io1-BznbYvnMfs9qI_1rpm0mTS7OJsFWiCPbLKlrS0-NrVXKolcnomYyCETZsuh0X21GU2HmEEn9Xxbp_5BgPp3Y1m7aouqL6vHzx1NNDxvaKe1v41A6J0GHErFtnJzsSWWiiV9240X7RHp3QP4CiPtzsBk7l42MIrNwAT4j41L5Im_AX8Lr0A0kYNMFdoUrh4ZI6owDYWA2Jg6A2aZynxD2aA5N9AEABMxwYGh06IlPav7tIIR0BXsoKlnrOIwPrGfKQOr9fz1yGHK4zQybBrJx5tFhPGwTurKQfd3zTHia9RzGl8B2hiqLrSsXAcKsZyjy9Jxsf5JWbOuhO5J8OKVX4hvGwObryjaAzOmoWyuFEIQELhNU3dA0ZXH0mNQhvA7WuVO3ebpyAZhKbLahlIyBrol-aRJydjkh9SK2hZLrW1J1Td32p7KA001XQR5KxyKfRC7dAGrdHwd5EWDGxoBP_D3CC_c_sWvK-lWI_rXxcx17DdZhfhYutgWcocx-T-fy1R8IZ_G-CNahnjpx5j7aQSltSYrOSXcn9GVV5RdmfAqvQzeQgE3XmfWMUf656bKh6-ud53b4KbwO3UACNl3nTZMU2s9kKTK6pQlkYJwFXct-Aft3KNhzUTWD39s7EV-HcCAFm7DrjgnBBLutpyszc0zNwq7BvwDqzwJUpiGL0s8ZT-VXIX4eKjZ_Vw6IKTWnXnl0Vv_BxUvBhPnpA3wivgrvUAo2YVcm-B4tYomzc0Z0H-Gxb0A3lV-HcSAHm7ErG8yBNLqxCRIxTz7320WMKameCWP14BlmTOVXYRzKwWac2ymjy3W_5_WQmtgRZSZFMneKy-4pYoPD7C9fhwcLr6Nq9jz-1WHgozh9GeVuMMOogXBcnJcBsUyZOjOgyWhD8RbVi6j6o4ertuA6OL3napNzZjd7A4MxNR-L6IOvRa-pbIX47-gXBrbo6iznzN4m68xs9nWn3BQJMYHRovvKb6ubi54U5Qx38N7gEpgHcrEt4Mx-9nsaswAUZyG76gRhV5Gm29bft9RqKMt1YXpP0cYH7QrXSR-mBJjC3DdAgJnJqvB4tAgcw-90n922ECW_IWV_cu-c5g3uCejUxm0h_QG1yXwOr4MvEf83_wyTPpEgHZuobRZXgK5Wi-P7p-5DSXfD6scjyB7bemhOMBtlVofmNVEbGbYrnGmkxHB0QyY3TXL9DQWOJF8WCwIR6dAHyMTckUOMTKUM8fqkKR5e4ruhKWvCBT9-GOASnmG-QBrOO7QwApbRgP3lUapC_M3b_4NBlrY:1vOFTv:avJ7TQz_fiJiww_ZY9F1EAtmvUPKd221DjK5dvhiC2A 2025-12-10 13:22:51.388841+00 nie1ws9ihz1c2tlxhfbz47rmvgtv4p6c .eJzNmklz6ygQgP9KyucsWkCgd5z7HOc0mUqxxkpkSaMlrzKv8t8HUBaZYBshv5IvwUFNA1-Lphv0a_NAhn77MHSifSj45scm3lxP6yhhz6LSD_gTqR7rW1ZXfVvQWy1y-_60u_2z5qL84112T8GWdFvVWuCMZTijmchYniGOKUEpgFKkGYtzmaQ5RCmGLEtpkiNVCpGmUU4yHAkeJ1Ar3Ylq6JSuv3_dbyqyE_ebH1f3m_v7AYiYqAImJFZFRhJdQATR_eZaSRRqzKOsJO2VJDesaFkpxoc7PfJOP_6mNo8ZUgXmCdf6Ugg99A1tOT67I3xXVHd06IpKdN1dM7RMwRB1y0V7NwoTzv_yk1eS721oKwhn7bCj7lGHwni7vloM4O0f_VgUXD-NoyixK2JHL0BIOhkwB7pAJKLLrLfPAVKZ66mwLA4wYteTXsiy_vlpnpo-CdYfNuPBFnMMGcbFiTiMhW0-4NANuYBaW4JMFylL9GghihZxNr8a0qqe-mPL5WCLVTiHsrA5Q4fu6cBUIeNMV6Z5sohz09ZMexsPwB-iq5ANnb1NNjupGydYWw1FNF_-Bnu_uJdA1X_mNlU0S7cae4xSXRlBcI63t1K7wIw32IhfGO85TGz62NETSqhZIWrL_eoXy4xZfklVUqoLKIPCG-s1LuvHTxfcCqZiF-8V4Gi6io1-BznbYvnMfs9qI_1rpm0mTS7OJsFWiCPbLKlrS0-NrVXKolcnomYyCETZsuh0X21GU2HmEEn9Xxbp_5BgPp3Y1m7aouqL6vHzx1NNDxvaKe1v41A6J0GHErFtnJzsSWWiiV9240X7RHp3QP4CiPtzsBk7l42MIrNwAT4j41L5Im_AX8Lr0A0kYNMFdoUrh4ZI6owDYWA2Jg6A2aZynxD2aA5N9AEABMxwYGh06IlPav7tIIR0BXsoKlnrOIwPrGfKQOr9fz1yGHK4zQybBrJx5tFhPGwTurKQfd3zTHia9RzGl8B2hiqLrSsXAcKsZyjy9Jxsf5JWbOuhO5J8OKVX4hvGwObryjaAzOmoWyuFEIQELhNU3dA0ZXH0mNQhvA7WuVO3ebpyAZhKbLahlIyBrol-aRJydjkh9SK2hZLrW1J1Td32p7KA001XQR5KxyKfRC7dAGrdHwd5EWDGxoBP_D3CC_c_sWvK-lWI_rXxcx17DdZhfhYutgWcocx-T-fy1R8IZ_G-CNahnjpx5j7aQSltSYrOSXcn9GVV5RdmfAqvQzeQgE3XmfWMUf656bKh6-ud53b4KbwO3UACNl3nTZMU2s9kKTK6pQlkYJwFXct-Aft3KNhzUTWD39s7EV-HcCAFm7DrjgnBBLutpyszc0zNwq7BvwDqzwJUpiGL0s8ZT-VXIX4eKjZ_Vw6IKTWnXnl0Vv_BxUvBhPnpA3wivgrvUAo2YVcm-B4tYomzc0Z0H-Gxb0A3lV-HcSAHm7ErG8yBNLqxCRIxTz7320WMKameCWP14BlmTOVXYRzKwWac2ymjy3W_5_WQmtgRZSZFMneKy-4pYoPD7C9fhwcLr6Nq9jz-1WHgozh9GeVuMMOogXBcnJcBsUyZOjOgyWhD8RbVi6j6o4ertuA6OL3napNzZjd7A4MxNR-L6IOvRa-pbIX47-gXBrbo6iznzN4m68xs9nWn3BQJMYHRovvKb6ubi54U5Qx38N7gEpgHcrEt4Mx-9nsaswAUZyG76gRhV5Gm29bft9RqKMt1YXpP0cYH7QrXSR-mBJjC3DdAgJnJqvB4tAgcw-90n922ECW_IWV_cu-c5g3uCejUxm0h_QG1yXwOr4MvEf83_wyTPpEgHZuobRZXgK5Wi-P7p-5DSXfD6scjyB7bemhOMBtlVofmNVEbGbYrnGmkxHB0QyY3TXL9DQWOJF8WCwIR6dAHyMTckUOMTKUM8fqkKR5e4ruhKWvCBT9-GOASnmG-QBrOO7QwApbRgP3lUapC_M3b_4NBlrY:1vNnsc:A97uezglXSc0c9SooQ51Ya2whoGFmD_LkLGlbrTqyKk 2025-12-09 07:54:30.634534+00 +9dk1ipkwk83y8dt19pgoqea4is9ysvl3 .eJzNmklz6ygQgP9KyucsWkCgd5z7HOc0nkqxxkpkSaMlrzKv8t8HUBLLBNsS0iv5YlyoaeBrlm7g1-aRdO3usWtE_ZjxzY9NuLkd5lHCXkShP_BnUjyV96ws2jqj91rk_uNrc_9nyUX-x4fskYIdaXaqtMAJS3BCE5GwNEEcU4JiAKWIExamMopTiGIMWRLTKEUqFSKOg5QkOBA8jKBWuhdF1yhdf__abgqyF9vNj5vtZrvtgAiJSmBEQpUkJNIJRBBtN7dKIlNt7mUlqW8kuWNZzXLRf9zrljf68ze1aciQSjCPuNYXQzhCX1fn_bcHwvdZ8UC7JitE0zxUXc0UDFHWXNQPvTDh_K9x8kryowytBeGs7vbU3WpfGO-3N7MBvP-jP4uM669hEETjlKpmJoFQCRIsWYxx1or9VM6mzLWwnoLFJh_bGaGjTiAkHTSfA50gEtB58-aYCqQy1R1jSehh2qYlrZB5-fPLYCV9Fqw9bdiTJaaY1Y-LE7EfC9t80KEbcqEHCY6QqSJmkW4tRMEszuZfRWpVU3tuoTpZYhXOvixszolD97BhKpFhojPjNJrFuapLptefEYA_RVch69t7myy6qBtHWFsNBTSdP4JHD9xroDq-5zZVPEm3anuIYp0ZQLDE6C3ULjBhBBvxK-M9hYlNP3XUhCJqZojacg_1Ypkwa11SmZTqBEovx9Iaxnn59LUE14Ipb2b0DHAUXcVGv4OcZbEwmFjvojbS_ybaZlDk6mzib4XQnkjAtaXHxtYqWNSzE1HTGQSCMRHCGe_0WG1CY2H6EMjZYUhVZ0WbFU9ff55LetrQTunxNvalcxG0LxHbxvHFmjAl0dY7sP7G70JgfUL-CoiP52Azdk4bGQRm4gK8IONcrUWjAR-E16HrScCmC-0M13EGRFJHHAgDszFxAMw2lY5xYc_G0EQfB0DADAeG-gU9GhOafzseIU3GHrNCltoP4x1rmTKQGv9vZ45HTpeZYFNPNs442o-HbUJXFHKse5oJL7Oewvga2E5QZbF1xSJAmPkMRRovyfYnqcWu7JozwYdTeiW-fgxsvq5oA8iU9rq1UgiB1_npAVXTVVWenT2gdgivg3Vq1y2ekSsWgLHEZhuKSe_oGu-XRj5nlwNSr2KXKbm2JkVTlXV7KQq4XHQV5L50bPKuw2gIoNb9eZAXAGZsDPhgvUd45v4n9lVevgnRvlXjlo6jAuswX4SLbQGnK3Nc01Jr9SfCSbyvgrXvSh05Yx-9QCltUYyWpLsX-vqqGOdmfAmvQ9eTgE3XGfX0Xv7SdFnXtOV-5Hb4JbwOXU8CNl3nTZMUep1JYmR0S-PIwDDxuhA_APu3y9hLVlTduNE7EF-HsCcFm7DrjgnBCLutpzMTc0zN_B4gHADqBxkq0pBZPm4xHsqvQnwZKjZ_VwyIKTWnXmmw6PrBxWvGhPk7BvhAfBXevhRswq5I8MNbxBInS3p0n-7xWIduKL8OY08ONmNXNJgCaXRj4yRiHn3tt7MYU1K8EMbKbqSbMZRfhbEvB4txHNghuGvp_ojrITW-I0pMiGTuFOfdU4QGh9lfDocHM6-jSvbS_2o38ElcvoxyF5hgVE84Ls7zgNi2dUZAg9b64s2KV1G0Zw9XbcF1cI7uq03OGd0cNQyG1DwW0Qdfs4aprIX47-wLA1t0dZZTem-TdUY2x7pjbpKIGMdo1n3lt9nNRUuyfMJy8FHgGph7crEt4Ix-jmvqowAUJj676gBhU5Cq2ZXft9Siy_N1YY7uoo0vsTNcJ32YEmASc98AAWYmqsL90SJwNL_RdTa7TOT8juTtxb1zGDe4O6BDG7eF9NN1E_mcngcHkfEjf4FOXwiQznXUNovLQVezxfH-qflU0tyx8ukMsqe67KoLzHqZ1aGN6qiNLLUznGGkxLBfhkxsGqX6DQUOJJ_nCwIRaNcHyMjckUOMTKb0WfVJlT2-hg9dlZeEC37-MMAlPMF8njScd2h-BCyjAfvlEVAu_ub9f9O_D4c:1vOXQZ:LJRQQAEQPL57SgmCuYg0323wuqRZN7iv5lWKX4fiM9E 2025-12-11 08:32:35.073302+00 ntf30dd3of18gsyba1risn3yscoj0t6y .eJzNmktz4ygQgP9Kyuck1guB5rj3Pe5pPZXiaSuRJa0emcpO5b8vIMeRCbER8pZ8CQ40DXzNoxv0e_WE-2731Le8ecrZ6scqXN2P8wimL7xUBewZl9vqkVZl1-TkUYk8Hkrbxz8rxos_DrInCna43cnaHKU0RSlJeUqzFDJEMIwTIHic0jATUZwBGCNA05hEGZQp53EcZDhFAWdhBJTSPS_7Vur6-_dmVeI936x-3G1Wm02fcEFkAiIcyiRliUogDshmdS8lctnnQVbg5k7gB5o3tOBD4V71vFXFFrUh_lQLiMhkgmgaOqjtm2IoW2O2z8t12-GOi6L6tSZ9m5e8bSvyzGm3HsQxY3-51pCyh1qk4ZjRpt-T7wbgx-X9_u5aLN5_qmKeM1UaBkFk0Q0YB0pbBHUTMY1UbwEMZnHWv2rcyJY63rhwNmoswtmXhck5tuged0wmIkxVZpxFszjXTUXl5HQB_CG6CFnf0Ztkk4u6UYSU1WBAsvkz2Hni3gJV95GbVMEk3bLvIYxVZgCSa8zeUp4CE2awFr8x3lOYmPRTS0swInqFBMGo00ik1NiXZCYhKgECgPnzvai2xy244bRqmPMKsFRdxEb_BznTYnBiu1e1kfo10TajKjdnE38rIDMjtB3psba1dHrV6oREDwYmQTrPOz1Vm5KY6zEEQv2XBuo_yKlLI6a16yYvu7zcHn88V-R7Q1ul3W3sS-ciaF8ihknD4GJLMpaJdKbX2vrCT66Rc_7qN_I3QNydg8nYumxEEOiFm6ArMi7kXuQM-FN4GbqeBEy6kblLZbbGoFARB0SJPphYkuhjKnNxYc_G0Bgq7QnVHCgcNvTIJTQ3rUhwm9OnvBSV8sNYTzsqDSTn_9v3xjxTZ4JNPdlY42g_HqZNbVHIqe5pJrzMegrjW2A7QZXB1haLJFyvZ8Cz-Jpsf-GG76q-PRN8WKUX4uvHwORrizYSkZFBt1IKQOLjuIxQtX1dF_m5bd4mvAzWqUM3edpiARALpI-hGA-OrvZ-SeRzdzki9cp3uZTrGly2ddV0l6KAy1UXQe5LxySPbLoToHR_XORda6fg-7qo3rjbRnEUXoauJwGTrtVNUctDaotieE26e97QHS7dDrmj8DJ0PQkYdCNrXDP4mNemS_u2q_aOm_FReBm6ngRMuvaIhquNPo2h1i30MQrC1CeiGQH7p8_pS17WvdvsHYkvQ9iTgknY9pKECNExfxZcdf4y_ppTrn-6EB6JL0LYl4JJ2PaGdDgrkUDpKH6BaGY89-EcvNVuB9xYfhnGnhxMxrY4LkuE1o0SfTvJouN-P4sxweULprTqHY-5sfwijH05mIyB6VjYJvYhqgFE-y4w1Q6iflGZd0sbahx6f_sMnWZexlf0Zfir3JAtv3wVb68wwaiecGyc5wExbWuNfUa99cWbl6-87M5eLZmCy-B0HqtJzhq7nHQMhEQ_lauwf9Y0FQ3n_559XzVFF2c5ZfQmWWvccqo7ZjqJsA5DZ73WfFndjHc4LyZsB4cKt8Dck4thgdga25y2NHihMEx9TtURwrbEdburvh6pZV8Uy8J0HqKJzxa8ZCEdvMmIbbzfYQZuda_i5_bSO4xNeJE5OnnoJk_zASay3YAighOd6NtrkCCqoyQ0XFQlln63qs12l_OCPeCiu-iLQBCh8x6U-mLTbjn1QedaFX9vr08RdytdYdC2x37HgZp2sjnjcvexfE3TfihpH2i1PYNs21R9fYHZILM4NKeBmshM3zq2B40IDNu6_kwpytSLPAoEm_u2GChXMhGRfnEFCOpM4XOK4jp_eg3XfV1UmHEm8uKMv2ITnmA-TxrWFxk_AqbRoJmRvv9cvf8He7uCiw:1vLId5:n8_xdalAfSPyiCi7SJPZLr1CI1IGxTC-WpvvN_z9NHQ 2025-12-02 10:08:07.630045+00 8g6qqs5ssg2zi3dlvoj8dmvskek2nues .eJzNmktz4ygQgP9Kyuck1guB5rj3Pe5pPZXiaSuRJa0emcpO5b8vIMeRCbER8pZ8CQ40DXzNoxv0e_WE-2731Le8ecrZ6scqXN2P8wimL7xUBewZl9vqkVZl1-TkUYk8Hkrbxz8rxos_DrInCna43cnaHKU0RSlJeUqzFDJEMIwTIHic0jATUZwBGCNA05hEGZQp53EcZDhFAWdhBJTSPS_7Vur6-_dmVeI936x-3G1Wm02fcEFkAiIcyiRliUogDshmdS8lctnnQVbg5k7gB5o3tOBD4V71vFXFFrUh_lQLiMhkgmgaOqjtm2IoW2O2z8t12-GOi6L6tSZ9m5e8bSvyzGm3HsQxY3-51pCyh1qk4ZjRpt-T7wbgx-X9_u5aLN5_qmKeM1UaBkFk0Q0YB0pbBHUTMY1UbwEMZnHWv2rcyJY63rhwNmoswtmXhck5tuged0wmIkxVZpxFszjXTUXl5HQB_CG6CFnf0Ztkk4u6UYSU1WBAsvkz2Hni3gJV95GbVMEk3bLvIYxVZgCSa8zeUp4CE2awFr8x3lOYmPRTS0swInqFBMGo00ik1NiXZCYhKgECgPnzvai2xy244bRqmPMKsFRdxEb_BznTYnBiu1e1kfo10TajKjdnE38rIDMjtB3psba1dHrV6oREDwYmQTrPOz1Vm5KY6zEEQv2XBuo_yKlLI6a16yYvu7zcHn88V-R7Q1ul3W3sS-ciaF8ihknD4GJLMpaJdKbX2vrCT66Rc_7qN_I3QNydg8nYumxEEOiFm6ArMi7kXuQM-FN4GbqeBEy6kblLZbbGoFARB0SJPphYkuhjKnNxYc_G0Bgq7QnVHCgcNvTIJTQ3rUhwm9OnvBSV8sNYTzsqDSTn_9v3xjxTZ4JNPdlY42g_HqZNbVHIqe5pJrzMegrjW2A7QZXB1haLJFyvZ8Cz-Jpsf-GG76q-PRN8WKUX4uvHwORrizYSkZFBt1IKQOLjuIxQtX1dF_m5bd4mvAzWqUM3edpiARALpI-hGA-OrvZ-SeRzdzki9cp3uZTrGly2ddV0l6KAy1UXQe5LxySPbLoToHR_XORda6fg-7qo3rjbRnEUXoauJwGTrtVNUctDaotieE26e97QHS7dDrmj8DJ0PQkYdCNrXDP4mNemS_u2q_aOm_FReBm6ngRMuvaIhquNPo2h1i30MQrC1CeiGQH7p8_pS17WvdvsHYkvQ9iTgknY9pKECNExfxZcdf4y_ppTrn-6EB6JL0LYl4JJ2PaGdDgrkUDpKH6BaGY89-EcvNVuB9xYfhnGnhxMxrY4LkuE1o0SfTvJouN-P4sxweULprTqHY-5sfwijH05mIyB6VjYJvYhqgFE-y4w1Q6iflGZd0sbahx6f_sMnWZexlf0Zfir3JAtv3wVb68wwaiecGyc5wExbWuNfUa99cWbl6-87M5eLZmCy-B0HqtJzhq7nHQMhEQ_lauwf9Y0FQ3n_559XzVFF2c5ZfQmWWvccqo7ZjqJsA5DZ73WfFndjHc4LyZsB4cKt8Dck4thgdga25y2NHihMEx9TtURwrbEdburvh6pZV8Uy8J0HqKJzxa8ZCEdvMmIbbzfYQZuda_i5_bSO4xNeJE5OnnoJk_zASay3YAighOd6NtrkCCqoyQ0XFQlln63qs12l_OCPeCiu-iLQBCh8x6U-mLTbjn1QedaFX9vr08RdytdYdC2x37HgZp2sjnjcvexfE3TfihpH2i1PYNs21R9fYHZILM4NKeBmshM3zq2B40IDNu6_kwpytSLPAoEm_u2GChXMhGRfnEFCOpM4XOK4jp_eg3XfV1UmHEm8uKMv2ITnmA-TxrWFxk_AqbRoJmRvv9cvf8He7uCiw:1vLYyA:AD0u5F-mNfMwOsOq-xELhPG46cKCAbcOeMXQfrwkvng 2025-12-03 03:34:58.965151+00 \. @@ -2163,29 +2344,40 @@ ntf30dd3of18gsyba1risn3yscoj0t6y .eJzNmktz4ygQgP9Kyuck1guB5rj3Pe5pPZXiaSuRJa0emc -- Data for Name: printing_plateorder; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.printing_plateorder (id, created_at, updated_at, design_code, plate_type, plate_date, plate_method, plate_image, plate_notes, reprint_reason, urgency_level, area, default_address, is_mark_frame, drawing_rating, color_matching_rating, sample_rating, difficulty_rating, required_completion_date, completion_date, fabric, width, style_name, production_method, sample_meter, required_sample_meters, approval_result, is_ordered, customer_feedback, business_object_id, customer_id, merchandiser_id, salesperson_id, is_invalid, process, fabric_source, designer_id) FROM stdin; -1 2025-11-18 09:55:35.334641+00 2025-11-18 09:55:35.334647+00 \N 首版 2025-11-17 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 1 3 \N \N f 1 \N \N -2 2025-11-18 09:58:01.444122+00 2025-11-18 09:58:01.444129+00 123 首版 2025-11-17 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N 123123 \N \N \N \N \N \N f \N 2 3 \N \N f 1 \N \N -3 2025-11-18 09:58:20.379338+00 2025-11-18 09:58:20.379345+00 123 复版 2025-11-17 16:00:00+00 \N 123 123 正常 213 \N f \N \N \N \N \N \N 123 123 \N \N \N \N \N f \N 3 3 \N 1 f 1 \N \N -5 2025-11-18 10:10:04.816531+00 2025-11-18 10:10:04.816538+00 \N 首版 2025-11-17 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 6 3 \N \N f 2 \N \N -6 2025-11-18 10:10:55.523408+00 2025-11-18 10:10:55.523419+00 \N 首版 2025-11-17 16:00:00+00 \N 123 \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 7 3 \N \N f 2 \N \N -7 2025-11-18 10:35:25.877134+00 2025-11-18 10:35:25.877145+00 \N 首版 2025-11-17 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 9 4 \N \N f 2 \N \N -8 2025-11-19 01:44:43.685246+00 2025-11-19 01:52:40.560325+00 \N 复版 2025-11-18 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 12 3 \N \N f 2 \N \N -9 2025-11-19 02:00:49.484416+00 2025-11-19 02:00:49.484427+00 \N 首版 2025-11-18 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 13 3 \N \N f 2 \N \N -4 2025-11-18 10:09:47.337719+00 2025-11-19 02:29:58.260464+00 \N 首版 2025-11-17 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 5 3 \N \N t 2 \N \N -80004 2025-11-21 08:45:33.728595+00 2025-11-21 08:45:33.728604+00 \N 首版 2025-11-20 16:00:00+00 图片开版 \N \N 加急 中大 \N f 画图难度1 调色难度1 \N \N 2025-11-21 2025-11-21 16:00:00+00 四面弹 1.5米 \N 定位 \N \N \N f \N 27 3 1 1 f 2 仓库布 \N -80005 2025-11-22 02:39:05.937858+00 2025-11-22 02:39:05.937868+00 \N 首版 2025-11-21 16:00:00+00 图片开版 \N \N 加急 \N f \N \N \N \N 2025-11-22 2025-11-22 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 28 3 \N \N f 2 仓库布 \N -10 2025-11-19 02:00:59.58389+00 2025-11-19 03:54:21.995604+00 \N 复版 2025-11-18 16:00:00+00 \N plate_images/QQ浏览器截图20250609233935_XYYq9Lt.png \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 14 3 \N \N f 2 \N \N -11 2025-11-19 02:04:48.932631+00 2025-11-19 09:27:56.024307+00 123123 首版 2025-11-18 16:00:00+00 样衣开版 plate_images/QQ浏览器截图20250609233935_ARgZcEm.png 123 123123 正常 123 213 f A A A \N 2025-11-19 \N 123 123 3123 32 \N 123.00 \N f \N 15 3 2 1 f 2 \N \N -12 2025-11-19 09:28:09.459625+00 2025-11-19 09:28:09.459634+00 \N 复版 2025-11-18 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 19 3 \N \N f 2 \N \N -80006 2025-11-23 13:11:02.777704+00 2025-11-23 13:11:02.777711+00 \N 修改单 2025-11-22 16:00:00+00 \N plate_images/5cab1e39605388041cf59ea15a2177c7.jpg 改颜色, \N 加急 中大 \N f \N \N \N \N \N 2025-11-23 16:00:00+00 四面弹 1.5米 \N 匹布 \N 3.00 \N f \N 29 5 2 2 f 2 仓库布 \N -13 2025-11-19 09:28:40.840111+00 2025-11-19 09:42:26.990677+00 \N 首版 2025-11-18 16:00:00+00 样衣开版 plate_images/QQ浏览器截图20250609233935_y9vJ9LT.png 123 213 正常 312312 321 f A A A \N 2025-11-19 \N 3123 312 312 批布 \N 123.00 \N f \N 20 3 1 1 f 2 \N \N -14 2025-11-20 01:52:49.284672+00 2025-11-20 01:52:49.284679+00 \N 首版 2025-11-19 16:00:00+00 \N \N \N 正常 123 f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 21 3 \N \N f 2 \N \N -80000 2025-11-20 03:48:01.383018+00 2025-11-20 03:48:01.383027+00 \N 首版 2025-11-19 16:00:00+00 \N \N \N 正常 123 \N f \N \N \N \N \N \N 123 123 123 \N \N \N \N f \N 22 3 1 1 f 2 123 \N -80001 2025-11-20 03:49:28.25281+00 2025-11-20 03:49:28.252817+00 \N 首版 2025-11-19 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 23 4 \N 1 f 2 \N \N -80002 2025-11-20 03:49:37.949557+00 2025-11-20 03:49:37.949565+00 \N 首版 2025-11-19 16:00:00+00 \N \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 24 4 \N \N f 1 \N \N -80003 2025-11-20 06:55:47.636958+00 2025-11-20 07:08:53.430169+00 80003 首版 2025-11-19 16:00:00+00 图片开版 plate_images/微信图片_2025-11-20_145534_377.png 123 123 加急 中大 \N f 画图难度1 调色难度1 套样难度1 难度1 2025-11-20 2025-11-19 16:00:00+00 四面弹 1.5米 \N 匹布 \N 123.00 待审批 f 123 25 3 1 1 f 2 仓库布 \N -80007 2025-11-24 11:33:15.702111+00 2025-11-24 11:33:15.702118+00 \N 首版 2025-11-23 16:00:00+00 图片开版 \N \N 加急 周边 \N f \N \N \N \N 2025-11-24 2025-11-24 16:00:00+00 四面弹 1.5米 \N 匹布 \N 3.00 \N f \N 30 6 2 2 f 2 仓库布 \N +COPY public.printing_plateorder (id, created_at, updated_at, design_code, plate_type, plate_date, plate_method, plate_image, plate_notes, reprint_reason, urgency_level, area, default_address, is_mark_frame, drawing_rating, color_matching_rating, sample_rating, difficulty_rating, required_completion_date, completion_date, fabric, width, style_name, production_method, sample_meter, required_sample_meters, approval_result, is_ordered, customer_feedback, business_object_id, customer_id, merchandiser_id, salesperson_id, is_invalid, process, fabric_source, designer_id, image_name, print_count) FROM stdin; +80009 2025-11-27 08:08:49.408011+00 2025-11-27 08:08:49.408018+00 \N 首版 2025-11-26 16:00:00+00 图片开版 [] \N \N 加急 \N f \N \N \N \N 2025-11-27 00:00:00+00 2025-11-27 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 32 3 \N \N f 2 仓库布 \N image.png 0 +80008 2025-11-27 07:23:06.020811+00 2025-11-27 07:23:06.02082+00 \N 首版 2025-11-26 16:00:00+00 图片开版 [] \N \N 加急 \N f \N \N \N \N 2025-11-27 00:00:00+00 2025-11-27 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 31 3 \N \N f 2 仓库布 \N image.png 0 +80007 2025-11-24 11:33:15.702111+00 2025-11-24 11:33:15.702118+00 \N 首版 2025-11-23 16:00:00+00 图片开版 [] \N \N 加急 周边 \N f \N \N \N \N 2025-11-24 00:00:00+00 2025-11-24 16:00:00+00 四面弹 1.5米 \N 匹布 \N 3.00 \N f \N 30 6 2 2 f 2 仓库布 \N \N 0 +80006 2025-11-23 13:11:02.777704+00 2025-11-23 13:11:02.777711+00 \N 修改单 2025-11-22 16:00:00+00 \N [] 改颜色, \N 加急 中大 \N f \N \N \N \N \N 2025-11-23 16:00:00+00 四面弹 1.5米 \N 匹布 \N 3.00 \N f \N 29 5 2 2 f 2 仓库布 \N \N 0 +80005 2025-11-22 02:39:05.937858+00 2025-11-22 02:39:05.937868+00 \N 首版 2025-11-21 16:00:00+00 图片开版 [] \N \N 加急 \N f \N \N \N \N 2025-11-22 00:00:00+00 2025-11-22 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 28 3 \N \N f 2 仓库布 \N \N 0 +80004 2025-11-21 08:45:33.728595+00 2025-11-21 08:45:33.728604+00 \N 首版 2025-11-20 16:00:00+00 图片开版 [] \N \N 加急 中大 \N f 画图难度1 调色难度1 \N \N 2025-11-21 00:00:00+00 2025-11-21 16:00:00+00 四面弹 1.5米 \N 定位 \N \N \N f \N 27 3 1 1 f 2 仓库布 \N \N 0 +80003 2025-11-20 06:55:47.636958+00 2025-11-20 07:08:53.430169+00 80003 首版 2025-11-19 16:00:00+00 图片开版 [] 123 123 加急 中大 \N f 画图难度1 调色难度1 套样难度1 难度1 2025-11-20 00:00:00+00 2025-11-19 16:00:00+00 四面弹 1.5米 \N 匹布 \N 123.00 待审批 f 123 25 3 1 1 f 2 仓库布 \N \N 0 +80002 2025-11-20 03:49:37.949557+00 2025-11-20 03:49:37.949565+00 \N 首版 2025-11-19 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 24 4 \N \N f 1 \N \N \N 0 +80001 2025-11-20 03:49:28.25281+00 2025-11-20 03:49:28.252817+00 \N 首版 2025-11-19 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 23 4 \N 1 f 2 \N \N \N 0 +80000 2025-11-20 03:48:01.383018+00 2025-11-20 03:48:01.383027+00 \N 首版 2025-11-19 16:00:00+00 \N [] \N \N 正常 123 \N f \N \N \N \N \N \N 123 123 123 \N \N \N \N f \N 22 3 1 1 f 2 123 \N \N 0 +14 2025-11-20 01:52:49.284672+00 2025-11-20 01:52:49.284679+00 \N 首版 2025-11-19 16:00:00+00 \N [] \N \N 正常 123 f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 21 3 \N \N f 2 \N \N \N 0 +12 2025-11-19 09:28:09.459625+00 2025-11-19 09:28:09.459634+00 \N 复版 2025-11-18 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 19 3 \N \N f 2 \N \N \N 0 +11 2025-11-19 02:04:48.932631+00 2025-11-19 09:27:56.024307+00 123123 首版 2025-11-18 16:00:00+00 样衣开版 [] 123 123123 正常 123 213 f A A A \N 2025-11-19 00:00:00+00 \N 123 123 3123 32 \N 123.00 \N f \N 15 3 2 1 f 2 \N \N \N 0 +10 2025-11-19 02:00:59.58389+00 2025-11-19 03:54:21.995604+00 \N 复版 2025-11-18 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 14 3 \N \N f 2 \N \N \N 0 +9 2025-11-19 02:00:49.484416+00 2025-11-19 02:00:49.484427+00 \N 首版 2025-11-18 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 13 3 \N \N f 2 \N \N \N 0 +8 2025-11-19 01:44:43.685246+00 2025-11-19 01:52:40.560325+00 \N 复版 2025-11-18 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 12 3 \N \N f 2 \N \N \N 0 +7 2025-11-18 10:35:25.877134+00 2025-11-18 10:35:25.877145+00 \N 首版 2025-11-17 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 9 4 \N \N f 2 \N \N \N 0 +6 2025-11-18 10:10:55.523408+00 2025-11-18 10:10:55.523419+00 \N 首版 2025-11-17 16:00:00+00 \N [] 123 \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 7 3 \N \N f 2 \N \N \N 0 +5 2025-11-18 10:10:04.816531+00 2025-11-18 10:10:04.816538+00 \N 首版 2025-11-17 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 6 3 \N \N f 2 \N \N \N 0 +4 2025-11-18 10:09:47.337719+00 2025-11-19 02:29:58.260464+00 \N 首版 2025-11-17 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 5 3 \N \N t 2 \N \N \N 0 +3 2025-11-18 09:58:20.379338+00 2025-11-18 09:58:20.379345+00 123 复版 2025-11-17 16:00:00+00 \N [] 123 123 正常 213 \N f \N \N \N \N \N \N 123 123 \N \N \N \N \N f \N 3 3 \N 1 f 1 \N \N \N 0 +2 2025-11-18 09:58:01.444122+00 2025-11-18 09:58:01.444129+00 123 首版 2025-11-17 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N 123123 \N \N \N \N \N \N f \N 2 3 \N \N f 1 \N \N \N 0 +1 2025-11-18 09:55:35.334641+00 2025-11-18 09:55:35.334647+00 \N 首版 2025-11-17 16:00:00+00 \N [] \N \N 正常 \N f \N \N \N \N \N \N \N \N \N \N \N \N \N f \N 1 3 \N \N f 1 \N \N \N 0 +80012 2025-11-27 09:04:52.036529+00 2025-11-27 09:04:52.036539+00 \N 首版 2025-11-26 16:00:00+00 图片开版 [] \N \N 加急 \N f \N \N \N \N 2025-11-27 00:00:00+00 2025-11-27 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 35 4 \N \N f 2 仓库布 \N image.png 0 +80011 2025-11-27 09:01:25.141528+00 2025-11-27 09:01:25.141536+00 \N 首版 2025-11-26 16:00:00+00 图片开版 [] \N \N 加急 \N f \N \N \N \N 2025-11-27 00:00:00+00 2025-11-27 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 34 4 \N \N f 2 仓库布 \N image.png 0 +80010 2025-11-27 08:09:00.363084+00 2025-11-27 09:00:48.383782+00 80010 首版 2025-11-26 16:00:00+00 图片开版 [] \N \N 加急 \N f \N \N \N \N 2025-11-27 00:00:00+00 2025-11-27 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 33 4 \N \N f 2 仓库布 \N image.png 0 +13 2025-11-19 09:28:40.840111+00 2025-11-27 09:14:45.498603+00 13 首版 2025-11-18 16:00:00+00 样衣开版 [] 123 213 正常 312312 321 f A A A \N 2025-11-19 00:00:00+00 \N 3123 312 312 批布 \N 123.00 \N f \N 20 3 1 1 f 2 \N \N plate_image.png,image.png 0 +80013 2025-11-28 01:32:58.418934+00 2025-11-28 01:32:58.41894+00 \N 首版 2025-11-27 16:00:00+00 图片开版 [{"url": "http://t5510mjho.hn-bkt.clouddn.com/uploads/2025/11/28/669cb9d758ec4d1d812221634c0a8f0c.png", "name": "pasted_image_1764293575811.png", "path": "uploads/2025/11/28/669cb9d758ec4d1d812221634c0a8f0c.png", "size": 9562, "file_id": 3, "uploaded_at": "2025-11-28T01:32:56.717249+00:00", "content_type": "image/png"}] \N \N 加急 \N f \N \N \N \N 2025-11-28 00:00:00+00 2025-11-28 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 36 3 \N \N f 2 仓库布 \N image.png 0 +80014 2025-11-28 01:33:14.214559+00 2025-11-28 01:33:19.972906+00 80014 首版 2025-11-27 16:00:00+00 图片开版 [{"url": "http://t5510mjho.hn-bkt.clouddn.com/uploads/2025/11/28/7ceb226e55ff49f1ae19288bceb30043.png", "name": "pasted_image_1764293584299.png", "path": "uploads/2025/11/28/7ceb226e55ff49f1ae19288bceb30043.png", "size": 27946, "file_id": 4, "uploaded_at": "2025-11-28T01:33:05.201488+00:00", "content_type": "image/png"}, {"url": "http://t5510mjho.hn-bkt.clouddn.com/uploads/2025/11/28/d8476a09bc65428186de1fff70acc8cb.png", "name": "pasted_image_1764293586483.png", "path": "uploads/2025/11/28/d8476a09bc65428186de1fff70acc8cb.png", "size": 19360, "file_id": 5, "uploaded_at": "2025-11-28T01:33:07.382898+00:00", "content_type": "image/png"}, {"url": "http://t5510mjho.hn-bkt.clouddn.com/uploads/2025/11/28/114c7bdecf9547dd8d55c19d814fcbe5.png", "name": "pasted_image_1764293589617.png", "path": "uploads/2025/11/28/114c7bdecf9547dd8d55c19d814fcbe5.png", "size": 25338, "file_id": 6, "uploaded_at": "2025-11-28T01:33:10.519571+00:00", "content_type": "image/png"}] \N \N 加急 \N f \N \N \N \N 2025-11-28 00:00:00+00 2025-11-28 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 37 6 \N \N f 2 仓库布 \N image.png,image.png,image.png 0 +80015 2025-11-28 01:43:59.357314+00 2025-11-28 01:54:32.381904+00 80015 首版 2025-11-27 16:00:00+00 图片开版 [] 123123 1231 加急 中大 \N f 画图难度1 调色难度1 套样难度1 难度1 2025-11-28 00:00:00+00 2025-11-28 16:00:00+00 四面弹 1.5米 \N 匹布 \N 123.00 待审批 f 123 38 4 1 2 f 2 仓库布 \N 测试 0 +80016 2025-11-28 02:39:41.388679+00 2025-11-28 02:39:41.388688+00 \N 首版 2025-11-27 16:00:00+00 图片开版 [] \N \N 加急 \N f \N \N \N \N 2025-11-28 00:00:00+00 2025-11-28 16:00:00+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 39 4 \N \N f 2 仓库布 \N \N 0 +80017 2025-11-28 02:57:18.733054+00 2025-11-28 02:57:18.733063+00 \N 首版 2025-11-28 02:53:36+00 图片开版 [{"url": "http://t5510mjho.hn-bkt.clouddn.com/uploads/2025/11/28/ebb3a627a5ce4eaaabc6fc4d9cd15bc7.png", "name": "pasted_image_1764298419736.png", "path": "uploads/2025/11/28/ebb3a627a5ce4eaaabc6fc4d9cd15bc7.png", "size": 55897, "file_id": 9, "uploaded_at": "2025-11-28T02:53:41.308123+00:00", "content_type": "image/png"}] \N \N 加急 \N f \N \N \N \N 2025-11-28 00:00:00+00 2025-11-29 02:53:36+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 40 4 \N \N f 2 仓库布 \N image.png 0 +80018 2025-11-28 02:58:57.700327+00 2025-11-28 02:58:57.700336+00 \N 首版 2025-11-28 02:58:40+00 图片开版 [{"url": "http://t5510mjho.hn-bkt.clouddn.com/uploads/2025/11/28/afaea93815684e2c8c3ea17124a702ed.png", "name": "pasted_image_1764298729933.png", "path": "uploads/2025/11/28/afaea93815684e2c8c3ea17124a702ed.png", "size": 35906, "file_id": 10, "uploaded_at": "2025-11-28T02:58:51.677179+00:00", "content_type": "image/png"}] \N \N 加急 \N f \N \N \N \N 2025-11-28 02:58:40+00 2025-11-29 02:58:40+00 四面弹 1.5米 \N 匹布 \N \N \N f \N 41 3 2 1 f 2 仓库布 \N image.png 0 \. @@ -2209,17 +2401,17 @@ COPY public.printing_printingjob (id, created_at, updated_at, quantity, unit, si -- Data for Name: printing_printingorder; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.printing_printingorder (id, created_at, updated_at, fabric, width, is_urgent, area, address, fabric_source, is_fabric_received, craft, description, outgoing_date, curve, new_curve, "position", printing_warn, rolling_warn, production_warn, customer_id, is_invalid, process_id) FROM stdin; -1 2025-11-18 10:03:03.512062+00 2025-11-18 10:03:03.512073+00 123214 123 f 中大 3123 f 4213 \N 2025-11-18 3213 321 123 312 312 321 3 f 1 -2 2025-11-18 10:15:58.06474+00 2025-11-18 10:15:58.064748+00 456 456 f 中大 456 f 456 \N 2025-11-18 645 645 6456 456 456 456 3 f 1 -3 2025-11-18 10:42:09.201812+00 2025-11-18 10:42:09.20182+00 123 3213123 f \N 123 f 23123 \N \N 123 213 123 123 123 213 3 f 1 -5 2025-11-18 10:42:48.781191+00 2025-11-18 16:29:16.270004+00 123 123 f \N \N t \N \N \N \N \N \N \N \N \N 4 f 1 -4 2025-11-18 10:42:09.764532+00 2025-11-18 16:29:38.034391+00 123 3213123 f \N 123 t 23123 \N \N 123 213 123 123 123 213 3 f 1 -6 2025-11-19 01:59:30.596405+00 2025-11-19 01:59:30.596416+00 123 23 f 中大 \N f \N \N 2025-11-19 \N \N \N \N \N \N 3 f 1 -7 2025-11-19 05:10:28.027246+00 2025-11-19 05:10:28.027259+00 四面弹 1.5米 f 中大 仓库布 f 批布 \N 2025-11-20 \N \N \N \N \N \N 3 f 1 -8 2025-11-19 06:16:00.11043+00 2025-11-19 06:16:00.110438+00 123 23 f 中大 \N f \N \N 2025-11-19 \N \N \N \N \N \N 3 f 1 -9 2025-11-19 08:21:50.42566+00 2025-11-19 08:21:50.425668+00 123 23 f 中大 \N f \N \N 2025-11-19 \N \N \N \N \N \N 3 f 1 -10 2025-11-21 03:10:14.707335+00 2025-11-21 03:10:14.707343+00 四面弹 1.5米 f 中大 仓库布 f 批布 \N 2025-11-22 测试 3124123 \N \N \N \N 3 f 1 +COPY public.printing_printingorder (id, created_at, updated_at, fabric, width, is_urgent, area, address, fabric_source, is_fabric_received, craft, description, outgoing_date, curve, new_curve, "position", printing_warn, rolling_warn, production_warn, customer_id, is_invalid, process_id, print_count) FROM stdin; +1 2025-11-18 10:03:03.512062+00 2025-11-18 10:03:03.512073+00 123214 123 f 中大 3123 f 4213 \N 2025-11-18 3213 321 123 312 312 321 3 f 1 0 +2 2025-11-18 10:15:58.06474+00 2025-11-18 10:15:58.064748+00 456 456 f 中大 456 f 456 \N 2025-11-18 645 645 6456 456 456 456 3 f 1 0 +3 2025-11-18 10:42:09.201812+00 2025-11-18 10:42:09.20182+00 123 3213123 f \N 123 f 23123 \N \N 123 213 123 123 123 213 3 f 1 0 +5 2025-11-18 10:42:48.781191+00 2025-11-18 16:29:16.270004+00 123 123 f \N \N t \N \N \N \N \N \N \N \N \N 4 f 1 0 +4 2025-11-18 10:42:09.764532+00 2025-11-18 16:29:38.034391+00 123 3213123 f \N 123 t 23123 \N \N 123 213 123 123 123 213 3 f 1 0 +6 2025-11-19 01:59:30.596405+00 2025-11-19 01:59:30.596416+00 123 23 f 中大 \N f \N \N 2025-11-19 \N \N \N \N \N \N 3 f 1 0 +7 2025-11-19 05:10:28.027246+00 2025-11-19 05:10:28.027259+00 四面弹 1.5米 f 中大 仓库布 f 批布 \N 2025-11-20 \N \N \N \N \N \N 3 f 1 0 +8 2025-11-19 06:16:00.11043+00 2025-11-19 06:16:00.110438+00 123 23 f 中大 \N f \N \N 2025-11-19 \N \N \N \N \N \N 3 f 1 0 +9 2025-11-19 08:21:50.42566+00 2025-11-19 08:21:50.425668+00 123 23 f 中大 \N f \N \N 2025-11-19 \N \N \N \N \N \N 3 f 1 0 +10 2025-11-21 03:10:14.707335+00 2025-11-28 05:21:10.802605+00 四面弹 1.5米 f 中大 仓库布 t 批布 \N 2025-11-22 测试 3124123 \N \N \N \N 3 f 1 0 \. @@ -2525,7 +2717,7 @@ COPY public.stateflow_stateparameter (id, created_at, updated_at, key, value, de COPY public.stock_inventory (created_at, updated_at, id, quantity, num_of_rolls, spec, description, merchant_id, product_id, warehouse_id) FROM stdin; 2025-11-20 08:03:02.167396+00 2025-11-20 08:03:02.167403+00 2 25.00 1 \N \N 1 1 3 2025-11-25 10:10:50.074909+00 2025-11-25 10:10:52.768415+00 3 -1602.00 -12 \N \N 1 2 2 -2025-11-20 07:54:45.864074+00 2025-11-25 10:13:09.762694+00 1 1712.00 10 \N \N 1 1 2 +2025-11-20 07:54:45.864074+00 2025-11-28 04:16:58.098231+00 1 1907.00 12 \N \N 1 1 2 \. @@ -2561,6 +2753,14 @@ COPY public.stock_stockchangedetail (created_at, updated_at, id, quantity, unit, 2025-11-25 09:21:02.981242+00 2025-11-25 09:21:02.981252+00 25 55.00 1 1 1 9 f \N 2025-11-25 09:28:55.964056+00 2025-11-25 09:28:55.964062+00 26 22.00 1 1 1 10 f \N 2025-11-25 10:12:41.752146+00 2025-11-25 10:12:59.642704+00 27 22.00 1 1 1 11 f 26 +2025-11-28 03:49:04.600109+00 2025-11-28 03:49:04.600114+00 30 97.00 1 1 1 14 f \N +2025-11-28 03:49:04.600598+00 2025-11-28 03:49:04.600602+00 31 98.00 1 1 1 14 f \N +2025-11-28 06:18:30.986094+00 2025-11-28 06:18:30.9861+00 32 97.00 1 1 1 15 f \N +2025-11-28 06:18:30.986555+00 2025-11-28 06:18:30.986558+00 33 98.00 1 1 1 15 f \N +2025-11-28 06:18:54.739337+00 2025-11-28 06:18:54.739342+00 34 97.00 1 1 1 16 f \N +2025-11-28 06:18:54.739784+00 2025-11-28 06:18:54.739787+00 35 98.00 1 1 1 16 f \N +2025-11-28 06:18:54.739967+00 2025-11-28 06:18:54.73997+00 36 97.00 1 1 1 16 f \N +2025-11-28 06:18:54.740122+00 2025-11-28 06:18:54.740125+00 37 94.00 1 1 1 16 f \N \. @@ -2579,6 +2779,9 @@ COPY public.stock_stockchangerecord (created_at, updated_at, id, type, source_ty 2025-11-25 08:49:45.154243+00 2025-11-25 10:10:50.088642+00 7 2 6 \N t 2025-11-25 10:10:50.087782+00 \N 2 1 2 2025-11-25 08:49:42.446213+00 2025-11-25 10:10:52.770383+00 6 2 6 \N t 2025-11-25 10:10:52.769618+00 \N 2 1 2 2025-11-25 10:12:01.37053+00 2025-11-25 10:13:09.76617+00 11 2 9 \N t 2025-11-25 10:13:09.765158+00 1 1 2 +2025-11-28 03:49:04.598896+00 2025-11-28 04:16:58.100963+00 14 1 1 9 t 2025-11-28 04:16:58.099857+00 \N 2 1 2 +2025-11-28 06:18:30.984995+00 2025-11-28 06:18:30.985004+00 15 1 1 10 f \N \N 2 1 3 +2025-11-28 06:18:54.73818+00 2025-11-28 06:18:54.73819+00 16 1 1 11 f \N \N 2 1 4 \. @@ -2622,6 +2825,8 @@ COPY public.stock_stocksnapshot (created_at, updated_at, id, delta, quantity_bef 2025-11-25 10:10:52.76742+00 2025-11-25 10:10:52.767424+00 25 -78.00 -1437.00 -1515.00 1 -12 \N \N f \N \N 1 2 6 2 2025-11-25 10:10:52.769042+00 2025-11-25 10:10:52.769047+00 26 -87.00 -1515.00 -1602.00 1 -13 \N \N f \N \N 1 2 6 2 2025-11-25 10:13:09.763497+00 2025-11-25 10:13:09.763502+00 27 -22.00 1734.00 1712.00 1 9 \N \N f \N \N 1 1 11 2 +2025-11-28 04:16:58.096138+00 2025-11-28 04:16:58.096144+00 28 97.00 1712.00 1809.00 1 12 \N \N f \N \N 1 1 14 2 +2025-11-28 04:16:58.099209+00 2025-11-28 04:16:58.099214+00 29 98.00 1809.00 1907.00 1 13 \N \N f \N \N 1 1 14 2 \. @@ -2629,7 +2834,7 @@ COPY public.stock_stocksnapshot (created_at, updated_at, id, delta, quantity_bef -- Name: api_uploaded_file_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.api_uploaded_file_id_seq', 1, true); +SELECT pg_catalog.setval('public.api_uploaded_file_id_seq', 10, true); -- @@ -2650,7 +2855,7 @@ SELECT pg_catalog.setval('public.auth_group_permissions_id_seq', 1, false); -- Name: auth_permission_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.auth_permission_id_seq', 157, true); +SELECT pg_catalog.setval('public.auth_permission_id_seq', 165, true); -- @@ -2723,6 +2928,13 @@ SELECT pg_catalog.setval('public.basic_info_employeetype_id_seq', 7, true); SELECT pg_catalog.setval('public.basic_info_merchant_id_seq', 3, true); +-- +-- Name: basic_info_merchantsetting_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('public.basic_info_merchantsetting_id_seq', 1, true); + + -- -- Name: basic_info_product_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- @@ -2779,32 +2991,39 @@ SELECT pg_catalog.setval('public.basic_info_vehicletype_id_seq', 1, false); SELECT pg_catalog.setval('public.basic_info_warehouse_id_seq', 4, true); +-- +-- Name: business_purchaseorderitem_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('public.business_purchaseorderitem_id_seq', 8, true); + + -- -- Name: django_admin_log_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_admin_log_id_seq', 99, true); +SELECT pg_catalog.setval('public.django_admin_log_id_seq', 116, true); -- -- Name: django_content_type_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_content_type_id_seq', 38, true); +SELECT pg_catalog.setval('public.django_content_type_id_seq', 40, true); -- -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_migrations_id_seq', 76, true); +SELECT pg_catalog.setval('public.django_migrations_id_seq', 91, true); -- -- Name: printing_plateorder_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.printing_plateorder_id_seq', 80007, true); +SELECT pg_catalog.setval('public.printing_plateorder_id_seq', 80018, true); -- @@ -2832,7 +3051,7 @@ SELECT pg_catalog.setval('public.state_log_parameter_record_id_seq', 9, true); -- Name: stateflow_order_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.stateflow_order_id_seq', 30, true); +SELECT pg_catalog.setval('public.stateflow_order_id_seq', 41, true); -- @@ -2888,21 +3107,21 @@ SELECT pg_catalog.setval('public.stock_inventory_id_seq', 3, true); -- Name: stock_purchaseorder_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.stock_purchaseorder_id_seq', 1, false); +SELECT pg_catalog.setval('public.stock_purchaseorder_id_seq', 12, true); -- -- Name: stock_stockchangedetail_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.stock_stockchangedetail_id_seq', 27, true); +SELECT pg_catalog.setval('public.stock_stockchangedetail_id_seq', 37, true); -- -- Name: stock_stockchangerecord_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.stock_stockchangerecord_id_seq', 11, true); +SELECT pg_catalog.setval('public.stock_stockchangerecord_id_seq', 16, true); -- @@ -2916,7 +3135,7 @@ SELECT pg_catalog.setval('public.stock_stockfreeze_id_seq', 1, false); -- Name: stock_stocksnapshot_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.stock_stocksnapshot_id_seq', 27, true); +SELECT pg_catalog.setval('public.stock_stocksnapshot_id_seq', 29, true); -- @@ -3103,6 +3322,14 @@ ALTER TABLE ONLY public.basic_info_merchant ADD CONSTRAINT basic_info_merchant_pkey PRIMARY KEY (id); +-- +-- Name: basic_info_merchantsetting basic_info_merchantsetting_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.basic_info_merchantsetting + ADD CONSTRAINT basic_info_merchantsetting_pkey PRIMARY KEY (id); + + -- -- Name: basic_info_product basic_info_product_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -3183,6 +3410,14 @@ ALTER TABLE ONLY public.basic_info_warehouse ADD CONSTRAINT basic_info_warehouse_pkey PRIMARY KEY (id); +-- +-- Name: business_purchaseorderitem business_purchaseorderitem_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.business_purchaseorderitem + ADD CONSTRAINT business_purchaseorderitem_pkey PRIMARY KEY (id); + + -- -- Name: django_admin_log django_admin_log_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -3548,6 +3783,13 @@ CREATE INDEX basic_info_employee_position_id_0f790d25 ON public.basic_info_emplo CREATE INDEX basic_info_employeetype_merchant_id_cf26fc93 ON public.basic_info_employeetype USING btree (merchant_id); +-- +-- Name: basic_info_merchantsetting_merchant_id_4ec5ec48; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX basic_info_merchantsetting_merchant_id_4ec5ec48 ON public.basic_info_merchantsetting USING btree (merchant_id); + + -- -- Name: basic_info_product_category_id_2bbbe6be; Type: INDEX; Schema: public; Owner: postgres -- @@ -3618,6 +3860,34 @@ CREATE INDEX basic_info_warehouse_merchant_id_d5bb4b76 ON public.basic_info_ware CREATE INDEX business_object_content_type_id_94dccf29 ON public.business_object USING btree (content_type_id); +-- +-- Name: business_purchaseorder_operator_id_759fe141; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX business_purchaseorder_operator_id_759fe141 ON public.business_purchaseorder USING btree (operator_id); + + +-- +-- Name: business_purchaseorder_warehouse_id_7aad4bbe; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX business_purchaseorder_warehouse_id_7aad4bbe ON public.business_purchaseorder USING btree (warehouse_id); + + +-- +-- Name: business_purchaseorderitem_product_id_1cd637ce; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX business_purchaseorderitem_product_id_1cd637ce ON public.business_purchaseorderitem USING btree (product_id); + + +-- +-- Name: business_purchaseorderitem_purchase_order_id_329d998b; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX business_purchaseorderitem_purchase_order_id_329d998b ON public.business_purchaseorderitem USING btree (purchase_order_id); + + -- -- Name: django_admin_log_content_type_id_c4bce8eb; Type: INDEX; Schema: public; Owner: postgres -- @@ -4077,6 +4347,14 @@ ALTER TABLE ONLY public.basic_info_employeetype ADD CONSTRAINT basic_info_employeet_merchant_id_cf26fc93_fk_basic_inf FOREIGN KEY (merchant_id) REFERENCES public.basic_info_merchant(id) DEFERRABLE INITIALLY DEFERRED; +-- +-- Name: basic_info_merchantsetting basic_info_merchants_merchant_id_4ec5ec48_fk_basic_inf; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.basic_info_merchantsetting + ADD CONSTRAINT basic_info_merchants_merchant_id_4ec5ec48_fk_basic_inf FOREIGN KEY (merchant_id) REFERENCES public.basic_info_merchant(id) DEFERRABLE INITIALLY DEFERRED; + + -- -- Name: basic_info_product basic_info_product_category_id_2bbbe6be_fk_basic_inf; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- @@ -4165,6 +4443,38 @@ ALTER TABLE ONLY public.business_object ADD CONSTRAINT business_object_content_type_id_94dccf29_fk_django_co FOREIGN KEY (content_type_id) REFERENCES public.django_content_type(id) DEFERRABLE INITIALLY DEFERRED; +-- +-- Name: business_purchaseorder business_purchaseord_operator_id_759fe141_fk_basic_inf; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.business_purchaseorder + ADD CONSTRAINT business_purchaseord_operator_id_759fe141_fk_basic_inf FOREIGN KEY (operator_id) REFERENCES public.basic_info_employee(id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: business_purchaseorderitem business_purchaseord_product_id_1cd637ce_fk_basic_inf; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.business_purchaseorderitem + ADD CONSTRAINT business_purchaseord_product_id_1cd637ce_fk_basic_inf FOREIGN KEY (product_id) REFERENCES public.basic_info_product(id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: business_purchaseorderitem business_purchaseord_purchase_order_id_329d998b_fk_business_; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.business_purchaseorderitem + ADD CONSTRAINT business_purchaseord_purchase_order_id_329d998b_fk_business_ FOREIGN KEY (purchase_order_id) REFERENCES public.business_purchaseorder(id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: business_purchaseorder business_purchaseord_warehouse_id_7aad4bbe_fk_basic_inf; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.business_purchaseorder + ADD CONSTRAINT business_purchaseord_warehouse_id_7aad4bbe_fk_basic_inf FOREIGN KEY (warehouse_id) REFERENCES public.basic_info_warehouse(id) DEFERRABLE INITIALLY DEFERRED; + + -- -- Name: django_admin_log django_admin_log_content_type_id_c4bce8eb_fk_django_co; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- @@ -4521,5 +4831,5 @@ ALTER TABLE ONLY public.stock_stocksnapshot -- PostgreSQL database dump complete -- -\unrestrict lAaLqLBC97fkSperdxYl5fjzjf6qhnkvpVyPWEkZyQ37Mu4fIXbcWZBIFk58Sg7 +\unrestrict 7vxIoMQ6fViE6Ury13lyTyPcnDejMchx1YmoWE9b14f1V4IdfU6SzZ7uNzJes6K diff --git a/docs/FIX_FILE_UPLOAD_401.md b/docs/FIX_FILE_UPLOAD_401.md index 96d0ea7..51384e1 100644 --- a/docs/FIX_FILE_UPLOAD_401.md +++ b/docs/FIX_FILE_UPLOAD_401.md @@ -1,5 +1,7 @@ # PlateOrder 文件上传时返回 401 错误修复报告 +> **更新(2025-11)**:`PlateOrder.plate_image` 现已改为 JSON 字段,仅接受通过 `/api/v1/upload/` 上传后的 `file_id` 引用,不再直接处理 multipart/form-data。以下内容保留旧问题的排查过程以供参考。 + ## 📋 问题描述 **症状:** diff --git a/docs/PlateOrder_API.md b/docs/PlateOrder_API.md index 9e42ba9..b2ba253 100644 --- a/docs/PlateOrder_API.md +++ b/docs/PlateOrder_API.md @@ -38,8 +38,8 @@ GET /api/v1/plate-orders/ | style_name | string | 否 | 款式名称(模糊查询) | | plate_date_from | date | 否 | 开版日期起始(YYYY-MM-DD) | | plate_date_to | date | 否 | 开版日期结束(YYYY-MM-DD) | -| required_completion_date_from | date | 否 | 要求完成日期起始 | -| required_completion_date_to | date | 否 | 要求完成日期结束 | +| required_completion_date_from | datetime | 否 | 要求完成时间起始(ISO8601,例:2025-11-20T08:00:00Z) | +| required_completion_date_to | datetime | 否 | 要求完成时间结束(ISO8601) | | created_date_from | date | 否 | 创建日期起始 | | created_date_to | date | 否 | 创建日期结束 | | search | string | 否 | 全文搜索(搜索设计编号、款式名称、客户名称、面料) | @@ -75,7 +75,7 @@ GET /api/v1/plate-orders/ "style_name": "花朵印染", "fabric": "棉布", "width": "150cm", - "required_completion_date": "2025-11-20", + "required_completion_date": "2025-11-20T18:00:00Z", "completion_date": null, "is_ordered": false, "process": 1, @@ -112,8 +112,24 @@ GET /api/v1/plate-orders/{id}/ "plate_type": "圆网", "plate_date": "2025-11-10T10:00:00Z", "plate_method": "手工", - "plate_image": "/media/plate_images/design001.jpg", - "plate_image_url": "http://example.com/media/plate_images/design001.jpg", + "plate_image": [ + { + "file_id": 12, + "name": "主图", + "url": "https://example.com/media/uploads/2025/11/plate-a.jpg", + "path": "uploads/2025/11/plate-a.jpg" + }, + { + "file_id": 13, + "name": "细节图", + "url": "https://example.com/media/uploads/2025/11/plate-b.jpg", + "path": "uploads/2025/11/plate-b.jpg" + } + ], + "plate_image_url": [ + "https://example.com/media/uploads/2025/11/plate-a.jpg", + "https://example.com/media/uploads/2025/11/plate-b.jpg" + ], "plate_notes": "注意色彩还原", "reprint_reason": null, "urgency_level": "加急", @@ -140,7 +156,7 @@ GET /api/v1/plate-orders/{id}/ "difficulty_rating": "中等", "sample_meter": "5米", "required_sample_meters": 10.00, - "required_completion_date": "2025-11-20", + "required_completion_date": "2025-11-20T18:00:00Z", "completion_date": null, "approval_result": "通过", "is_ordered": false, @@ -181,7 +197,11 @@ Content-Type: application/json "salesperson": 3, "merchandiser": 4, "designer": 8, - "required_completion_date": "2025-11-25", + "plate_image": [ + {"file_id": 21, "name": "主图"}, + {"file_id": 22, "name": "细节图"} + ], + "required_completion_date": "2025-11-25T06:30:00Z", "is_mark_frame": false, "plate_method": "机器", "production_method": "小批量", @@ -197,7 +217,7 @@ Content-Type: application/json - `plate_type`: 版型 - `plate_date`: 开版日期 - `plate_method`: 开版方式 -- `plate_image`: 开版图片(文件上传) +- `plate_image`: 开版图片引用列表(数组,元素包含 `file_id` 与可选 `name`,需要先通过 `/api/v1/upload/` 上传文件) - `plate_notes`: 打版注意事项 - `reprint_reason`: 复版原因 - `urgency_level`: 紧急程度 @@ -218,7 +238,7 @@ Content-Type: application/json - `difficulty_rating`: 难度评级 - `sample_meter`: 米样 - `required_sample_meters`: 所需样品米数 -- `required_completion_date`: 要求完成时间 +- `required_completion_date`: 要求完成时间(日期+具体时间,UTC 推荐) - `completion_date`: 完成时间 - `approval_result`: 审批结果 - `is_ordered`: 是否已下单 @@ -576,7 +596,7 @@ GET /api/v1/plate-orders/{id}/timeline/ | plate_type | string | 版型(首版/复版等) | | plate_date | datetime | 开版时间 | | plate_method | string | 开版方式 | -| plate_image | file | 开版图片 | +| plate_image | array | 开版图片引用列表(通过 `UploadedFile` ID 关联) | | plate_notes | text | 打版注意事项 | | reprint_reason | text | 复版原因 | | urgency_level | string | 紧急程度 | @@ -599,7 +619,7 @@ GET /api/v1/plate-orders/{id}/timeline/ | difficulty_rating | string | 难度评级 | | sample_meter | string | 米样 | | required_sample_meters | decimal | 所需样品米数 | -| required_completion_date | date | 要求完成时间 | +| required_completion_date | datetime | 要求完成时间(ISO8601 字符串) | | completion_date | datetime | 完成时间 | | approval_result | string | 审批结果 | | is_ordered | boolean | 是否已下单 | @@ -647,7 +667,7 @@ curl -X POST "http://api.example.com/api/v1/plate-orders/" \ "style_name": "新款印染", "fabric": "纯棉", "urgency_level": "加急", - "required_completion_date": "2025-11-20", + "required_completion_date": "2025-11-20T18:00:00Z", "process": 1 }' ``` @@ -709,7 +729,7 @@ curl -X GET "http://api.example.com/api/v1/plate-orders/1/timeline/" \ 2. **权限控制**: 作废和恢复操作需要特定权限 3. **日期格式**: 所有日期字段使用 ISO 8601 格式(YYYY-MM-DD 或 YYYY-MM-DDTHH:MM:SSZ) 4. **分页**: 列表接口默认支持分页,使用 `limit` 和 `offset` 参数控制 -5. **文件上传**: `plate_image` 字段需要使用 multipart/form-data 格式上传 +5. **文件引用**: 需先调用 `/api/v1/upload/` 上传文件并获取 `file_id`,再通过 `plate_image` 数组提交引用(无需 multipart) 6. **自动创建流程实例**: 创建开版订单时,如果提供了 `process` 字段,系统会自动创建对应的 BusinessObject 流程实例。如果不提供则使用默认流程 7. **流程验证**: 提供的 `process` ID 必须在系统中存在,否则会返回验证错误 8. **流程操作**: diff --git a/docs/business_purchase.md b/docs/business_purchase.md index 27f0131..f15f353 100644 --- a/docs/business_purchase.md +++ b/docs/business_purchase.md @@ -11,18 +11,17 @@ ```json { "supplier": 1, - "warehouse": 2, + "warehouse_id": 2, "order_date": "2025-11-26", - "total_amount": "1200.50", "remarks": "测试采购单", "items": [ { "product_id": 10, - "quantities": ["10.5", "6.0"] - }, - { - "product_id": 18, - "quantities": ["3.25"] + "quantity": 120, + "num_of_rolls": 3, + "price": "12.50", + "unit": "米", + "empty_diff_percent": "0" } ] } @@ -31,15 +30,23 @@ | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | supplier | integer | ✅ | 供应商 ID,必须隶属于当前商户 | -| warehouse | integer | ✅ | 入库仓库 ID | +| warehouse / warehouse_id | integer | ✅ | 入库仓库 ID(两字段二选一,推荐 `warehouse_id`) | | order_date | string (date) | ✅ | 订单日期(`YYYY-MM-DD`) | -| total_amount | string/number | ✅ | 采购总金额 | | remarks | string | 否 | 备注 | -| items | array | ✅ | 入库明细,结构需满足 `StockFlowService` 的要求(目前仅支持严谨模式:product_id + quantities) | +| items | array | ✅ | 入库明细,根据仓库模式提供不同字段 | -> `items` 内部字段会被直接传给 `StockFlowService`,因此: -> - 严谨/严进模式:`{'product_id': 1, 'quantities': ['10', '5']}` -> - 宽松模式/严进严出出库暂未开放,后续按需扩展。 +#### items 结构说明 + +- **宽进仓(UNRESTRICTED)** + - 必填:`product_id`、`quantity`、`num_of_rolls` + - `quantity_of_rolls` 会自动置空,并以 `{value, num_of_rolls}` 的形式传递给 `StockFlowService.stock_in` 的宽松模式。 + +- **严进仓(RESTRICT_IN / RESTRICT_IN_OUT)** + - 必填:`product_id`、`numbers`(数组) + - 后端会以数组长度设置 `num_of_rolls`,把所有数值拼为 `quantity_of_rolls="10,5,8"`,同时求和得到 `quantity`,并生成 `{'quantities': ['10','5','8']}` 传递给严谨模式。 + +公共可选字段:`price`、`unit`、`color`、`empty_diff_percent`、`batch_number`、`remarks`。缺省时默认使用 0 或产品单位。 +⚠️ 当仓库模式与 items 字段不匹配(例如严进仓缺少 `numbers`、宽进仓提供 `numbers`)时将返回 `400`,提示“仓库为 ×× 模式,items[n] 需要提供 …”。 ### 响应 @@ -56,21 +63,20 @@ | 状态码 | 示例 | 说明 | |--------|------|------| -| 400 | `{"error": "缺少供应商 ID"}` | 请求缺失关键字段 | -| 400 | `{"error": "供应商 99 不存在"}` | 提供的供应商不属于当前商户 | +| 400 | `{"error": "缺少仓库 ID"}` | 请求缺失关键字段 | +| 400 | `{"error": "仓库为严进模式,items[0] 需要提供 numbers 数组"}` | 参数与仓库模式不匹配 | +| 400 | `{"error": "供应商 99 不存在"}` | 供应商不属于当前商户 | | 403 | `{"error": "无权限访问"}` | 当前用户无员工信息 | ### 关联任务(business/tasks.py) -`create_purchase_order_stock_entries` 任务会接收 `purchase_order_id`、`warehouse_id`、`items` 等信息,并通过 `StockFlowService.stock_in` 创建入库记录。 +`create_purchase_order_stock_entries` 任务会接收 `purchase_order_id`、`warehouse_id`、已转换好的 `items` 信息,并通过 `StockFlowService.stock_in` 创建入库记录。 - 任务日志示例:`采购单 35 入库任务完成` - 返回 payload 包含 `stock_change_record_id`、`created_details_count` ### 测试 -`api_v1/tests.py` 中新增 `PurchaseOrderAPITestCase`,通过 eager Celery 设置验证: - -1. API 请求返回 201 -2. Celery 任务被成功调用(patch `create_purchase_order_stock_entries.delay` 断言参数) +`api_v1/tests.py` 中的 `PurchaseOrderAPITestCase` 覆盖宽进/严进模式、模式不匹配和未登录场景; +`business/tests.py` 中的 `PurchaseOrderServiceTestCase`、`PurchaseOrderStockTaskTestCase` 验证 service 层逻辑与 Celery 任务(通过 mock `StockFlowService`)。 diff --git a/docs/print_count_delta.md b/docs/print_count_delta.md new file mode 100644 index 0000000..e566ce1 --- /dev/null +++ b/docs/print_count_delta.md @@ -0,0 +1,54 @@ +# 打印次数增量 API + +该接口用于为支持 `print_count` 字段的业务对象递增打印次数。目前支持: + +- `printing_order` → `printing.models.PrintingOrder` +- `plate_order` → `printing.models.PlateOrder` + +未来可以在后端枚举中扩展更多对象类型(例如采购单等)。 + +## Endpoint + +- **Method**: `POST` +- **Path**: `/api/v1/print-count/delta/` +- **Auth**: 需要登录(仅校验是否认证) +- **Content-Type**: `application/json` + +## 请求参数 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `object_type` | string | 是 | 对象类型,`printing_order` 或 `plate_order` | +| `object_id` | integer | 是 | 目标对象的 ID | +| `delta` | integer/string | 否 | 增量值,默认 1;若传入小于 1 或无法转换为整数,则自动回退为 1 | + +## 响应 + +```json +{ + "object_type": "printing_order", + "object_id": 12, + "delta": 3, + "print_count": 5 +} +``` + +字段说明: + +- `delta`: 实际生效的增量(若请求非法会改为 1) +- `print_count`: 更新后的打印次数 + +## 错误响应 + +| HTTP 状态码 | 场景 | 响应 | +|-------------|------|------| +| 400 | `object_type` 不受支持 | `{"detail": "不支持的对象类型"}` | +| 404 | `object_id` 与 `object_type` 匹配的对象不存在 | `{"detail": "指定对象不存在"}` | +| 401 | 未认证访问 | DRF 默认未认证响应 | + +## 扩展说明 + +- `delta` 仅做加法,不提供回退逻辑。 +- 所有增量操作在数据库事务中执行,可避免并发更新冲突。 +- 新的对象类型只需在后端的 `PrintCountObjectType` 枚举和映射表中补充即可。 + diff --git a/docs/开版管理字段.md b/docs/开版管理字段.md index dbab0cc..c7bbe3a 100644 --- a/docs/开版管理字段.md +++ b/docs/开版管理字段.md @@ -16,7 +16,7 @@ | 调色评级 | ✅ | ❌ | **需添加** `color_matching_rating` | CharField(20) | 调色质量评级 | | 套样评级 | ✅ | ❌ | **需添加** `sample_rating` | CharField(20) | 套样质量评级 | | 难度评级 | ✅ | ❌ | **需添加** `difficulty_rating` | CharField(20) | 难度评级 | -| 要求完成时间 | ✅ | ❌ | **需添加** `required_completion_date` | DateField | 要求完成日期 | +| 要求完成时间 | ✅ | ❌ | **需添加** `required_completion_date` | DateTimeField | 要求完成日期+时间 | | 布料 | ✅ | ❌ | **需添加** `fabric` | CharField(100) | 布料信息 | | 幅宽 | ✅ | ❌ | **需添加** `width` | CharField(50) | 幅宽 | | 款号名称 | ✅ | ❌ | **需添加** `style_name` | CharField(100) | 款号名称 | diff --git a/printing/migrations/0018_plateorder_plate_image_to_jsonfield.py b/printing/migrations/0018_plateorder_plate_image_to_jsonfield.py new file mode 100644 index 0000000..67dd5aa --- /dev/null +++ b/printing/migrations/0018_plateorder_plate_image_to_jsonfield.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-27 10:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('printing', '0017_alter_plateorder_image_name'), + ] + + operations = [ + migrations.AlterField( + model_name='plateorder', + name='plate_image', + field=models.JSONField(blank=True, default=dict, help_text='存储多个开版图片的路径,格式: {"image_1": "/path/to/image1.jpg", "image_2": "/path/to/image2.jpg"}', verbose_name='开版图集合'), + ), + ] diff --git a/printing/migrations/0019_alter_plateorder_plate_image.py b/printing/migrations/0019_alter_plateorder_plate_image.py new file mode 100644 index 0000000..c4cc9ca --- /dev/null +++ b/printing/migrations/0019_alter_plateorder_plate_image.py @@ -0,0 +1,69 @@ +from django.db import migrations, models + + +def _build_entry(value, name_hint=''): + if not value: + return None + if isinstance(value, dict): + entry = {} + entry.update(value) + # Ensure keys exist + entry.setdefault('name', name_hint or entry.get('name') or '') + if 'url' not in entry and 'path' in entry: + entry['url'] = entry['path'] + elif 'path' not in entry and 'url' in entry: + entry['path'] = entry['url'] + return entry + if isinstance(value, str): + return { + 'name': name_hint or value.split('/')[-1], + 'path': value, + 'url': value, + } + return None + + +def forwards(apps, schema_editor): + PlateOrder = apps.get_model('printing', 'PlateOrder') + for obj in PlateOrder.objects.all(): + raw = obj.plate_image + if not raw: + obj.plate_image = [] + obj.save(update_fields=['plate_image']) + continue + + normalized = [] + if isinstance(raw, list): + for entry in raw: + built = _build_entry(entry) + if built: + normalized.append(built) + elif isinstance(raw, dict): + for key, value in raw.items(): + built = _build_entry(value, name_hint=str(key)) + if built: + normalized.append(built) + else: + built = _build_entry(raw) + if built: + normalized.append(built) + + obj.plate_image = normalized + obj.save(update_fields=['plate_image']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('printing', '0018_plateorder_plate_image_to_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='plateorder', + name='plate_image', + field=models.JSONField(blank=True, default=list, help_text='存储多个开版图片引用信息,例如 [{"file_id": 1, "name": "封面", "path": "/media/xxx"}]', verbose_name='开版图'), + ), + migrations.RunPython(forwards, migrations.RunPython.noop), + ] + diff --git a/printing/migrations/0020_alter_plateorder_required_completion_date.py b/printing/migrations/0020_alter_plateorder_required_completion_date.py new file mode 100644 index 0000000..30aa10d --- /dev/null +++ b/printing/migrations/0020_alter_plateorder_required_completion_date.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-28 02:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('printing', '0019_alter_plateorder_plate_image'), + ] + + operations = [ + migrations.AlterField( + model_name='plateorder', + name='required_completion_date', + field=models.DateTimeField(blank=True, null=True, verbose_name='要求完成时间'), + ), + ] diff --git a/printing/migrations/0021_plateorder_print_count_printingorder_print_count.py b/printing/migrations/0021_plateorder_print_count_printingorder_print_count.py new file mode 100644 index 0000000..7cafac1 --- /dev/null +++ b/printing/migrations/0021_plateorder_print_count_printingorder_print_count.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-11-28 03:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('printing', '0020_alter_plateorder_required_completion_date'), + ] + + operations = [ + migrations.AddField( + model_name='plateorder', + name='print_count', + field=models.PositiveIntegerField(default=0, verbose_name='打印次数'), + ), + migrations.AddField( + model_name='printingorder', + name='print_count', + field=models.PositiveIntegerField(default=0, verbose_name='打印次数'), + ), + ] diff --git a/printing/models.py b/printing/models.py index b976922..f9c4949 100644 --- a/printing/models.py +++ b/printing/models.py @@ -1,5 +1,5 @@ -from django.db import models from django.conf import settings +from django.db import models from flower.common import ModelBase from basic_info import models as basic_models from stateflow import models as stateflow_models @@ -15,7 +15,12 @@ class PlateOrder(ModelBase): plate_type = models.CharField(max_length=20, blank=True, null=True, verbose_name='起版情况') # 首版/复版等 plate_date = models.DateTimeField(null=True, blank=True, verbose_name='下版时间') plate_method = models.CharField(max_length=50, blank=True, null=True, verbose_name='开版方式') - plate_image = models.FileField(upload_to='plate_images/', null=True, blank=True, verbose_name='开版图') + plate_image = models.JSONField( + default=list, + blank=True, + verbose_name='开版图', + help_text='存储多个开版图片引用信息,例如 [{"file_id": 1, "name": "封面", "path": "/media/xxx"}]' + ) image_name = models.TextField(blank=True, null=True, verbose_name='图片名称') plate_notes = models.TextField(blank=True, null=True, verbose_name='打版注意事项') reprint_reason = models.TextField(blank=True, null=True, verbose_name='复版原因') @@ -68,7 +73,7 @@ class PlateOrder(ModelBase): difficulty_rating = models.CharField(max_length=20, blank=True, null=True, verbose_name='难度评级') # 时间相关 - required_completion_date = models.DateField(null=True, blank=True, verbose_name='要求完成时间') + required_completion_date = models.DateTimeField(null=True, blank=True, verbose_name='要求完成时间') completion_date = models.DateTimeField(null=True, blank=True, verbose_name='完成时间') # 产品信息 @@ -95,6 +100,7 @@ class PlateOrder(ModelBase): is_ordered = models.BooleanField(default=False, verbose_name='是否已下单') is_invalid = models.BooleanField(default=False, verbose_name='是否作废') customer_feedback = models.TextField(blank=True, null=True, verbose_name='客户修改意见') + print_count = models.PositiveIntegerField(default=0, verbose_name='打印次数') # 流程管理 process = models.IntegerField(default=settings.PLATE_ORDER_DEFAULT_PROCESS_ID, verbose_name='关联流程') @@ -228,6 +234,7 @@ class PrintingOrder(ModelBase): printing_warn = models.TextField(blank=True, null=True, verbose_name='打印注意事项') rolling_warn = models.TextField(blank=True, null=True, verbose_name='滚筒注意事项') production_warn = models.TextField(blank=True, null=True, verbose_name='生产注意事项') + print_count = models.PositiveIntegerField(default=0, verbose_name='打印次数') is_invalid = models.BooleanField(default=False, verbose_name='是否作废') process = models.ForeignKey( stateflow_models.Process, diff --git a/printing/printing_api.yml b/printing/printing_api.yml index d41fa13..216bc32 100644 --- a/printing/printing_api.yml +++ b/printing/printing_api.yml @@ -414,12 +414,12 @@ paths: description: 开版日期结束。 - in: query name: required_completion_date_from - schema: {type: string, format: date} - description: 要求完成日期起始。 + schema: {type: string, format: date-time} + description: 要求完成时间起始(ISO8601)。 - in: query name: required_completion_date_to - schema: {type: string, format: date} - description: 要求完成日期结束。 + schema: {type: string, format: date-time} + description: 要求完成时间结束(ISO8601)。 - in: query name: created_date_from schema: {type: string, format: date} @@ -713,10 +713,11 @@ components: type: string description: 开版方式。 plate_image: - type: string - format: binary + type: array nullable: true - description: 开版图片(仅 multipart/form-data 时可上传)。 + description: 已上传开版图片引用列表(先调用 /api/v1/upload/ 获取 file_id) + items: + $ref: '#/components/schemas/PlateImageInput' image_name: type: string nullable: true @@ -784,9 +785,9 @@ components: description: 客户要求的米样米数。 required_completion_date: type: string - format: date + format: date-time nullable: true - description: 要求完成日期。 + description: 要求完成时间(含具体时刻)。 completion_date: type: string format: date-time @@ -806,6 +807,49 @@ components: type: integer nullable: true description: 关联流程 ID,缺省时采用 settings.PLATE_ORDER_DEFAULT_PROCESS_ID。 + PlateImageInput: + type: object + required: [file_id] + properties: + file_id: + type: integer + description: 上传文件的 ID(来自 `/api/v1/upload/`) + name: + type: string + nullable: true + description: 可选名称,默认使用文件原始名称 + PlateImageItem: + type: object + properties: + file_id: + type: integer + nullable: true + description: 上传文件 ID,若为外部链接可为空 + name: + type: string + nullable: true + description: 图片名称或备注 + url: + type: string + nullable: true + description: 图片可访问 URL + path: + type: string + nullable: true + description: 图片相对路径 + size: + type: integer + nullable: true + description: 文件大小(字节) + content_type: + type: string + nullable: true + description: 文件内容类型 + uploaded_at: + type: string + format: date-time + nullable: true + description: 上传时间 StateAdvanceRequest: type: object properties: diff --git a/printing/test_plate_order.py b/printing/test_plate_order.py index 58a59e7..93d55ea 100644 --- a/printing/test_plate_order.py +++ b/printing/test_plate_order.py @@ -102,7 +102,7 @@ class PlateOrderModelTestCase(TestCase): """测试带时间信息的开版订单""" now = timezone.now() completion_date = now + timezone.timedelta(days=7) - required_date = (now + timezone.timedelta(days=10)).date() + required_date = now + timezone.timedelta(days=10) plate_order = printing_models.PlateOrder.objects.create( design_code='DES004', diff --git a/stock/admin.py b/stock/admin.py index c0d7390..4593aa0 100644 --- a/stock/admin.py +++ b/stock/admin.py @@ -81,12 +81,18 @@ class StockChangeDetailAdmin(admin.ModelAdmin): 'product', 'quantity', 'direction', + 'consume_with', + 'consumer', 'unit', ) search_fields = ('stock_change_record__id', 'product__name') - list_filter = ('stock_change_record', 'stock_change_record__type') + list_filter = ('stock_change_record', 'stock_change_record__type', 'is_consumed') ordering = ('-created_at',) + @admin.display(description='消耗者') + def consumer(self, obj: models.StockChangeDetail): + return obj.consumed_by_detail + @admin.display(description='出入方向') def direction(self, obj): return '入库' if obj.stock_change_record.is_incoming else '出库'