1
0
forked from erp-dev/erp

feat: added print_count api, added merchant setting model, added manual mode setting for purchase order

This commit is contained in:
2025-11-28 15:01:11 +08:00
parent 53f2a24b20
commit 0c62398120
36 changed files with 1634 additions and 478 deletions

View File

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

11
api_v1/enums.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -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/<int:product_id>/image/', product_image.ProductImageUploadView.as_view(), name='product_image_upload'),

View File

@@ -0,0 +1,57 @@
from django.db import transaction
from django.db.models import F
from rest_framework import permissions, status
from rest_framework.response import Response
from rest_framework.views import APIView
from api_v1.serializers import PrintCountDeltaSerializer
from api_v1.enums import PrintCountObjectType
from printing import models as printing_models
class PrintCountDeltaView(APIView):
"""通用打印次数递增接口"""
permission_classes = [permissions.IsAuthenticated]
OBJECT_MODEL_MAP = {
PrintCountObjectType.PRINTING_ORDER: printing_models.PrintingOrder,
PrintCountObjectType.PLATE_ORDER: printing_models.PlateOrder,
}
def post(self, request, *args, **kwargs):
serializer = PrintCountDeltaSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
model = self.OBJECT_MODEL_MAP.get(data['object_type'])
if model is None:
return Response(
{'detail': '不支持的对象类型'},
status=status.HTTP_400_BAD_REQUEST,
)
with transaction.atomic():
updated = model.objects.filter(id=data['object_id']).update(
print_count=F('print_count') + data['delta']
)
if not updated:
return Response(
{'detail': '指定对象不存在'},
status=status.HTTP_404_NOT_FOUND,
)
fresh_value = model.objects.only('print_count').get(id=data['object_id']).print_count
return Response(
{
'object_type': data['object_type'],
'object_id': data['object_id'],
'delta': data['delta'],
'print_count': fresh_value,
},
status=status.HTTP_200_OK,
)
adjust_print_count = PrintCountDeltaView.as_view()

View File

@@ -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`: 排序字段。

View File

@@ -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
# 创建 PlateOrdersave 方法会自动创建 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)

View File

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

View File

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

View File

@@ -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: 全文搜索(设计编号、款式名称、客户名称、面料)

View File

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

View File

@@ -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')

View File

@@ -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': '商户设置',
},
),
]

View File

@@ -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='设置值(字符串)'),
),
]

View File

@@ -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='设置类型'),
),
]

View File

@@ -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', '自动创建出入库记录任务'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
# PlateOrder 文件上传时返回 401 错误修复报告
> **更新2025-11**`PlateOrder.plate_image` 现已改为 JSON 字段,仅接受通过 `/api/v1/upload/` 上传后的 `file_id` 引用,不再直接处理 multipart/form-data。以下内容保留旧问题的排查过程以供参考。
## 📋 问题描述
**症状:**

View File

@@ -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 | 否 | 要求完成时间起始ISO86012025-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<object> | 开版图片引用列表(通过 `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. **流程操作**:

View File

@@ -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`)。

54
docs/print_count_delta.md Normal file
View File

@@ -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` 枚举和映射表中补充即可。

View File

@@ -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) | 款号名称 |

View File

@@ -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='开版图集合'),
),
]

View File

@@ -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),
]

View File

@@ -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='要求完成时间'),
),
]

View File

@@ -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='打印次数'),
),
]

View File

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

View File

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

View File

@@ -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',

View File

@@ -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 '出库'