forked from erp-dev/erp
feat: added type field into warehouse model
This commit is contained in:
419
docs/EMPLOYEE_TYPE_REFACTOR.md
Normal file
419
docs/EMPLOYEE_TYPE_REFACTOR.md
Normal file
@@ -0,0 +1,419 @@
|
||||
# 员工职位类型 (EmployeeType) 重构文档
|
||||
|
||||
本文档描述了 `Employee` 模型的职位管理重构,从字符串枚举改为关联外键表 `EmployeeType`。
|
||||
|
||||
## 变更概述
|
||||
|
||||
### 1. 新增模型:`EmployeeType`
|
||||
|
||||
创建了新的 `EmployeeType` 模型来管理员工职位类型,替代原有的硬编码枚举 `EmployeeTypeEnum`。
|
||||
|
||||
**模型定义**:
|
||||
|
||||
```python
|
||||
class EmployeeType(ModelBase):
|
||||
"""员工职位类型"""
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='employee_types', verbose_name='所属商户')
|
||||
title = models.CharField(max_length=50, verbose_name='职位名称')
|
||||
description = models.TextField(blank=True, null=True, verbose_name='职位描述')
|
||||
reverse = models.CharField(max_length=100, blank=True, null=True, verbose_name='预留字段')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = '员工职位类型'
|
||||
verbose_name_plural = '员工职位类型'
|
||||
unique_together = ('merchant', 'title')
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `merchant`: 所属商户(外键)
|
||||
- `title`: 职位名称
|
||||
- `description`: 职位描述(可选)
|
||||
- `reverse`: 预留字段(可选)
|
||||
|
||||
**约束**:
|
||||
- `unique_together`: 同一商户下职位名称唯一
|
||||
|
||||
### 2. 修改模型:`Employee`
|
||||
|
||||
#### 字段变更
|
||||
|
||||
- **删除**: `job_type` 字段(`CharField`)
|
||||
- **新增**: `position` 字段(`ForeignKey` 指向 `EmployeeType`)
|
||||
|
||||
#### 向后兼容
|
||||
|
||||
为保持 API 兼容性,`job_type` 现在是一个**只读属性**(`@property`),返回关联的 `EmployeeType.title`:
|
||||
|
||||
```python
|
||||
@property
|
||||
def job_type(self) -> str:
|
||||
"""
|
||||
返回职位名称(向后兼容)
|
||||
|
||||
如果有关联的职位类型,返回其 title,否则返回空字符串
|
||||
"""
|
||||
if self.position:
|
||||
return self.position.title
|
||||
return ''
|
||||
```
|
||||
|
||||
**模型定义**:
|
||||
|
||||
```python
|
||||
class Employee(ModelBase):
|
||||
# ... 其他字段 ...
|
||||
position = models.ForeignKey(
|
||||
'EmployeeType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='employees',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='职位',
|
||||
)
|
||||
# ...
|
||||
```
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
**迁移文件**: `basic_info/migrations/0011_remove_employee_job_type_employeetype_and_more.py`
|
||||
|
||||
**操作步骤**:
|
||||
1. 创建 `EmployeeType` 模型
|
||||
2. 删除 `Employee.job_type` 字段
|
||||
3. 添加 `Employee.position` 字段
|
||||
|
||||
**注意**:
|
||||
- 旧的 `job_type` 数据会在迁移时丢失
|
||||
- 需要手动创建新的 `EmployeeType` 记录并关联到员工
|
||||
|
||||
## API 变更
|
||||
|
||||
### 1. 新增接口:`/api/backend/employee-types/`
|
||||
|
||||
管理员工职位类型的完整 CRUD 接口。
|
||||
|
||||
#### 创建职位类型
|
||||
|
||||
**端点**: `POST /api/backend/employee-types/`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"title": "打纸工",
|
||||
"description": "负责打纸工作"
|
||||
}
|
||||
```
|
||||
|
||||
**响应** (201 Created):
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"merchant": 1,
|
||||
"title": "打纸工",
|
||||
"description": "负责打纸工作",
|
||||
"reverse": null,
|
||||
"created_at": "2025-11-20T10:00:00Z",
|
||||
"updated_at": "2025-11-20T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 列出职位类型
|
||||
|
||||
**端点**: `GET /api/backend/employee-types/`
|
||||
|
||||
**响应** (200 OK):
|
||||
```json
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"merchant": 1,
|
||||
"title": "打纸工",
|
||||
"description": "负责打纸工作",
|
||||
"reverse": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"merchant": 1,
|
||||
"title": "滚筒工",
|
||||
"description": "操作滚筒设备",
|
||||
"reverse": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 更新职位类型
|
||||
|
||||
**端点**: `PATCH /api/backend/employee-types/{id}/`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"description": "更新后的描述"
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除职位类型
|
||||
|
||||
**端点**: `DELETE /api/backend/employee-types/{id}/`
|
||||
|
||||
**响应**: 204 No Content
|
||||
|
||||
**注意**: 如果有员工关联到该职位类型,删除会失败(`PROTECT` 约束)。
|
||||
|
||||
### 2. 修改接口:`/api/backend/employees/`
|
||||
|
||||
#### 字段变更
|
||||
|
||||
- **新增**: `position` (integer, 外键 ID, 可选)
|
||||
- **保留**: `job_type` (string, 只读, 向后兼容)
|
||||
|
||||
#### 创建员工(带职位)
|
||||
|
||||
**端点**: `POST /api/backend/employees/`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "张三",
|
||||
"position": 1,
|
||||
"mobile": "13800138000"
|
||||
}
|
||||
```
|
||||
|
||||
**响应** (201 Created):
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"merchant": 1,
|
||||
"name": "张三",
|
||||
"position": 1,
|
||||
"job_type": "打纸工",
|
||||
"mobile": "13800138000",
|
||||
"status": "在职"
|
||||
}
|
||||
```
|
||||
|
||||
#### 创建员工(不指定职位)
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "李四",
|
||||
"mobile": "13900139000"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"merchant": 1,
|
||||
"name": "李四",
|
||||
"position": null,
|
||||
"job_type": "",
|
||||
"mobile": "13900139000",
|
||||
"status": "在职"
|
||||
}
|
||||
```
|
||||
|
||||
#### 更新员工职位
|
||||
|
||||
**端点**: `PATCH /api/backend/employees/{id}/`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"position": 2
|
||||
}
|
||||
```
|
||||
|
||||
**响应** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "张三",
|
||||
"position": 2,
|
||||
"job_type": "滚筒工",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### `job_type` 字段说明
|
||||
|
||||
- **只读**: 不能通过 API 直接设置
|
||||
- **自动计算**: 返回 `position.title` 或空字符串
|
||||
- **向后兼容**: 保持旧接口的兼容性
|
||||
|
||||
## Admin 界面变更
|
||||
|
||||
### 1. 新增:`EmployeeTypeAdmin`
|
||||
|
||||
管理员工职位类型的 Admin 界面。
|
||||
|
||||
**列表显示**: `title`, `description`
|
||||
**搜索字段**: `title`, `description`
|
||||
|
||||
### 2. 修改:`EmployeeAdmin`
|
||||
|
||||
#### 列表显示
|
||||
|
||||
- **移除**: `job_type` 字段
|
||||
- **新增**: `position_display` (显示职位名称)
|
||||
|
||||
#### 筛选器
|
||||
|
||||
- **移除**: `job_type` 筛选器
|
||||
- **保留**: `status` 筛选器
|
||||
|
||||
## 测试
|
||||
|
||||
### 模型测试
|
||||
|
||||
**文件**: `basic_info/tests.py`
|
||||
|
||||
**测试用例**:
|
||||
- `test_create_employee_type`: 创建职位类型
|
||||
- `test_employee_type_unique_together`: 唯一性约束
|
||||
- `test_employee_with_position`: 员工关联职位
|
||||
- `test_employee_without_position`: 员工无职位
|
||||
- `test_employee_type_reverse_field`: 预留字段
|
||||
- `test_multiple_employees_same_position`: 多个员工同一职位
|
||||
|
||||
### API 测试
|
||||
|
||||
**文件**: `api_man/tests.py`
|
||||
|
||||
**职位类型 API 测试**:
|
||||
- `test_create_employee_type`: 创建职位类型
|
||||
- `test_list_employee_types`: 列出职位类型
|
||||
- `test_update_employee_type`: 更新职位类型
|
||||
- `test_delete_employee_type`: 删除职位类型
|
||||
|
||||
**员工 API 测试**:
|
||||
- `test_create_employee_with_position`: 创建带职位的员工
|
||||
- `test_create_employee_without_position`: 创建不带职位的员工
|
||||
- `test_list_employees_includes_job_type`: 列表包含 `job_type`
|
||||
- `test_update_employee_position`: 更新员工职位
|
||||
- `test_job_type_is_read_only`: `job_type` 只读验证
|
||||
|
||||
**运行测试**:
|
||||
```bash
|
||||
# 模型测试
|
||||
uv run python manage.py test basic_info.tests.EmployeeTypeTestCase
|
||||
|
||||
# API 测试
|
||||
uv run python manage.py test api_man.tests.EmployeeTypeAPITestCase api_man.tests.EmployeeAPITestCase
|
||||
```
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从旧系统迁移
|
||||
|
||||
1. **创建职位类型**
|
||||
|
||||
手动创建所需的 `EmployeeType` 记录:
|
||||
|
||||
```python
|
||||
from basic_info.models import Merchant, EmployeeType
|
||||
|
||||
merchant = Merchant.objects.get(id=1)
|
||||
|
||||
EmployeeType.objects.create(merchant=merchant, title='打纸')
|
||||
EmployeeType.objects.create(merchant=merchant, title='滚筒')
|
||||
EmployeeType.objects.create(merchant=merchant, title='仓库')
|
||||
EmployeeType.objects.create(merchant=merchant, title='送货')
|
||||
```
|
||||
|
||||
2. **更新现有员工**
|
||||
|
||||
将员工关联到对应的职位类型:
|
||||
|
||||
```python
|
||||
from basic_info.models import Employee, EmployeeType
|
||||
|
||||
# 为所有员工设置职位
|
||||
printer_type = EmployeeType.objects.get(merchant=merchant, title='打纸')
|
||||
Employee.objects.filter(merchant=merchant).update(position=printer_type)
|
||||
```
|
||||
|
||||
### 前端迁移
|
||||
|
||||
#### 读取员工信息
|
||||
|
||||
- **兼容**: `job_type` 字段仍然可用(只读)
|
||||
- **推荐**: 使用 `position` 字段(ID)并根据需要查询 `EmployeeType`
|
||||
|
||||
#### 创建/更新员工
|
||||
|
||||
- **旧方式** (已废弃): 不能再设置 `job_type`
|
||||
- **新方式**: 设置 `position` 字段为 `EmployeeType` 的 ID
|
||||
|
||||
```javascript
|
||||
// 旧方式 (不再支持)
|
||||
// { name: "张三", job_type: "打纸" }
|
||||
|
||||
// 新方式
|
||||
{ name: "张三", position: 1 } // 1 是 EmployeeType 的 ID
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
1. **灵活性**: 职位类型可以动态创建和管理,不需要修改代码
|
||||
2. **可扩展性**: 每个职位类型可以添加更多属性(如权限、级别等)
|
||||
3. **多商户支持**: 每个商户可以定义自己的职位类型
|
||||
4. **描述信息**: 可以为每个职位添加详细描述
|
||||
5. **向后兼容**: 保留 `job_type` 属性,现有前端代码无需大改
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据丢失**: 迁移会删除旧的 `job_type` 数据,迁移前请备份
|
||||
2. **外键约束**: 删除 `EmployeeType` 前需要确保没有员工关联
|
||||
3. **唯一性**: 同一商户下职位名称必须唯一
|
||||
4. **只读属性**: `job_type` 现在是只读的,不能通过 API 直接设置
|
||||
5. **空值处理**: `position` 可以为空,`job_type` 会返回空字符串
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 序列化器特殊处理
|
||||
|
||||
由于 `EmployeeType` 模型有 `unique_together = ('merchant', 'title')` 约束,DRF 会自动添加 `UniqueTogetherValidator`,导致创建时要求提供 `merchant` 字段。
|
||||
|
||||
**解决方案**: 在 `EmployeeTypeSerializer.__init__` 中移除验证器对 `merchant` 字段的要求:
|
||||
|
||||
```python
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for validator in self.validators:
|
||||
if hasattr(validator, 'fields') and 'merchant' in validator.fields:
|
||||
validator.fields = tuple(f for f in validator.fields if f != 'merchant')
|
||||
```
|
||||
|
||||
这样 `merchant` 可以在 `BaseViewSet.perform_create` 中自动设置。
|
||||
|
||||
### 属性 vs 字段
|
||||
|
||||
`job_type` 使用 `@property` 装饰器实现,这是一个**模型属性**而非**数据库字段**:
|
||||
|
||||
- **优点**: 保持接口兼容,自动计算
|
||||
- **缺点**: 不能用于数据库查询、排序或过滤
|
||||
- **解决**: 需要过滤时使用 `position__title` 进行关联查询
|
||||
|
||||
## 相关文件
|
||||
|
||||
- **模型**: `basic_info/models.py`
|
||||
- **序列化器**: `api_man/serializers.py`
|
||||
- **视图**: `api_man/views.py`
|
||||
- **URL**: `api_man/urls.py`
|
||||
- **Admin**: `basic_info/admin.py`
|
||||
- **迁移**: `basic_info/migrations/0011_remove_employee_job_type_employeetype_and_more.py`
|
||||
- **测试**: `basic_info/tests.py`, `api_man/tests.py`
|
||||
|
||||
@@ -28,6 +28,7 @@ GET /api/v1/plate-orders/
|
||||
| customer_phone | string | 否 | 客户电话(模糊查询) |
|
||||
| salesperson | integer | 否 | 业务员ID |
|
||||
| merchandiser | integer | 否 | 跟单员ID |
|
||||
| designer | integer | 否 | 设计师ID |
|
||||
| plate_type | string | 否 | 版型(模糊查询) |
|
||||
| urgency_level | string | 否 | 紧急程度(模糊查询) |
|
||||
| is_invalid | boolean | 否 | 是否作废(true/false) |
|
||||
@@ -69,6 +70,8 @@ GET /api/v1/plate-orders/
|
||||
"salesperson_name": "张三",
|
||||
"merchandiser": 4,
|
||||
"merchandiser_name": "李四",
|
||||
"designer": 8,
|
||||
"designer_name": "王五",
|
||||
"style_name": "花朵印染",
|
||||
"fabric": "棉布",
|
||||
"width": "150cm",
|
||||
@@ -124,6 +127,8 @@ GET /api/v1/plate-orders/{id}/
|
||||
"salesperson_name": "张三",
|
||||
"merchandiser": 4,
|
||||
"merchandiser_name": "李四",
|
||||
"designer": 8,
|
||||
"designer_name": "王五",
|
||||
"style_name": "花朵印染",
|
||||
"fabric": "棉布",
|
||||
"width": "150cm",
|
||||
@@ -175,6 +180,7 @@ Content-Type: application/json
|
||||
"urgency_level": "正常",
|
||||
"salesperson": 3,
|
||||
"merchandiser": 4,
|
||||
"designer": 8,
|
||||
"required_completion_date": "2025-11-25",
|
||||
"is_mark_frame": false,
|
||||
"plate_method": "机器",
|
||||
@@ -199,6 +205,7 @@ Content-Type: application/json
|
||||
- `default_address`: 默认地址
|
||||
- `salesperson`: 业务员ID
|
||||
- `merchandiser`: 跟单员ID
|
||||
- `designer`: 设计师ID
|
||||
- `style_name`: 款式名称
|
||||
- `fabric`: 面料
|
||||
- `fabric_source`: 布料来源(可空字符串,如“客户提供”)
|
||||
@@ -579,6 +586,7 @@ GET /api/v1/plate-orders/{id}/timeline/
|
||||
| default_address | string | 默认地址 |
|
||||
| salesperson | FK | 业务员(外键) |
|
||||
| merchandiser | FK | 跟单员(外键) |
|
||||
| designer | FK | 设计师(外键) |
|
||||
| process | integer | 流程ID(无外键约束) |
|
||||
| style_name | string | 款式名称 |
|
||||
| fabric | string | 面料 |
|
||||
|
||||
146
docs/WAREHOUSE_TYPE.md
Normal file
146
docs/WAREHOUSE_TYPE.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# 仓库类型 (WarehouseTypeEnum)
|
||||
|
||||
本文档描述了 `basic_info` 模块中 `WareHouse` 模型的 `type` 字段及其枚举值。
|
||||
|
||||
## 功能说明
|
||||
|
||||
仓库类型字段 (`type`) 用于区分仓库的类别,目前支持两种类型:
|
||||
|
||||
- **整仓**: 存储完整的、未分割的货物
|
||||
- **散仓**: 存储零散的、分割后的货物
|
||||
|
||||
## 枚举值
|
||||
|
||||
### WarehouseTypeEnum
|
||||
|
||||
| 值 (Integer) | 名称 (String) | 描述 |
|
||||
|--------------|---------------|------|
|
||||
| `1` | `整仓` | 默认值,用于存储整卷/整件货物 |
|
||||
| `2` | `散仓` | 用于存储散卷/拆分后的货物 |
|
||||
|
||||
## 模型定义
|
||||
|
||||
```python
|
||||
class WarehouseTypeEnum(models.IntegerChoices):
|
||||
"""仓库类别枚举"""
|
||||
WHOLE = 1, '整仓'
|
||||
SCATTERED = 2, '散仓'
|
||||
|
||||
|
||||
class WareHouse(ModelBase):
|
||||
# ...
|
||||
type = models.IntegerField(
|
||||
choices=WarehouseTypeEnum.choices,
|
||||
default=WarehouseTypeEnum.WHOLE,
|
||||
verbose_name='仓库类别',
|
||||
)
|
||||
# ...
|
||||
```
|
||||
|
||||
## API 使用
|
||||
|
||||
### 创建仓库
|
||||
|
||||
**端点**: `POST /api/backend/warehouses/`
|
||||
|
||||
**请求体示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "主仓库",
|
||||
"type": 1,
|
||||
"location": "工厂一楼",
|
||||
"area": "广州",
|
||||
"mode": 1
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
- `type`: 仓库类型,可选值:`1` (整仓) 或 `2` (散仓),默认为 `1`
|
||||
|
||||
### 列表查询
|
||||
|
||||
**端点**: `GET /api/backend/warehouses/`
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "主仓库",
|
||||
"type": 1,
|
||||
"location": "工厂一楼",
|
||||
"mode": 1,
|
||||
"area": "广州"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "散货仓",
|
||||
"type": 2,
|
||||
"location": "工厂二楼",
|
||||
"mode": 1,
|
||||
"area": "广州"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 更新仓库
|
||||
|
||||
**端点**: `PATCH /api/backend/warehouses/{id}/`
|
||||
|
||||
**请求体示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": 2
|
||||
}
|
||||
```
|
||||
|
||||
## Admin 界面
|
||||
|
||||
在 Django Admin 界面中:
|
||||
|
||||
- **列表页**: `type` 字段显示在列表中,并可用于筛选
|
||||
- **编辑页**: `type` 字段显示为下拉选择框
|
||||
- **显示文本**: 使用中文标签(整仓/散仓)
|
||||
|
||||
### Admin 配置
|
||||
|
||||
- `list_display` 包含 `type` 字段
|
||||
- `list_filter` 包含 `type` 字段,方便按类型筛选
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **默认值**: 创建仓库时,如果未指定 `type`,默认为 `1`(整仓)
|
||||
2. **验证**: `type` 字段只接受 `1` 或 `2` 两个值
|
||||
3. **兼容性**: 现有仓库记录在迁移后将自动设置为默认值(整仓)
|
||||
4. **扩展性**: 如需添加新的仓库类型,可在 `WarehouseTypeEnum` 中添加新枚举值
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
新字段通过以下迁移添加:
|
||||
|
||||
- 迁移文件: `basic_info/migrations/0010_warehouse_type.py`
|
||||
- 字段: `type` (IntegerField)
|
||||
- 默认值: `1` (整仓)
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
测试文件包括:
|
||||
|
||||
- **模型测试**: `basic_info/tests.py` - 测试字段默认值、枚举值等
|
||||
- **API 测试**: `api_man/tests.py` - 测试 CRUD 操作和 API 返回值
|
||||
|
||||
运行测试:
|
||||
|
||||
```bash
|
||||
# 测试模型
|
||||
uv run python manage.py test basic_info.tests.WarehouseTypeTestCase
|
||||
|
||||
# 测试 API
|
||||
uv run python manage.py test api_man.tests.WarehouseAPITestCase
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user