1
0
forked from erp-dev/erp

feat: added migrate for set id init value into 8000 of plate_order

This commit is contained in:
2025-11-20 13:01:44 +08:00
parent ad0b6a84d2
commit 6091280fdc
12 changed files with 931 additions and 606 deletions

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

View File

@@ -200,7 +200,24 @@ class PrintingJobCreateUpdateSerializer(serializers.ModelSerializer):
return updated_instance
class PlateOrderListSerializer(serializers.ModelSerializer):
class PlateOrderDesignCodeMixin:
"""确保 design_code 为空时使用主键补全"""
@staticmethod
def _normalize_design_code(design_code: str | None, instance_id: int | None) -> str | None:
if design_code:
return design_code
if instance_id:
return str(instance_id)
return design_code
def to_representation(self, instance):
data = super().to_representation(instance)
data['design_code'] = self._normalize_design_code(data.get('design_code'), instance.id)
return data
class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSerializer):
"""开版订单列表序列化器"""
customer_name = serializers.CharField(source="customer.name", read_only=True)
salesperson_name = serializers.CharField(source="salesperson.name", read_only=True)
@@ -217,9 +234,9 @@ class PlateOrderListSerializer(serializers.ModelSerializer):
'customer', 'customer_name', 'area',
'salesperson', 'salesperson_name',
'merchandiser', 'merchandiser_name',
'style_name', 'fabric', 'width',
'style_name', 'fabric', 'fabric_source', 'width',
'required_completion_date', 'completion_date',
'is_ordered', 'process', 'process_name',
'is_ordered', 'process', 'process_name', 'business_object_id',
'status', 'progress_percentage',
'created_at', 'updated_at'
]
@@ -240,7 +257,7 @@ class PlateOrderListSerializer(serializers.ModelSerializer):
return None
class PlateOrderDetailSerializer(serializers.ModelSerializer):
class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSerializer):
"""开版订单详情序列化器"""
customer_name = serializers.CharField(source="customer.name", read_only=True)
customer_phone = serializers.CharField(source="customer.mobile", read_only=True)
@@ -264,7 +281,7 @@ class PlateOrderDetailSerializer(serializers.ModelSerializer):
'customer', 'customer_name', 'customer_phone', 'area', 'default_address',
'salesperson', 'salesperson_name',
'merchandiser', 'merchandiser_name',
'style_name', 'fabric', 'width', 'production_method',
'style_name', 'fabric', 'fabric_source', 'width', 'production_method',
'is_mark_frame', 'drawing_rating', 'color_matching_rating',
'sample_rating', 'difficulty_rating',
'sample_meter', 'required_sample_meters',
@@ -317,7 +334,7 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
"urgency_level", "is_invalid",
"customer", "area", "default_address",
"salesperson", "merchandiser",
"style_name", "fabric", "width", "production_method",
"style_name", "fabric", "fabric_source", "width", "production_method",
"is_mark_frame", "drawing_rating", "color_matching_rating",
"sample_rating", "difficulty_rating",
"sample_meter", "required_sample_meters",

View File

@@ -188,6 +188,49 @@ class PlateOrderAPITestCase(TestCase):
self.assertIn('customer_name', response.data)
self.assertIn('salesperson_name', response.data)
def test_design_code_fallback_in_detail(self):
"""design_code 为空时返回补零后的主键"""
plate_order = printing_models.PlateOrder.objects.create(
customer=self.customer,
design_code='',
plate_type='圆网',
style_name='测试款式',
fabric='棉布',
)
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['design_code'], f'{plate_order.id:06d}')
def test_design_code_fallback_in_list(self):
"""列表接口也应返回补零后的设计编号"""
printing_models.PlateOrder.objects.create(
customer=self.customer,
design_code='',
plate_type='圆网',
style_name='款式1',
fabric='棉布',
)
response = self.client.get('/api/v1/plate-orders/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(results[0]['design_code'], f"{results[0]['id']:06d}")
def test_fabric_source_field_in_detail(self):
"""验证布料来源字段在返回中存在"""
plate_order = printing_models.PlateOrder.objects.create(
customer=self.customer,
design_code='DESIGN010',
plate_type='圆网',
fabric='棉布',
fabric_source='客户提供',
)
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['fabric_source'], '客户提供')
def test_update_plate_order(self):
"""测试更新开版订单"""
plate_order = printing_models.PlateOrder.objects.create(

View File

@@ -201,6 +201,7 @@ Content-Type: application/json
- `merchandiser`: 跟单员ID
- `style_name`: 款式名称
- `fabric`: 面料
- `fabric_source`: 布料来源(可空字符串,如“客户提供”)
- `width`: 幅宽
- `production_method`: 做货方式
- `is_mark_frame`: 是否套唛架
@@ -219,7 +220,7 @@ Content-Type: application/json
**响应**
- 状态码: `201 Created`
- 响应体: 创建的开版订单详情(同详情接口)
- 响应体: 创建的开版订单详情(同详情接口)。若 `design_code` 为空,返回值会自动使用 6 位补零的主键(如 `000123`)。
**错误响应**
```json
@@ -707,6 +708,8 @@ curl -X GET "http://api.example.com/api/v1/plate-orders/1/timeline/" \
- 推进/回退操作需要订单已关联 BusinessObject
- timeline 接口不包含已撤销的流程记录
- completed-states 接口默认不包含已撤销记录,可通过 `include_cancelled=true` 参数包含
9. **设计编号回退**: 所有响应中若 `design_code` 为空,会自动使用主键生成 6 位补零字符串(如 `000123`),确保前端始终有值可显示
10. **布料来源**: `fabric_source` 为可选字段,建议用于区分客户提供、仓库调拨等不同来源信息
---

View File

@@ -0,0 +1,34 @@
## StockChangeSourceEnum 取值与校验
本文件记录了 `stock.models.StockChangeSourceEnum` 的业务含义以及
`StockChangeRecord.clean()` 的校验规则,便于后续扩展时保持一致性。
### 入库来源type = ADD
- `1` 采购
- `2` 销退
- `3` 调入
- `4` 盘盈
- `5` 合并
### 出库来源type = REMOVE
- `6` 销售
- `7` 采购退货
- `8` 调出
- `9` 盘亏
- `10` 拆卷
### 校验逻辑
`StockChangeRecord.clean()` 会根据 `type` 判断允许的 `source_type`
- 入库记录仅接受“入库来源”集合中的取值;
- 出库记录仅接受“出库来源”集合中的取值;
- 违背上述约束时会分别抛出
“入库记录的来源类型无效”或“出库记录的来源类型无效”。
### 扩展指引
1. 新增来源枚举时,请根据业务属性将其划入入库或出库集合;
2. 更新 `StockChangeSourceEnum.incoming_values()` /
`outgoing_values()` 返回的集合;
3. 同步补充 `stock/tests.py`
`StockChangeRecordValidationTestCase` 的覆盖场景;
4. 若 API 文档涉及 `source_type` 取值,请同步修订。

View File

@@ -9,7 +9,7 @@ from . import models
class PlateOrderAdmin(admin.ModelAdmin):
list_display = (
'id',
'design_code',
'design_code_display',
'customer_name',
'style_name',
'plate_type',
@@ -47,7 +47,7 @@ class PlateOrderAdmin(admin.ModelAdmin):
'fields': ['customer', 'area', 'default_address', 'salesperson', 'merchandiser']
}),
('产品信息', {
'fields': ['style_name', 'fabric', 'width', 'production_method']
'fields': ['style_name', 'fabric', 'fabric_source', 'width', 'production_method']
}),
('开版信息', {
'fields': [
@@ -76,6 +76,10 @@ class PlateOrderAdmin(admin.ModelAdmin):
}),
]
@admin.display(description='设计编号')
def design_code_display(self, obj):
return obj.design_code or (str(obj.id) if obj.id else '-')
@admin.display(description='客户')
def customer_name(self, obj):
return obj.customer.name if obj.customer else '-'

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2025-11-20 02:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('printing', '0012_plateorder_process'),
]
operations = [
migrations.AddField(
model_name='plateorder',
name='fabric_source',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='布料来源'),
),
migrations.AlterField(
model_name='plateorder',
name='process',
field=models.IntegerField(default=2, verbose_name='关联流程'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-20 03:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('printing', '0013_plateorder_fabric_source_alter_plateorder_process'),
]
operations = [
migrations.RunSQL(
# 设置序列从 80000 开始(或你想要的任何值)
sql="SELECT setval('printing_plateorder_id_seq', 80000, false);",
reverse_sql="SELECT setval('printing_plateorder_id_seq', 1, false);"
),
]

View File

@@ -64,6 +64,7 @@ class PlateOrder(ModelBase):
# 产品信息
fabric = models.CharField(max_length=100, blank=True, null=True, verbose_name='布料')
fabric_source = models.CharField(max_length=100, blank=True, null=True, verbose_name='布料来源')
width = models.CharField(max_length=50, blank=True, null=True, verbose_name='幅宽')
style_name = models.CharField(max_length=100, blank=True, null=True, verbose_name='款号名称')

File diff suppressed because it is too large Load Diff

View File

@@ -22,11 +22,35 @@ class StockChangeSourceEnum(models.IntegerChoices):
SALES_RETURN = 2, '销退'
TRANSPORT_IN = 3, '调入'
RECHECK_ADD = 4, '盘盈'
COMBINE = 5, '合并'
SALES = 5, '销售'
PURCHASE_RETURN = 6, '采购退货'
TRANSPORT_OUT = 7, '调出'
RECHECK_REMOVE = 8, '盘亏'
SALES = 6, '销售'
PURCHASE_RETURN = 7, '采购退货'
TRANSPORT_OUT = 8, '调出'
RECHECK_REMOVE = 9, '盘亏'
EXPLODE = 10, '拆卷'
@classmethod
def incoming_values(cls) -> set[int]:
"""返回被视为入库的来源类型"""
return {
cls.PURCHASE,
cls.SALES_RETURN,
cls.TRANSPORT_IN,
cls.RECHECK_ADD,
cls.COMBINE,
}
@classmethod
def outgoing_values(cls) -> set[int]:
"""返回被视为出库的来源类型"""
return {
cls.SALES,
cls.PURCHASE_RETURN,
cls.TRANSPORT_OUT,
cls.RECHECK_REMOVE,
cls.EXPLODE,
}
class StockChangeTypeEnum(models.IntegerChoices):
@@ -103,10 +127,10 @@ class StockChangeRecord(ModelBase):
def clean(self):
if self.type == StockChangeTypeEnum.ADD:
if self.source_type > 4:
if self.source_type not in StockChangeSourceEnum.incoming_values():
raise ValidationError('入库记录的来源类型无效')
elif self.type == StockChangeTypeEnum.REMOVE:
if self.source_type < 5:
if self.source_type not in StockChangeSourceEnum.outgoing_values():
raise ValidationError('出库记录的来源类型无效')
def save(self, *args, **kwargs):

View File

@@ -1,3 +1,4 @@
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from decimal import Decimal
@@ -128,6 +129,54 @@ class StockServicesTestCase(TestCase):
)
class StockChangeRecordValidationTestCase(StockServicesTestCase):
"""验证 StockChangeRecord.clean 的来源类型校验"""
def test_add_record_accepts_combine_source(self):
record = models.StockChangeRecord(
merchant=self.merchant,
warehouse=self.warehouse_main,
type=models.StockChangeTypeEnum.ADD,
source_type=models.StockChangeSourceEnum.COMBINE,
)
try:
record.full_clean()
except ValidationError as exc:
self.fail(f'合并应视为入库来源,但触发校验错误: {exc}')
def test_remove_record_accepts_explode_source(self):
record = models.StockChangeRecord(
merchant=self.merchant,
warehouse=self.warehouse_main,
type=models.StockChangeTypeEnum.REMOVE,
source_type=models.StockChangeSourceEnum.EXPLODE,
)
try:
record.full_clean()
except ValidationError as exc:
self.fail(f'拆卷应视为出库来源,但触发校验错误: {exc}')
def test_add_record_rejects_outgoing_source(self):
record = models.StockChangeRecord(
merchant=self.merchant,
warehouse=self.warehouse_main,
type=models.StockChangeTypeEnum.ADD,
source_type=models.StockChangeSourceEnum.SALES,
)
with self.assertRaises(ValidationError):
record.full_clean()
def test_remove_record_rejects_incoming_source(self):
record = models.StockChangeRecord(
merchant=self.merchant,
warehouse=self.warehouse_main,
type=models.StockChangeTypeEnum.REMOVE,
source_type=models.StockChangeSourceEnum.PURCHASE,
)
with self.assertRaises(ValidationError):
record.full_clean()
class FindInventoryTestCase(StockServicesTestCase):
"""测试 find_inventory 函数"""