forked from erp-dev/erp
feat: added migrate for set id init value into 8000 of plate_order
This commit is contained in:
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"."
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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` 为可选字段,建议用于区分客户提供、仓库调拨等不同来源信息
|
||||
|
||||
---
|
||||
|
||||
|
||||
34
docs/STOCK_CHANGE_SOURCE_TYPES.md
Normal file
34
docs/STOCK_CHANGE_SOURCE_TYPES.md
Normal 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` 取值,请同步修订。
|
||||
@@ -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 '-'
|
||||
|
||||
@@ -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='关联流程'),
|
||||
),
|
||||
]
|
||||
18
printing/migrations/0014_auto_20251120_1144.py
Normal file
18
printing/migrations/0014_auto_20251120_1144.py
Normal 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);"
|
||||
),
|
||||
]
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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 函数"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user