forked from erp-dev/erp
feat: added type field into warehouse model
This commit is contained in:
@@ -8,7 +8,7 @@ class AdminBase(admin.ModelAdmin):
|
||||
|
||||
def get_readonly_fields(self, request, obj = ...):
|
||||
readonly_fields = list(super().get_readonly_fields(request, obj))
|
||||
if request.user.is_superuser and isinstance(obj, models.Employee) is False:
|
||||
if request.user.is_superuser is False and isinstance(obj, models.Employee) is False:
|
||||
readonly_fields.append('merchant')
|
||||
return readonly_fields
|
||||
|
||||
@@ -82,8 +82,8 @@ class BankAccountAdmin(AdminBase):
|
||||
|
||||
@admin.register(models.WareHouse)
|
||||
class WareHouseAdmin(AdminBase):
|
||||
list_display = ('name', 'location', 'area', 'mobile', 'mode')
|
||||
list_filter = ('area', 'mode')
|
||||
list_display = ('name', 'type', 'location', 'area', 'mobile', 'mode')
|
||||
list_filter = ('area', 'mode', 'type')
|
||||
search_fields = ('name', 'location', 'mobile')
|
||||
|
||||
|
||||
@@ -104,13 +104,27 @@ class CustomerAdmin(AdminBase):
|
||||
|
||||
@admin.register(models.Employee)
|
||||
class EmployeeAdmin(AdminBase):
|
||||
list_display = ('name', 'mobile', 'status', 'job_type', 'sys_user')
|
||||
list_display = ('name', 'mobile', 'status', 'position_display', 'sys_user')
|
||||
search_fields = ('name', 'mobile')
|
||||
list_filter = (
|
||||
'job_type',
|
||||
'position',
|
||||
'status',
|
||||
)
|
||||
|
||||
@admin.display(description='职位')
|
||||
def position_display(self, obj: models.Employee):
|
||||
"""显示职位名称"""
|
||||
return obj.job_type if obj.position else '-'
|
||||
|
||||
def get_readonly_fields(self, request, obj=...):
|
||||
return []
|
||||
|
||||
|
||||
@admin.register(models.EmployeeType)
|
||||
class EmployeeTypeAdmin(AdminBase):
|
||||
list_display = ('id', 'title', 'description')
|
||||
search_fields = ('title', 'description')
|
||||
|
||||
def get_readonly_fields(self, request, obj=...):
|
||||
return []
|
||||
|
||||
|
||||
18
basic_info/migrations/0010_warehouse_type.py
Normal file
18
basic_info/migrations/0010_warehouse_type.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-20 05:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('basic_info', '0009_product_minimum_quantity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='warehouse',
|
||||
name='type',
|
||||
field=models.IntegerField(choices=[(1, '整仓'), (2, '散仓')], default=1, verbose_name='仓库类别'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-20 05:56
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('basic_info', '0010_warehouse_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='employee',
|
||||
name='job_type',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EmployeeType',
|
||||
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)),
|
||||
('title', models.CharField(max_length=50, verbose_name='职位名称')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='职位描述')),
|
||||
('reverse', models.CharField(blank=True, max_length=100, null=True, verbose_name='预留字段')),
|
||||
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='employee_types', to='basic_info.merchant', verbose_name='所属商户')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '员工职位类型',
|
||||
'verbose_name_plural': '员工职位类型',
|
||||
'unique_together': {('merchant', 'title')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employee',
|
||||
name='position',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='employees', to='basic_info.employeetype', verbose_name='职位'),
|
||||
),
|
||||
]
|
||||
@@ -19,6 +19,12 @@ class WareHouseModeEnum(models.IntegerChoices):
|
||||
UNRESTRICTED = 3, '宽进宽出'
|
||||
|
||||
|
||||
class WarehouseTypeEnum(models.IntegerChoices):
|
||||
"""仓库类别枚举"""
|
||||
WHOLE = 1, '整仓'
|
||||
SCATTERED = 2, '散仓'
|
||||
|
||||
|
||||
class MerchantTypeEnum(models.IntegerChoices):
|
||||
"""商户类型枚举"""
|
||||
STORE = 1, '布行'
|
||||
@@ -62,6 +68,23 @@ class DeviceTypeEnum(models.TextChoices):
|
||||
# ==================== 模型定义 ====================
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
class QuickInput(ModelBase):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
name = models.CharField(max_length=100, verbose_name='名')
|
||||
@@ -118,6 +141,11 @@ class WareHouse(ModelBase):
|
||||
merchant = models.ForeignKey('Merchant', on_delete=models.PROTECT, related_name='warehouses', verbose_name='所属商户')
|
||||
name = models.CharField(max_length=100, verbose_name='仓库名称')
|
||||
location = models.CharField(max_length=200, blank=True, null=True, verbose_name='仓库地址')
|
||||
type = models.IntegerField(
|
||||
choices=WarehouseTypeEnum.choices,
|
||||
default=WarehouseTypeEnum.WHOLE,
|
||||
verbose_name='仓库类别',
|
||||
)
|
||||
mode = models.IntegerField(
|
||||
choices=WareHouseModeEnum.choices,
|
||||
default=WareHouseModeEnum.RESTRICT_IN,
|
||||
@@ -311,10 +339,12 @@ class Employee(ModelBase):
|
||||
verbose_name='系统用户',
|
||||
)
|
||||
name = models.CharField(max_length=100, verbose_name='员工姓名')
|
||||
job_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=EmployeeTypeEnum.choices,
|
||||
default=EmployeeTypeEnum.PRINTER,
|
||||
position = models.ForeignKey(
|
||||
'EmployeeType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='employees',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='职位',
|
||||
)
|
||||
mobile = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机')
|
||||
@@ -327,6 +357,17 @@ class Employee(ModelBase):
|
||||
verbose_name='员工状态',
|
||||
)
|
||||
|
||||
@property
|
||||
def job_type(self) -> str:
|
||||
"""
|
||||
返回职位名称(向后兼容)
|
||||
|
||||
如果有关联的职位类型,返回其 title,否则返回空字符串
|
||||
"""
|
||||
if self.position:
|
||||
return self.position.title
|
||||
return ''
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@@ -1,3 +1,166 @@
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import (
|
||||
Merchant, MerchantTypeEnum, WareHouse, WarehouseTypeEnum,
|
||||
WareHouseModeEnum, EmployeeType, Employee, EmployeeStatusEnum
|
||||
)
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class WarehouseTypeTestCase(TestCase):
|
||||
"""测试仓库类型字段"""
|
||||
|
||||
def setUp(self):
|
||||
"""设置测试数据"""
|
||||
self.merchant = Merchant.objects.create(
|
||||
name='测试商户',
|
||||
type=MerchantTypeEnum.STORE
|
||||
)
|
||||
|
||||
def test_warehouse_default_type(self):
|
||||
"""测试仓库类型的默认值"""
|
||||
warehouse = WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='默认仓库',
|
||||
)
|
||||
self.assertEqual(warehouse.type, WarehouseTypeEnum.WHOLE)
|
||||
|
||||
def test_warehouse_type_whole(self):
|
||||
"""测试创建整仓"""
|
||||
warehouse = WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='整仓1',
|
||||
type=WarehouseTypeEnum.WHOLE
|
||||
)
|
||||
self.assertEqual(warehouse.type, WarehouseTypeEnum.WHOLE)
|
||||
self.assertEqual(warehouse.get_type_display(), '整仓')
|
||||
|
||||
def test_warehouse_type_scattered(self):
|
||||
"""测试创建散仓"""
|
||||
warehouse = WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='散仓1',
|
||||
type=WarehouseTypeEnum.SCATTERED
|
||||
)
|
||||
self.assertEqual(warehouse.type, WarehouseTypeEnum.SCATTERED)
|
||||
self.assertEqual(warehouse.get_type_display(), '散仓')
|
||||
|
||||
def test_warehouse_type_enum_values(self):
|
||||
"""测试仓库类型枚举的所有值"""
|
||||
self.assertEqual(WarehouseTypeEnum.WHOLE.value, 1)
|
||||
self.assertEqual(WarehouseTypeEnum.WHOLE.label, '整仓')
|
||||
self.assertEqual(WarehouseTypeEnum.SCATTERED.value, 2)
|
||||
self.assertEqual(WarehouseTypeEnum.SCATTERED.label, '散仓')
|
||||
|
||||
def test_warehouse_with_all_fields(self):
|
||||
"""测试带有所有字段的仓库"""
|
||||
warehouse = WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='完整仓库',
|
||||
type=WarehouseTypeEnum.SCATTERED,
|
||||
mode=WareHouseModeEnum.RESTRICT_IN_OUT,
|
||||
location='测试地址',
|
||||
area='测试区域',
|
||||
contact='张三',
|
||||
mobile='13800138000',
|
||||
description='测试描述'
|
||||
)
|
||||
self.assertEqual(warehouse.type, WarehouseTypeEnum.SCATTERED)
|
||||
self.assertEqual(warehouse.mode, WareHouseModeEnum.RESTRICT_IN_OUT)
|
||||
self.assertEqual(warehouse.name, '完整仓库')
|
||||
|
||||
|
||||
class EmployeeTypeTestCase(TestCase):
|
||||
"""测试员工职位类型"""
|
||||
|
||||
def setUp(self):
|
||||
"""设置测试数据"""
|
||||
self.merchant = Merchant.objects.create(
|
||||
name='测试商户',
|
||||
type=MerchantTypeEnum.STORE
|
||||
)
|
||||
|
||||
def test_create_employee_type(self):
|
||||
"""测试创建员工职位类型"""
|
||||
emp_type = EmployeeType.objects.create(
|
||||
merchant=self.merchant,
|
||||
title='测试职位',
|
||||
description='职位描述'
|
||||
)
|
||||
self.assertEqual(emp_type.title, '测试职位')
|
||||
self.assertEqual(emp_type.description, '职位描述')
|
||||
self.assertEqual(str(emp_type), '测试职位')
|
||||
|
||||
def test_employee_type_unique_together(self):
|
||||
"""测试商户+职位名称的唯一性约束"""
|
||||
EmployeeType.objects.create(
|
||||
merchant=self.merchant,
|
||||
title='打纸'
|
||||
)
|
||||
|
||||
# 同一商户不能创建重复职位
|
||||
with self.assertRaises(Exception):
|
||||
EmployeeType.objects.create(
|
||||
merchant=self.merchant,
|
||||
title='打纸'
|
||||
)
|
||||
|
||||
def test_employee_with_position(self):
|
||||
"""测试员工关联职位"""
|
||||
emp_type = EmployeeType.objects.create(
|
||||
merchant=self.merchant,
|
||||
title='滚筒工'
|
||||
)
|
||||
|
||||
employee = Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='张三',
|
||||
position=emp_type,
|
||||
status=EmployeeStatusEnum.ACTIVE
|
||||
)
|
||||
|
||||
self.assertEqual(employee.position, emp_type)
|
||||
self.assertEqual(employee.job_type, '滚筒工') # 测试 job_type 属性
|
||||
|
||||
def test_employee_without_position(self):
|
||||
"""测试员工没有职位时 job_type 返回空字符串"""
|
||||
employee = Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='李四',
|
||||
status=EmployeeStatusEnum.ACTIVE
|
||||
)
|
||||
|
||||
self.assertIsNone(employee.position)
|
||||
self.assertEqual(employee.job_type, '')
|
||||
|
||||
def test_employee_type_reverse_field(self):
|
||||
"""测试预留字段"""
|
||||
emp_type = EmployeeType.objects.create(
|
||||
merchant=self.merchant,
|
||||
title='仓库管理员',
|
||||
reverse='预留数据'
|
||||
)
|
||||
|
||||
self.assertEqual(emp_type.reverse, '预留数据')
|
||||
|
||||
def test_multiple_employees_same_position(self):
|
||||
"""测试多个员工可以有相同职位"""
|
||||
emp_type = EmployeeType.objects.create(
|
||||
merchant=self.merchant,
|
||||
title='打纸工'
|
||||
)
|
||||
|
||||
emp1 = Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='员工1',
|
||||
position=emp_type
|
||||
)
|
||||
|
||||
emp2 = Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='员工2',
|
||||
position=emp_type
|
||||
)
|
||||
|
||||
self.assertEqual(emp1.job_type, '打纸工')
|
||||
self.assertEqual(emp2.job_type, '打纸工')
|
||||
self.assertEqual(emp_type.employees.count(), 2)
|
||||
|
||||
Reference in New Issue
Block a user