1
0
forked from erp-dev/erp

feat: added type field into warehouse model

This commit is contained in:
2025-11-20 15:44:35 +08:00
parent 6091280fdc
commit 2a838199fa
22 changed files with 1384 additions and 38 deletions

View File

@@ -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 []

View 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='仓库类别'),
),
]

View File

@@ -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='职位'),
),
]

View File

@@ -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

View File

@@ -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)