feat: project group can be belonged to many departments

This commit is contained in:
2025-09-22 11:33:18 +08:00
parent 2d6761a436
commit 24256f588c
8 changed files with 71 additions and 104 deletions
+4 -3
View File
@@ -312,10 +312,11 @@ class EmailNotificationMiddleware(MiddlewareMixin):
notification_data = {
'id': project_group_data.get('id', ''),
'name': project_group_data.get('name', ''),
'department': project_group_data.get('department', ''),
'department_name': project_group_data.get('department_name', ''),
'departments': project_group_data.get('departments', ''),
'department_names': project_group_data.get('department_names', ''),
'departments_info': project_group_data.get('departments_info', ''),
'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_method': request.method
}
+10 -1
View File
@@ -66,6 +66,7 @@ LABEL_MAP = {
'grade_major': '年级专业',
'project_group': '项目组',
'project_group_name': '项目组',
'department_names': '所属部门',
'position': '职位',
'start_date': '开始日期',
'end_date': '结束日期',
@@ -91,7 +92,15 @@ def _format_value(value):
return str(value)
if isinstance(value, bool):
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:
return json.dumps(value, ensure_ascii=False)
except Exception:
+10 -69
View File
@@ -6,12 +6,18 @@ from .models import Personnel, ProjectGroup
@admin.register(ProjectGroup)
class ProjectGroupAdmin(admin.ModelAdmin):
"""项目组管理"""
list_display = ['name', 'department', 'description', 'created_at']
list_filter = ['department', 'created_at']
list_display = ['name', 'get_departments', 'description', 'created_at']
list_filter = ['departments', 'created_at']
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)
class PersonnelAdmin(admin.ModelAdmin):
"""人员信息管理"""
@@ -51,68 +57,3 @@ class PersonnelAdmin(admin.ModelAdmin):
def get_queryset(self, request):
"""优化查询"""
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']
+9 -3
View File
@@ -6,17 +6,23 @@ from finance.models import Department
class ProjectGroup(models.Model):
"""项目组模型"""
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="项目组描述")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
class Meta:
verbose_name = "项目组"
verbose_name_plural = verbose_name
ordering = ['department', 'name']
ordering = ['name']
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):
+12 -5
View File
@@ -6,13 +6,18 @@ from .models import Personnel, ProjectGroup
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:
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']
def get_departments_info(self, obj):
"""获取部门详细信息"""
return [{'id': dept.id, 'name': dept.name} for dept in obj.departments.all()]
class PersonnelReadSerializer(serializers.ModelSerializer):
"""人员信息读取序列化器"""
@@ -45,11 +50,13 @@ class PersonnelWriteSerializer(serializers.ModelSerializer):
]
def validate_project_group(self, value):
"""验证项目组是否属于选定的部门"""
"""验证项目组是否包含选定的部门"""
if value and hasattr(self, 'initial_data'):
department_id = self.initial_data.get('department')
if department_id and value.department_id != int(department_id):
raise serializers.ValidationError("项目组必须属于选定的部门")
if department_id:
# 检查项目组的部门列表中是否包含选定的部门
if not value.departments.filter(id=int(department_id)).exists():
raise serializers.ValidationError("项目组必须包含选定的部门")
return value
def validate(self, attrs):
+4 -14
View File
@@ -197,16 +197,16 @@ class ProjectGroupViewSet(viewsets.ModelViewSet):
queryset = ProjectGroup.objects.all()
serializer_class = ProjectGroupSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['department']
filterset_fields = ['departments']
search_fields = ['name', 'description']
ordering = ['department', 'name']
ordering = ['name']
@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)
project_groups = ProjectGroup.objects.filter(departments__id=department_id).distinct()
serializer = self.get_serializer(project_groups, many=True)
return Response(serializer.data)
return Response([])
@@ -221,7 +221,7 @@ class ProjectGroupViewSet(viewsets.ModelViewSet):
project_group_data = {
'id': project_group.id,
'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,
'created_at': str(project_group.created_at),
'operation_path': request.path,
@@ -267,13 +267,3 @@ class ProjectGroupViewSet(viewsets.ModelViewSet):
else:
ip = request.META.get('REMOTE_ADDR')
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-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 label="操作" width="150">
<template #default="scope">
@@ -86,8 +92,8 @@
<el-form-item label="项目组名称" prop="name">
<el-input v-model="projectGroupForm.name" placeholder="请输入项目组名称" />
</el-form-item>
<el-form-item label="所属部门" prop="department">
<el-select v-model="projectGroupForm.department" placeholder="请选择所属部门">
<el-form-item label="所属部门" prop="departments">
<el-select v-model="projectGroupForm.departments" placeholder="请选择所属部门" multiple>
<el-option
v-for="dept in departments"
:key="dept.id"
@@ -152,14 +158,14 @@ export default {
projectGroupForm: {
id: null,
name: '',
department: '',
departments: [],
description: ''
},
projectGroupRules: {
name: [
{ required: true, message: '请输入项目组名称', trigger: 'blur' }
],
department: [
departments: [
{ required: true, message: '请选择所属部门', trigger: 'change' }
]
}
@@ -170,7 +176,9 @@ export default {
if (!this.departmentFilter) {
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() {
@@ -270,13 +278,16 @@ export default {
// 项目组管理方法
showAddProjectGroupDialog() {
this.projectGroupForm = { id: null, name: '', department: '', description: '' }
this.projectGroupForm = { id: null, name: '', departments: [], description: '' }
this.projectGroupDialogTitle = '添加项目组'
this.projectGroupDialogVisible = true
},
editProjectGroup(projectGroup) {
this.projectGroupForm = { ...projectGroup }
this.projectGroupForm = {
...projectGroup,
departments: projectGroup.departments || []
}
this.projectGroupDialogTitle = '编辑项目组'
this.projectGroupDialogVisible = true
},
+3 -1
View File
@@ -209,7 +209,9 @@ export default {
return []
}
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: {