feat: project group can be belonged to many departments
This commit is contained in:
@@ -312,10 +312,11 @@ class EmailNotificationMiddleware(MiddlewareMixin):
|
|||||||
notification_data = {
|
notification_data = {
|
||||||
'id': project_group_data.get('id', ''),
|
'id': project_group_data.get('id', ''),
|
||||||
'name': project_group_data.get('name', ''),
|
'name': project_group_data.get('name', ''),
|
||||||
'department': project_group_data.get('department', ''),
|
'departments': project_group_data.get('departments', ''),
|
||||||
'department_name': project_group_data.get('department_name', ''),
|
'department_names': project_group_data.get('department_names', ''),
|
||||||
|
'departments_info': project_group_data.get('departments_info', ''),
|
||||||
'description': project_group_data.get('description', ''),
|
'description': project_group_data.get('description', ''),
|
||||||
'timestamp': project_group_data.get('updated_at', ''),
|
'timestamp': project_group_data.get('created_at', ''),
|
||||||
'operation_path': request.path,
|
'operation_path': request.path,
|
||||||
'operation_method': request.method
|
'operation_method': request.method
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ LABEL_MAP = {
|
|||||||
'grade_major': '年级专业',
|
'grade_major': '年级专业',
|
||||||
'project_group': '项目组',
|
'project_group': '项目组',
|
||||||
'project_group_name': '项目组',
|
'project_group_name': '项目组',
|
||||||
|
'department_names': '所属部门',
|
||||||
'position': '职位',
|
'position': '职位',
|
||||||
'start_date': '开始日期',
|
'start_date': '开始日期',
|
||||||
'end_date': '结束日期',
|
'end_date': '结束日期',
|
||||||
@@ -91,7 +92,15 @@ def _format_value(value):
|
|||||||
return str(value)
|
return str(value)
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
return '是' if value else '否'
|
return '是' if value else '否'
|
||||||
if isinstance(value, (list, dict)):
|
if isinstance(value, list):
|
||||||
|
# 对于列表,优先显示为逗号分隔的字符串
|
||||||
|
if all(isinstance(item, str) for item in value):
|
||||||
|
return ', '.join(value)
|
||||||
|
try:
|
||||||
|
return json.dumps(value, ensure_ascii=False)
|
||||||
|
except Exception:
|
||||||
|
return str(value)
|
||||||
|
if isinstance(value, dict):
|
||||||
try:
|
try:
|
||||||
return json.dumps(value, ensure_ascii=False)
|
return json.dumps(value, ensure_ascii=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ from .models import Personnel, ProjectGroup
|
|||||||
@admin.register(ProjectGroup)
|
@admin.register(ProjectGroup)
|
||||||
class ProjectGroupAdmin(admin.ModelAdmin):
|
class ProjectGroupAdmin(admin.ModelAdmin):
|
||||||
"""项目组管理"""
|
"""项目组管理"""
|
||||||
list_display = ['name', 'department', 'description', 'created_at']
|
list_display = ['name', 'get_departments', 'description', 'created_at']
|
||||||
list_filter = ['department', 'created_at']
|
list_filter = ['departments', 'created_at']
|
||||||
search_fields = ['name', 'description']
|
search_fields = ['name', 'description']
|
||||||
ordering = ['department', 'name']
|
ordering = ['name']
|
||||||
|
filter_horizontal = ['departments']
|
||||||
|
|
||||||
|
def get_departments(self, obj):
|
||||||
|
"""获取部门列表显示"""
|
||||||
|
return ", ".join([dept.name for dept in obj.departments.all()])
|
||||||
|
get_departments.short_description = '所属部门'
|
||||||
|
|
||||||
|
|
||||||
import django_filters
|
|
||||||
@admin.register(Personnel)
|
@admin.register(Personnel)
|
||||||
class PersonnelAdmin(admin.ModelAdmin):
|
class PersonnelAdmin(admin.ModelAdmin):
|
||||||
"""人员信息管理"""
|
"""人员信息管理"""
|
||||||
@@ -51,68 +57,3 @@ class PersonnelAdmin(admin.ModelAdmin):
|
|||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
"""优化查询"""
|
"""优化查询"""
|
||||||
return super().get_queryset(request).select_related('department', 'project_group')
|
return super().get_queryset(request).select_related('department', 'project_group')
|
||||||
from .models import Personnel, ProjectGroup
|
|
||||||
from finance.models import Department
|
|
||||||
|
|
||||||
|
|
||||||
class PersonnelFilter(django_filters.FilterSet):
|
|
||||||
"""人员信息过滤器"""
|
|
||||||
|
|
||||||
# 基本筛选
|
|
||||||
department = django_filters.ModelChoiceFilter(
|
|
||||||
queryset=Department.objects.all(),
|
|
||||||
field_name='department',
|
|
||||||
label='部门'
|
|
||||||
)
|
|
||||||
|
|
||||||
project_group = django_filters.ModelChoiceFilter(
|
|
||||||
queryset=ProjectGroup.objects.all(),
|
|
||||||
field_name='project_group',
|
|
||||||
label='项目组'
|
|
||||||
)
|
|
||||||
|
|
||||||
position = django_filters.CharFilter(
|
|
||||||
field_name='position',
|
|
||||||
lookup_expr='icontains',
|
|
||||||
label='职位'
|
|
||||||
)
|
|
||||||
|
|
||||||
gender = django_filters.ChoiceFilter(
|
|
||||||
choices=Personnel.GENDER_CHOICES,
|
|
||||||
field_name='gender',
|
|
||||||
label='性别'
|
|
||||||
)
|
|
||||||
|
|
||||||
is_active = django_filters.BooleanFilter(
|
|
||||||
field_name='is_active',
|
|
||||||
label='在职状态'
|
|
||||||
)
|
|
||||||
|
|
||||||
# 日期范围筛选
|
|
||||||
start_date_from = django_filters.DateFilter(
|
|
||||||
field_name='start_date',
|
|
||||||
lookup_expr='gte',
|
|
||||||
label='任职开始时间(从)'
|
|
||||||
)
|
|
||||||
|
|
||||||
start_date_to = django_filters.DateFilter(
|
|
||||||
field_name='start_date',
|
|
||||||
lookup_expr='lte',
|
|
||||||
label='任职开始时间(到)'
|
|
||||||
)
|
|
||||||
|
|
||||||
end_date_from = django_filters.DateFilter(
|
|
||||||
field_name='end_date',
|
|
||||||
lookup_expr='gte',
|
|
||||||
label='任职结束时间(从)'
|
|
||||||
)
|
|
||||||
|
|
||||||
end_date_to = django_filters.DateFilter(
|
|
||||||
field_name='end_date',
|
|
||||||
lookup_expr='lte',
|
|
||||||
label='任职结束时间(到)'
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Personnel
|
|
||||||
fields = ['department', 'project_group', 'position', 'gender', 'is_active']
|
|
||||||
|
|||||||
@@ -6,17 +6,23 @@ from finance.models import Department
|
|||||||
class ProjectGroup(models.Model):
|
class ProjectGroup(models.Model):
|
||||||
"""项目组模型"""
|
"""项目组模型"""
|
||||||
name = models.CharField(max_length=100, verbose_name="项目组名称")
|
name = models.CharField(max_length=100, verbose_name="项目组名称")
|
||||||
department = models.ForeignKey(Department, on_delete=models.CASCADE, verbose_name="所属部门")
|
departments = models.ManyToManyField(Department, verbose_name="所属部门", related_name="project_groups")
|
||||||
description = models.TextField(blank=True, null=True, verbose_name="项目组描述")
|
description = models.TextField(blank=True, null=True, verbose_name="项目组描述")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "项目组"
|
verbose_name = "项目组"
|
||||||
verbose_name_plural = verbose_name
|
verbose_name_plural = verbose_name
|
||||||
ordering = ['department', 'name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.department.name} - {self.name}"
|
department_names = ", ".join([dept.name for dept in self.departments.all()])
|
||||||
|
return f"{self.name} ({department_names})" if department_names else self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def department_names(self):
|
||||||
|
"""返回所属部门名称列表"""
|
||||||
|
return [dept.name for dept in self.departments.all()]
|
||||||
|
|
||||||
|
|
||||||
class Personnel(models.Model):
|
class Personnel(models.Model):
|
||||||
|
|||||||
@@ -6,13 +6,18 @@ from .models import Personnel, ProjectGroup
|
|||||||
|
|
||||||
class ProjectGroupSerializer(serializers.ModelSerializer):
|
class ProjectGroupSerializer(serializers.ModelSerializer):
|
||||||
"""项目组序列化器"""
|
"""项目组序列化器"""
|
||||||
department_name = serializers.CharField(source='department.name', read_only=True)
|
department_names = serializers.ListField(read_only=True)
|
||||||
|
departments_info = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectGroup
|
model = ProjectGroup
|
||||||
fields = ['id', 'name', 'department', 'department_name', 'description', 'created_at']
|
fields = ['id', 'name', 'departments', 'department_names', 'departments_info', 'description', 'created_at']
|
||||||
read_only_fields = ['created_at']
|
read_only_fields = ['created_at']
|
||||||
|
|
||||||
|
def get_departments_info(self, obj):
|
||||||
|
"""获取部门详细信息"""
|
||||||
|
return [{'id': dept.id, 'name': dept.name} for dept in obj.departments.all()]
|
||||||
|
|
||||||
|
|
||||||
class PersonnelReadSerializer(serializers.ModelSerializer):
|
class PersonnelReadSerializer(serializers.ModelSerializer):
|
||||||
"""人员信息读取序列化器"""
|
"""人员信息读取序列化器"""
|
||||||
@@ -45,11 +50,13 @@ class PersonnelWriteSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def validate_project_group(self, value):
|
def validate_project_group(self, value):
|
||||||
"""验证项目组是否属于选定的部门"""
|
"""验证项目组是否包含选定的部门"""
|
||||||
if value and hasattr(self, 'initial_data'):
|
if value and hasattr(self, 'initial_data'):
|
||||||
department_id = self.initial_data.get('department')
|
department_id = self.initial_data.get('department')
|
||||||
if department_id and value.department_id != int(department_id):
|
if department_id:
|
||||||
raise serializers.ValidationError("项目组必须属于选定的部门")
|
# 检查项目组的部门列表中是否包含选定的部门
|
||||||
|
if not value.departments.filter(id=int(department_id)).exists():
|
||||||
|
raise serializers.ValidationError("项目组必须包含选定的部门")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
|||||||
@@ -197,16 +197,16 @@ class ProjectGroupViewSet(viewsets.ModelViewSet):
|
|||||||
queryset = ProjectGroup.objects.all()
|
queryset = ProjectGroup.objects.all()
|
||||||
serializer_class = ProjectGroupSerializer
|
serializer_class = ProjectGroupSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['department']
|
filterset_fields = ['departments']
|
||||||
search_fields = ['name', 'description']
|
search_fields = ['name', 'description']
|
||||||
ordering = ['department', 'name']
|
ordering = ['name']
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def by_department(self, request):
|
def by_department(self, request):
|
||||||
"""按部门获取项目组"""
|
"""按部门获取项目组"""
|
||||||
department_id = request.query_params.get('department_id')
|
department_id = request.query_params.get('department_id')
|
||||||
if department_id:
|
if department_id:
|
||||||
project_groups = ProjectGroup.objects.filter(department_id=department_id)
|
project_groups = ProjectGroup.objects.filter(departments__id=department_id).distinct()
|
||||||
serializer = self.get_serializer(project_groups, many=True)
|
serializer = self.get_serializer(project_groups, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
return Response([])
|
return Response([])
|
||||||
@@ -221,7 +221,7 @@ class ProjectGroupViewSet(viewsets.ModelViewSet):
|
|||||||
project_group_data = {
|
project_group_data = {
|
||||||
'id': project_group.id,
|
'id': project_group.id,
|
||||||
'name': "[已删除]" + project_group.name,
|
'name': "[已删除]" + project_group.name,
|
||||||
'department_name': project_group.department.name if project_group.department else '',
|
'department_names': project_group.department_names,
|
||||||
'description': project_group.description,
|
'description': project_group.description,
|
||||||
'created_at': str(project_group.created_at),
|
'created_at': str(project_group.created_at),
|
||||||
'operation_path': request.path,
|
'operation_path': request.path,
|
||||||
@@ -267,13 +267,3 @@ class ProjectGroupViewSet(viewsets.ModelViewSet):
|
|||||||
else:
|
else:
|
||||||
ip = request.META.get('REMOTE_ADDR')
|
ip = request.META.get('REMOTE_ADDR')
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
|
||||||
def by_department(self, request):
|
|
||||||
"""按部门获取项目组"""
|
|
||||||
department_id = request.query_params.get('department_id')
|
|
||||||
if department_id:
|
|
||||||
project_groups = ProjectGroup.objects.filter(department_id=department_id)
|
|
||||||
serializer = self.get_serializer(project_groups, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
return Response([])
|
|
||||||
|
|||||||
@@ -54,7 +54,13 @@
|
|||||||
|
|
||||||
<el-table :data="filteredProjectGroups" style="width: 100%" v-loading="projectGroupLoading">
|
<el-table :data="filteredProjectGroups" style="width: 100%" v-loading="projectGroupLoading">
|
||||||
<el-table-column prop="name" label="项目组名称" />
|
<el-table-column prop="name" label="项目组名称" />
|
||||||
<el-table-column prop="department_name" label="所属部门" />
|
<el-table-column label="所属部门" min-width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-for="dept in scope.row.departments_info" :key="dept.id" size="small" style="margin-right: 5px;">
|
||||||
|
{{ dept.name }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="description" label="描述" show-overflow-tooltip />
|
<el-table-column prop="description" label="描述" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="150">
|
<el-table-column label="操作" width="150">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -86,8 +92,8 @@
|
|||||||
<el-form-item label="项目组名称" prop="name">
|
<el-form-item label="项目组名称" prop="name">
|
||||||
<el-input v-model="projectGroupForm.name" placeholder="请输入项目组名称" />
|
<el-input v-model="projectGroupForm.name" placeholder="请输入项目组名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="所属部门" prop="department">
|
<el-form-item label="所属部门" prop="departments">
|
||||||
<el-select v-model="projectGroupForm.department" placeholder="请选择所属部门">
|
<el-select v-model="projectGroupForm.departments" placeholder="请选择所属部门" multiple>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="dept in departments"
|
v-for="dept in departments"
|
||||||
:key="dept.id"
|
:key="dept.id"
|
||||||
@@ -152,14 +158,14 @@ export default {
|
|||||||
projectGroupForm: {
|
projectGroupForm: {
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
department: '',
|
departments: [],
|
||||||
description: ''
|
description: ''
|
||||||
},
|
},
|
||||||
projectGroupRules: {
|
projectGroupRules: {
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: '请输入项目组名称', trigger: 'blur' }
|
{ required: true, message: '请输入项目组名称', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
department: [
|
departments: [
|
||||||
{ required: true, message: '请选择所属部门', trigger: 'change' }
|
{ required: true, message: '请选择所属部门', trigger: 'change' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -170,7 +176,9 @@ export default {
|
|||||||
if (!this.departmentFilter) {
|
if (!this.departmentFilter) {
|
||||||
return this.projectGroups
|
return this.projectGroups
|
||||||
}
|
}
|
||||||
return this.projectGroups.filter(group => group.department === parseInt(this.departmentFilter))
|
return this.projectGroups.filter(group =>
|
||||||
|
group.departments && group.departments.includes(parseInt(this.departmentFilter))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -270,13 +278,16 @@ export default {
|
|||||||
|
|
||||||
// 项目组管理方法
|
// 项目组管理方法
|
||||||
showAddProjectGroupDialog() {
|
showAddProjectGroupDialog() {
|
||||||
this.projectGroupForm = { id: null, name: '', department: '', description: '' }
|
this.projectGroupForm = { id: null, name: '', departments: [], description: '' }
|
||||||
this.projectGroupDialogTitle = '添加项目组'
|
this.projectGroupDialogTitle = '添加项目组'
|
||||||
this.projectGroupDialogVisible = true
|
this.projectGroupDialogVisible = true
|
||||||
},
|
},
|
||||||
|
|
||||||
editProjectGroup(projectGroup) {
|
editProjectGroup(projectGroup) {
|
||||||
this.projectGroupForm = { ...projectGroup }
|
this.projectGroupForm = {
|
||||||
|
...projectGroup,
|
||||||
|
departments: projectGroup.departments || []
|
||||||
|
}
|
||||||
this.projectGroupDialogTitle = '编辑项目组'
|
this.projectGroupDialogTitle = '编辑项目组'
|
||||||
this.projectGroupDialogVisible = true
|
this.projectGroupDialogVisible = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ export default {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const departmentId = parseInt(this.form.department)
|
const departmentId = parseInt(this.form.department)
|
||||||
return this.projectGroups.filter(group => group.department === departmentId)
|
return this.projectGroups.filter(group =>
|
||||||
|
group.departments && group.departments.includes(departmentId)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
Reference in New Issue
Block a user