From 6091280fdc1dd92b1e1af895ef22160af2caadac Mon Sep 17 00:00:00 2001 From: colaftc Date: Thu, 20 Nov 2025 13:01:44 +0800 Subject: [PATCH] feat: added migrate for set id init value into 8000 of plate_order --- .vscode/settings.json | 7 + api_v1/views/printing/serializers.py | 29 +- api_v1/views/printing/test_plate_order_api.py | 43 + docs/PlateOrder_API.md | 5 +- docs/STOCK_CHANGE_SOURCE_TYPES.md | 34 + printing/admin.py | 8 +- ..._fabric_source_alter_plateorder_process.py | 23 + .../migrations/0014_auto_20251120_1144.py | 18 + printing/models.py | 1 + printing/printing_api.yml | 1284 +++++++++-------- stock/models.py | 36 +- stock/tests.py | 49 + 12 files changed, 931 insertions(+), 606 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 docs/STOCK_CHANGE_SOURCE_TYPES.md create mode 100644 printing/migrations/0013_plateorder_fabric_source_alter_plateorder_process.py create mode 100644 printing/migrations/0014_auto_20251120_1144.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3e99ede --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "." + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/api_v1/views/printing/serializers.py b/api_v1/views/printing/serializers.py index b1c3c4e..3dcb591 100644 --- a/api_v1/views/printing/serializers.py +++ b/api_v1/views/printing/serializers.py @@ -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", diff --git a/api_v1/views/printing/test_plate_order_api.py b/api_v1/views/printing/test_plate_order_api.py index beb7082..b2f7c21 100644 --- a/api_v1/views/printing/test_plate_order_api.py +++ b/api_v1/views/printing/test_plate_order_api.py @@ -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( diff --git a/docs/PlateOrder_API.md b/docs/PlateOrder_API.md index 9508894..9c2f834 100644 --- a/docs/PlateOrder_API.md +++ b/docs/PlateOrder_API.md @@ -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` 为可选字段,建议用于区分客户提供、仓库调拨等不同来源信息 --- diff --git a/docs/STOCK_CHANGE_SOURCE_TYPES.md b/docs/STOCK_CHANGE_SOURCE_TYPES.md new file mode 100644 index 0000000..60780bf --- /dev/null +++ b/docs/STOCK_CHANGE_SOURCE_TYPES.md @@ -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` 取值,请同步修订。 diff --git a/printing/admin.py b/printing/admin.py index 508118e..7a03b41 100644 --- a/printing/admin.py +++ b/printing/admin.py @@ -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 '-' diff --git a/printing/migrations/0013_plateorder_fabric_source_alter_plateorder_process.py b/printing/migrations/0013_plateorder_fabric_source_alter_plateorder_process.py new file mode 100644 index 0000000..8e9ed8c --- /dev/null +++ b/printing/migrations/0013_plateorder_fabric_source_alter_plateorder_process.py @@ -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='关联流程'), + ), + ] diff --git a/printing/migrations/0014_auto_20251120_1144.py b/printing/migrations/0014_auto_20251120_1144.py new file mode 100644 index 0000000..85435b1 --- /dev/null +++ b/printing/migrations/0014_auto_20251120_1144.py @@ -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);" + ), + ] diff --git a/printing/models.py b/printing/models.py index d986d4f..52386e3 100644 --- a/printing/models.py +++ b/printing/models.py @@ -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='款号名称') diff --git a/printing/printing_api.yml b/printing/printing_api.yml index 2c0fd83..e8c0006 100644 --- a/printing/printing_api.yml +++ b/printing/printing_api.yml @@ -1,373 +1,94 @@ -openapi: 3.0.3 +openapi: 3.0.0 info: - title: Printing API - description: | - 印染模块接口文档,包含 PrintingOrder(印染订单)和 PrintingJob(印染款式明细)的 CRUD 和自定义动作。 - 所有接口仅允许印染工厂用户访问(IsPrintingFactory)。 - version: '1.0.0' + title: Flower Printing API + version: 2.0.0 servers: - url: /api/v1 - description: 本地开发 API 前缀 -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - schemas: - PrintingOrder: - type: object - properties: - id: - type: integer - description: 订单 ID - human_id: - type: string - description: 由系统生成的人类可读编号(格式: YYYYMMDD000001) - example: "20251112000001" - customer: - type: integer - description: 客户 ID - customer_name: - type: string - description: 客户名称 - customer_phone: - type: string - description: 客户电话 - customer_address: - type: string - description: 客户地址 - fabric: - type: string - description: 面料 - example: "纯棉布料" - width: - type: string - description: 幅宽 - example: "150cm" - is_urgent: - type: boolean - description: 是否紧急 - default: false - area: - type: string - description: 地区 - example: "广州" - address: - type: string - description: 地址 - example: "白云区xxx" - fabric_source: - type: string - description: 布料来源 - example: "客户提供" - is_fabric_received: - type: boolean - description: 布料是否已收 - default: false - craft: - type: string - description: 工艺 - example: "活性印花" - description: - type: string - description: 订单描述 - outgoing_date: - type: string - format: date - description: 出货日期 - example: "2025-11-20" - curve: - type: string - description: 曲线 - new_curve: - type: string - description: 新加曲线 - position: - type: string - description: 位置 - printing_warn: - type: string - description: 打印注意事项 - rolling_warn: - type: string - description: 滚筒注意事项 - production_warn: - type: string - description: 生产注意事项 - is_invalid: - type: boolean - description: 是否作废 - default: false - created_at: - type: string - format: date-time - description: 创建时间 - updated_at: - type: string - format: date-time - description: 更新时间 - required: [customer, fabric, width] - PrintingOrderCreate: - type: object - properties: - customer: - type: integer - description: 客户 ID - fabric: - type: string - description: 面料 - example: "纯棉布料" - width: - type: string - description: 幅宽 - example: "150cm" - is_urgent: - type: boolean - description: 是否紧急 - default: false - area: - type: string - description: 地区 - example: "广州" - address: - type: string - description: 地址 - example: "白云区xxx" - fabric_source: - type: string - description: 布料来源 - example: "客户提供" - is_fabric_received: - type: boolean - description: 布料是否已收 - default: false - craft: - type: string - description: 工艺 - example: "活性印花" - description: - type: string - description: 订单描述 - outgoing_date: - type: string - format: date - description: 出货日期 - example: "2025-11-20" - curve: - type: string - description: 曲线 - new_curve: - type: string - description: 新加曲线 - position: - type: string - description: 位置 - printing_warn: - type: string - description: 打印注意事项 - rolling_warn: - type: string - description: 滚筒注意事项 - production_warn: - type: string - description: 生产注意事项 - is_invalid: - type: boolean - description: 是否作废 - default: false - required: [customer, fabric, width] - PrintingJob: - type: object - properties: - id: - type: integer - description: 款式明细 ID - printing_order: - type: integer - description: 印染订单 ID - printing_order_id: - type: string - description: 印染订单人类可读编号 - example: "20251112000001" - product: - type: integer - description: 产品 ID - product_name: - type: string - description: 产品名称 - example: "测试产品" - product_code: - type: string - description: 产品编号 - example: "TEST001" - quantity: - type: integer - description: 数量(必须 > 0) - example: 100 - minimum: 1 - unit: - type: string - description: 单位 - example: "米" - size: - type: string - description: 一段尺寸 - example: "50*60" - pieces: - type: integer - description: 件数(必须 > 0) - example: 10 - minimum: 1 - description: - type: string - description: 备注 - created_at: - type: string - format: date-time - description: 创建时间 - updated_at: - type: string - format: date-time - description: 更新时间 - required: [printing_order, product, quantity, unit, size, pieces] - PrintingJobCreate: - type: object - properties: - printing_order: - type: integer - description: 印染订单 ID - product: - type: integer - description: 产品 ID - quantity: - type: integer - description: 数量(必须 > 0) - example: 100 - minimum: 1 - unit: - type: string - description: 单位 - example: "米" - size: - type: string - description: 一段尺寸 - example: "50*60" - pieces: - type: integer - description: 件数(必须 > 0) - example: 10 - minimum: 1 - description: - type: string - description: 备注 - required: [printing_order, product, quantity, unit, size, pieces] - parameters: - limit: - name: limit - in: query - schema: - type: integer - description: 返回条目上限(分页) - offset: - name: offset - in: query - schema: - type: integer - description: 分页偏移 - search: - name: search - in: query - schema: - type: string - description: 全文搜索关键字 - ordering: - name: ordering - in: query - schema: - type: string - description: 排序字段,例如 id 或 -id + description: Default API prefix +tags: + - name: PrintingOrder + description: 印染订单接口 + - name: PrintingJob + description: 印染款式/任务接口 + - name: PlateOrder + description: 开版订单接口 paths: /printing-orders/: get: - summary: 获取印染订单列表 - description: 支持过滤(customer, is_urgent, is_fabric_received, is_invalid, area, outgoing_date_from/to, created_date_from/to)、search、ordering 和分页。 + tags: [PrintingOrder] + summary: 列出印染订单 parameters: - - $ref: '#/components/parameters/limit' - - $ref: '#/components/parameters/offset' - - $ref: '#/components/parameters/search' - - $ref: '#/components/parameters/ordering' - - name: customer - in: query - schema: - type: integer - description: 客户 ID - - name: customer_name - in: query - schema: - type: string - description: 客户名称(模糊) - - name: customer_phone - in: query - schema: - type: string - description: 客户电话(模糊) - - name: fabric - in: query - schema: - type: string - description: 面料(模糊) - - name: is_urgent - in: query - schema: - type: boolean - - name: is_fabric_received - in: query - schema: - type: boolean - - name: is_invalid - in: query - schema: - type: boolean - - name: area - in: query - schema: - type: string - - name: outgoing_date_from - in: query - schema: - type: string - format: date - - name: outgoing_date_to - in: query - schema: - type: string - format: date + - in: query + name: customer + schema: {type: integer} + description: 客户 ID。 + - in: query + name: customer_name + schema: {type: string} + description: 客户名称(模糊匹配)。 + - in: query + name: customer_phone + schema: {type: string} + description: 客户电话(模糊匹配)。 + - in: query + name: fabric + schema: {type: string} + description: 面料(模糊匹配)。 + - in: query + name: area + schema: {type: string} + description: 地区(模糊匹配)。 + - in: query + name: is_urgent + schema: {type: boolean} + description: 是否紧急。 + - in: query + name: is_fabric_received + schema: {type: boolean} + description: 布料是否已收。 + - in: query + name: is_invalid + schema: {type: boolean} + description: 是否作废。 + - in: query + name: outgoing_date_from + schema: {type: string, format: date} + description: 出货日期起始。 + - in: query + name: outgoing_date_to + schema: {type: string, format: date} + description: 出货日期结束。 + - in: query + name: created_date_from + schema: {type: string, format: date} + description: 创建日期起始。 + - in: query + name: created_date_to + schema: {type: string, format: date} + description: 创建日期结束。 + - in: query + name: search + schema: {type: string} + description: 全文搜索(客户名称/面料/地区/工艺)。 + - in: query + name: ordering + schema: {type: string} + description: 排序字段,前缀 - 代表倒序。 + - in: query + name: limit + schema: {type: integer, minimum: 1} + description: 分页大小。 + - in: query + name: offset + schema: {type: integer, minimum: 0} + description: 分页偏移量。 responses: '200': - description: 列表 - content: - application/json: - schema: - oneOf: - - type: array - items: - $ref: '#/components/schemas/PrintingOrder' - - type: object - properties: - count: - type: integer - results: - type: array - items: - $ref: '#/components/schemas/PrintingOrder' - '401': - description: 未认证 - '403': - description: 没有权限 + description: 成功返回分页结果。 security: - BearerAuth: [] post: + tags: [PrintingOrder] summary: 创建印染订单 requestBody: required: true @@ -376,40 +97,29 @@ paths: schema: $ref: '#/components/schemas/PrintingOrderCreate' responses: - '201': - description: 创建成功 - content: - application/json: - schema: - $ref: '#/components/schemas/PrintingOrder' - '400': - description: 校验错误 - '403': - description: 没有权限 + '201': {description: 创建成功} + '400': {description: 参数错误或流程不可用} security: - BearerAuth: [] + /printing-orders/{id}/: parameters: - - name: id - in: path + - in: path + name: id required: true - schema: - type: integer + schema: {type: integer} + description: 印染订单 ID。 get: + tags: [PrintingOrder] summary: 获取印染订单详情 responses: - '200': - description: 详情 - content: - application/json: - schema: - $ref: '#/components/schemas/PrintingOrder' - '404': - description: 未找到 + '200': {description: 详情数据} + '404': {description: 未找到} security: - BearerAuth: [] put: - summary: 更新印染订单(全量) + tags: [PrintingOrder] + summary: 全量更新印染订单 requestBody: required: true content: @@ -417,20 +127,13 @@ paths: schema: $ref: '#/components/schemas/PrintingOrderCreate' responses: - '200': - description: 更新成功 - content: - application/json: - schema: - $ref: '#/components/schemas/PrintingOrder' - '400': - description: 校验错误 - '403': - description: 没有权限 + '200': {description: 更新成功} + '400': {description: 参数错误或流程不可更改} security: - BearerAuth: [] patch: - summary: 更新印染订单(部分) + tags: [PrintingOrder] + summary: 局部更新印染订单 requestBody: required: true content: @@ -438,180 +141,107 @@ paths: schema: $ref: '#/components/schemas/PrintingOrderCreate' responses: - '200': - description: 更新成功 - content: - application/json: - schema: - $ref: '#/components/schemas/PrintingOrder' - '400': - description: 校验错误 + '200': {description: 更新成功} + '400': {description: 参数错误或流程不可更改} security: - BearerAuth: [] delete: - summary: 删除(不支持) - description: 删除操作已禁用,请使用 作废 操作。 + tags: [PrintingOrder] + summary: 禁用删除 responses: - '405': - description: 不支持删除 - content: - application/json: - schema: - type: object - properties: - detail: - type: string - security: - - BearerAuth: [] + '405': {description: 删除被禁止,请使用作废接口} + /printing-orders/{id}/invalidate/: post: - summary: 作废订单 - description: 将订单标记为已作废(需要 printing.can_invalidate_printingorder 权限)。 - parameters: - - name: id - in: path - required: true - schema: - type: integer + tags: [PrintingOrder] + summary: 作废印染订单 responses: - '200': - description: 作废成功 - content: - application/json: - schema: - type: object - properties: - detail: - type: string - data: - $ref: '#/components/schemas/PrintingOrder' - '400': - description: 已作废或请求错误 - '403': - description: 没有权限 + '200': {description: 作废成功} + '400': {description: 已作废} + '403': {description: 无权限} security: - BearerAuth: [] + /printing-orders/{id}/activate/: post: - summary: 恢复订单 - description: 将订单从作废状态恢复(需要 printing.can_activate_printingorder 权限)。 - parameters: - - name: id - in: path - required: true - schema: - type: integer + tags: [PrintingOrder] + summary: 恢复印染订单 responses: - '200': - description: 恢复成功 - content: - application/json: - schema: - type: object - properties: - detail: - type: string - data: - $ref: '#/components/schemas/PrintingOrder' - '400': - description: 未作废或请求错误 - '403': - description: 没有权限 + '200': {description: 恢复成功} + '400': {description: 订单未作废} + '403': {description: 无权限} security: - BearerAuth: [] + /printing-orders/{id}/mark_fabric_received/: post: + tags: [PrintingOrder] summary: 标记布料已收 - description: 将订单标记为布料已收到。 - parameters: - - name: id - in: path - required: true - schema: - type: integer responses: - '200': - description: 标记成功 - content: - application/json: - schema: - type: object - properties: - detail: - type: string - data: - $ref: '#/components/schemas/PrintingOrder' - '400': - description: 已标记或请求错误 + '200': {description: 状态更新成功} + '400': {description: 已标记} security: - BearerAuth: [] /printing-jobs/: get: - summary: 获取印染款式明细列表 - description: 支持过滤(printing_order, product, product_name, unit, quantity_min/max, pieces_min/max)、search、ordering 和分页。 + tags: [PrintingJob] + summary: 列出印染款式明细 parameters: - - $ref: '#/components/parameters/limit' - - $ref: '#/components/parameters/offset' - - $ref: '#/components/parameters/search' - - $ref: '#/components/parameters/ordering' - - name: printing_order - in: query - schema: - type: integer - - name: product - in: query - schema: - type: integer - - name: product_name - in: query - schema: - type: string - - name: unit - in: query - schema: - type: string - - name: quantity_min - in: query - schema: - type: integer - - name: quantity_max - in: query - schema: - type: integer - - name: pieces_min - in: query - schema: - type: integer - - name: pieces_max - in: query - schema: - type: integer + - in: query + name: printing_order + schema: {type: integer} + description: 所属印染订单 ID。 + - in: query + name: product + schema: {type: integer} + description: 产品 ID。 + - in: query + name: product_name + schema: {type: string} + description: 产品名称(模糊匹配)。 + - in: query + name: unit + schema: {type: string} + description: 单位(模糊匹配)。 + - in: query + name: quantity_min + schema: {type: number} + description: 数量下限。 + - in: query + name: quantity_max + schema: {type: number} + description: 数量上限。 + - in: query + name: pieces_min + schema: {type: integer} + description: 件数下限。 + - in: query + name: pieces_max + schema: {type: integer} + description: 件数上限。 + - in: query + name: search + schema: {type: string} + description: 全文搜索(产品/单位/尺寸/备注)。 + - in: query + name: ordering + schema: {type: string} + description: 排序字段。 + - in: query + name: limit + schema: {type: integer, minimum: 1} + description: 分页大小。 + - in: query + name: offset + schema: {type: integer, minimum: 0} + description: 分页偏移。 responses: - '200': - description: 列表 - content: - application/json: - schema: - oneOf: - - type: array - items: - $ref: '#/components/schemas/PrintingJob' - - type: object - properties: - count: - type: integer - results: - type: array - items: - $ref: '#/components/schemas/PrintingJob' - '401': - description: 未认证 - '403': - description: 没有权限 + '200': {description: 成功返回分页结果} security: - BearerAuth: [] post: + tags: [PrintingJob] summary: 创建印染款式明细 requestBody: required: true @@ -620,41 +250,29 @@ paths: schema: $ref: '#/components/schemas/PrintingJobCreate' responses: - '201': - description: 创建成功 - content: - application/json: - schema: - $ref: '#/components/schemas/PrintingJob' - '400': - description: 校验错误 - '403': - description: 没有权限 + '201': {description: 创建成功} + '400': {description: 参数错误} security: - BearerAuth: [] /printing-jobs/{id}/: parameters: - - name: id - in: path + - in: path + name: id required: true - schema: - type: integer + schema: {type: integer} + description: 印染任务 ID。 get: - summary: 获取款式明细详情 + tags: [PrintingJob] + summary: 获取印染款式详情 responses: - '200': - description: 详情 - content: - application/json: - schema: - $ref: '#/components/schemas/PrintingJob' - '404': - description: 未找到 + '200': {description: 详情数据} + '404': {description: 未找到} security: - BearerAuth: [] put: - summary: 更新款式明细(全量) + tags: [PrintingJob] + summary: 全量更新印染款式 requestBody: required: true content: @@ -662,18 +280,12 @@ paths: schema: $ref: '#/components/schemas/PrintingJobCreate' responses: - '200': - description: 更新成功 - content: - application/json: - schema: - $ref: '#/components/schemas/PrintingJob' - '400': - description: 校验错误 + '200': {description: 更新成功} security: - BearerAuth: [] patch: - summary: 更新款式明细(部分) + tags: [PrintingJob] + summary: 局部更新印染款式 requestBody: required: true content: @@ -681,30 +293,520 @@ paths: schema: $ref: '#/components/schemas/PrintingJobCreate' responses: - '200': - description: 更新成功 - content: - application/json: - schema: - $ref: '#/components/schemas/PrintingJob' - '400': - description: 校验错误 + '200': {description: 更新成功} security: - BearerAuth: [] delete: - summary: 删除(不支持) - description: 删除操作已禁用,请使用所需的业务操作。 + tags: [PrintingJob] + summary: 禁用删除 responses: - '405': - description: 不支持删除 - content: - application/json: - schema: - type: object - properties: - detail: - type: string + '405': {description: 删除被禁止} + + /printing-jobs/{id}/advance-to-next-state/: + post: + tags: [PrintingJob] + summary: 推进到下一个流程状态 + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/StateAdvanceRequest' + responses: + '200': {description: 推进成功} + '400': {description: 流程已完成或缺少必填参数} security: - BearerAuth: [] -components_end: {} + /printing-jobs/{id}/step-back-one-state/: + post: + tags: [PrintingJob] + summary: 回退一个流程状态 + responses: + '200': {description: 回退成功} + '400': {description: 无可回退状态或未绑定流程} + security: + - BearerAuth: [] + + /printing-jobs/{id}/completed-states/: + get: + tags: [PrintingJob] + summary: 查询已完成的流程节点 + parameters: + - in: query + name: include_cancelled + schema: {type: boolean} + description: 是否包含已撤销记录,默认 false。 + responses: + '200': {description: 返回完成节点列表} + security: + - BearerAuth: [] + + /printing-jobs/{id}/timeline/: + get: + tags: [PrintingJob] + summary: 查看流程时间线 + responses: + '200': {description: 返回完整时间线(未包含撤销记录)} + security: + - BearerAuth: [] + + /plate-orders/: + get: + tags: [PlateOrder] + summary: 列出开版订单 + parameters: + - in: query + name: customer + schema: {type: integer} + description: 客户 ID。 + - in: query + name: customer_name + schema: {type: string} + description: 客户名称(模糊匹配)。 + - in: query + name: customer_phone + schema: {type: string} + description: 客户电话(模糊匹配)。 + - in: query + name: salesperson + schema: {type: integer} + description: 业务员 ID。 + - in: query + name: merchandiser + schema: {type: integer} + description: 跟单员 ID。 + - in: query + name: plate_type + schema: {type: string} + description: 版型(模糊匹配)。 + - in: query + name: urgency_level + schema: {type: string} + description: 紧急程度。 + - in: query + name: is_invalid + schema: {type: boolean} + description: 是否作废。 + - in: query + name: is_ordered + schema: {type: boolean} + description: 是否已下单。 + - in: query + name: is_mark_frame + schema: {type: boolean} + description: 是否套唛架。 + - in: query + name: fabric + schema: {type: string} + description: 面料(模糊匹配)。 + - in: query + name: style_name + schema: {type: string} + description: 款式名称(模糊匹配)。 + - in: query + name: plate_date_from + schema: {type: string, format: date} + description: 开版日期起始。 + - in: query + name: plate_date_to + schema: {type: string, format: date} + description: 开版日期结束。 + - in: query + name: required_completion_date_from + schema: {type: string, format: date} + description: 要求完成日期起始。 + - in: query + name: required_completion_date_to + schema: {type: string, format: date} + description: 要求完成日期结束。 + - in: query + name: created_date_from + schema: {type: string, format: date} + description: 创建日期起始。 + - in: query + name: created_date_to + schema: {type: string, format: date} + description: 创建日期结束。 + - in: query + name: search + schema: {type: string} + description: 全文搜索(设计编号/款式/客户/面料)。 + - in: query + name: ordering + schema: {type: string} + description: 排序字段。 + - in: query + name: limit + schema: {type: integer, minimum: 1} + description: 分页大小。 + - in: query + name: offset + schema: {type: integer, minimum: 0} + description: 分页偏移。 + responses: + '200': {description: 成功返回分页结果} + security: + - BearerAuth: [] + post: + tags: [PlateOrder] + summary: 创建开版订单 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PlateOrderCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/PlateOrderCreate' + responses: + '201': {description: 创建成功} + '400': {description: 参数错误或流程不存在} + security: + - BearerAuth: [] + + /plate-orders/{id}/: + parameters: + - in: path + name: id + required: true + schema: {type: integer} + description: 开版订单 ID。 + get: + tags: [PlateOrder] + summary: 获取开版订单详情 + responses: + '200': {description: 详情数据} + '404': {description: 未找到} + security: + - BearerAuth: [] + put: + tags: [PlateOrder] + summary: 全量更新开版订单 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PlateOrderCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/PlateOrderCreate' + responses: + '200': {description: 更新成功} + security: + - BearerAuth: [] + patch: + tags: [PlateOrder] + summary: 局部更新开版订单 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PlateOrderCreate' + multipart/form-data: + schema: + $ref: '#/components/schemas/PlateOrderCreate' + responses: + '200': {description: 更新成功} + security: + - BearerAuth: [] + delete: + tags: [PlateOrder] + summary: 禁用删除 + responses: + '405': {description: 删除被禁止} + + /plate-orders/{id}/invalidate/: + post: + tags: [PlateOrder] + summary: 作废开版订单 + responses: + '200': {description: 作废成功} + '400': {description: 已作废} + '403': {description: 无权限} + security: + - BearerAuth: [] + + /plate-orders/{id}/activate/: + post: + tags: [PlateOrder] + summary: 恢复开版订单 + responses: + '200': {description: 恢复成功} + '400': {description: 未作废} + '403': {description: 无权限} + security: + - BearerAuth: [] + + /plate-orders/{id}/advance-to-next-state/: + post: + tags: [PlateOrder] + summary: 推进到下一个流程状态 + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/StateAdvanceRequest' + responses: + '200': {description: 推进成功} + '400': {description: 流程已完成或缺少必填参数} + security: + - BearerAuth: [] + + /plate-orders/{id}/step-back-one-state/: + post: + tags: [PlateOrder] + summary: 回退一个流程状态 + responses: + '200': {description: 回退成功} + '400': {description: 无可回退状态或未绑定流程} + security: + - BearerAuth: [] + + /plate-orders/{id}/completed-states/: + get: + tags: [PlateOrder] + summary: 查询已完成的流程节点 + parameters: + - in: query + name: include_cancelled + schema: {type: boolean} + description: 是否包含已撤销记录,默认 false。 + responses: + '200': {description: 返回完成节点列表} + security: + - BearerAuth: [] + + /plate-orders/{id}/timeline/: + get: + tags: [PlateOrder] + summary: 查看流程时间线 + responses: + '200': {description: 返回完整时间线(未包含撤销记录)} + security: + - BearerAuth: [] + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + PrintingOrderCreate: + type: object + required: [customer, fabric, width] + properties: + customer: + type: integer + description: 客户 ID。 + fabric: + type: string + description: 面料名称。 + width: + type: string + description: 幅宽描述。 + is_urgent: + type: boolean + description: 是否紧急。 + default: false + area: + type: string + description: 地区。 + address: + type: string + description: 详细地址。 + fabric_source: + type: string + description: 布料来源。 + is_fabric_received: + type: boolean + description: 布料是否已收。 + default: false + craft: + type: string + description: 工艺说明。 + description: + type: string + description: 订单描述。 + outgoing_date: + type: string + format: date + description: 计划出货日期。 + curve: + type: string + description: 曲线。 + new_curve: + type: string + description: 新增曲线。 + position: + type: string + description: 印位描述。 + printing_warn: + type: string + description: 打印注意事项。 + rolling_warn: + type: string + description: 滚筒注意事项。 + production_warn: + type: string + description: 生产注意事项。 + is_invalid: + type: boolean + description: 是否作废。 + default: false + process: + type: integer + nullable: true + description: 关联的印染流程 ID,缺省时采用 settings.PRINTING_DEFAULT_PROCESS_ID。 + PrintingJobCreate: + type: object + required: [printing_order, product, quantity, unit] + properties: + printing_order: + type: integer + description: 所属印染订单 ID。 + product: + type: integer + description: 产品 ID。 + quantity: + type: number + description: 数量,必须大于 0。 + unit: + type: string + description: 数量单位。 + size: + type: string + nullable: true + description: 尺寸描述。 + pieces: + type: integer + nullable: true + description: 件数。 + description: + type: string + nullable: true + description: 备注。 + PlateOrderCreate: + type: object + required: [customer] + properties: + customer: + type: integer + description: 客户 ID。 + design_code: + type: string + description: 设计编号。 + plate_type: + type: string + description: 版型。 + plate_date: + type: string + format: date-time + nullable: true + description: 开版时间。 + plate_method: + type: string + description: 开版方式。 + plate_image: + type: string + format: binary + nullable: true + description: 开版图片(仅 multipart/form-data 时可上传)。 + plate_notes: + type: string + description: 打版注意事项。 + reprint_reason: + type: string + description: 复版原因。 + urgency_level: + type: string + description: 紧急程度,默认为“正常”。 + is_invalid: + type: boolean + description: 是否作废。 + default: false + area: + type: string + description: 区域。 + default_address: + type: string + description: 默认地址。 + salesperson: + type: integer + nullable: true + description: 业务员 ID。 + merchandiser: + type: integer + nullable: true + description: 跟单员 ID。 + style_name: + type: string + description: 款式名称。 + fabric: + type: string + description: 面料。 + width: + type: string + description: 幅宽。 + production_method: + type: string + description: 做货方式。 + is_mark_frame: + type: boolean + description: 是否套唛架。 + drawing_rating: + type: string + description: 画图评级。 + color_matching_rating: + type: string + description: 调色评级。 + sample_rating: + type: string + description: 套样评级。 + difficulty_rating: + type: string + description: 难度评级。 + sample_meter: + type: string + description: 米样描述。 + required_sample_meters: + type: number + nullable: true + description: 客户要求的米样米数。 + required_completion_date: + type: string + format: date + nullable: true + description: 要求完成日期。 + completion_date: + type: string + format: date-time + nullable: true + description: 实际完成时间。 + approval_result: + type: string + description: 审批结果。 + is_ordered: + type: boolean + description: 是否已下单。 + default: false + customer_feedback: + type: string + description: 客户反馈。 + process: + type: integer + nullable: true + description: 关联流程 ID,缺省时采用 settings.PLATE_ORDER_DEFAULT_PROCESS_ID。 + StateAdvanceRequest: + type: object + properties: + parameters: + type: object + additionalProperties: + type: string + description: 传递给下一个流程节点的参数键值对,必须满足节点必填要求。 diff --git a/stock/models.py b/stock/models.py index 9dd0bd7..d6536e0 100644 --- a/stock/models.py +++ b/stock/models.py @@ -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): diff --git a/stock/tests.py b/stock/tests.py index 9e68fdf..d540f3c 100644 --- a/stock/tests.py +++ b/stock/tests.py @@ -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 函数"""