from odoo import api, models class ProjectProject(models.Model): _inherit = 'project.project' @api.model def get_mindmap_tree(self, single_id=None): """返回 项目→阶段→任务 的树形结构(dict),供 mindmap 视图渲染。 不建中间表:直接利用已有的 project.project.type_ids(阶段)/ project.task.project_id / project.task.stage_id。 字段是否存在在服务端用 _fields 直接判断,杜绝 JS 侧写死字段导致的 Invalid field / 白屏。 """ self = self.sudo() # 1) 项目(单项目视角则只取当前项目) proj_domain = [('id', '=', single_id)] if single_id else [] projects = self.search_read(proj_domain, ['name', 'type_ids']) # 2) 阶段:经各项目 type_ids 收集、保序去重 stage_ids = [] for p in projects: stage_ids += p['type_ids'] stage_ids = list(dict.fromkeys(stage_ids)) stages = self.env['project.task.type'].search_read( [('id', 'in', stage_ids)], ['name', 'sequence'] ) if stage_ids else [] stage_by_id = {s['id']: s for s in stages} # 3) 任务:进度字段在部分版本/客制下不存在,服务端安全探测 task_fields = ['name', 'display_name', 'project_id', 'stage_id'] tk = self.env['project.task'] progress_field = None if 'subtask_completion_percentage' in tk._fields: progress_field = 'subtask_completion_percentage' elif 'progress' in tk._fields: progress_field = 'progress' if progress_field: task_fields.append(progress_field) task_domain = [('project_id', '=', single_id)] if single_id else [] tasks = tk.search_read(task_domain, task_fields) def _norm(v): if v is None or v is False: return 0 try: v = float(v) except (TypeError, ValueError): return 0 # 部分字段存 0-1,有的存 0-100;统一归一到 0-100 return int(v) if v > 1 else int(v * 100) project_nodes = {} for p in projects: project_nodes[p['id']] = { 'id': 'P%d' % p['id'], 'name': p['name'] or ('项目%s' % p['id']), 'kind': 'project', 'progress': 0, 'children': [], } for p in projects: pn = project_nodes[p['id']] proj_stages = [stage_by_id[i] for i in p['type_ids'] if i in stage_by_id] proj_stages.sort(key=lambda s: s.get('sequence') or 0) for st in proj_stages: sn = { 'id': 'P%d_S%d' % (p['id'], st['id']), 'name': st['name'], 'kind': 'stage', 'progress': 0, 'children': [], } pn['children'].append(sn) st_tasks = [ t for t in tasks if t.get('project_id') and t['project_id'][0] == p['id'] and t.get('stage_id') and t['stage_id'][0] == st['id'] ] for t in st_tasks: raw = t.get(progress_field) if progress_field else None sn['children'].append({ 'id': 'T%d' % t['id'], 'name': t.get('display_name') or t['name'], 'kind': 'task', 'resId': t['id'], 'progress': _norm(raw), }) sn['progress'] = round( sum(c['progress'] for c in sn['children']) / len(sn['children']) ) if sn['children'] else 0 pn['progress'] = round( sum(c['progress'] for c in pn['children']) / len(pn['children']) ) if pn['children'] else 0 roots = list(project_nodes.values()) if single_id and len(roots) == 1: return roots[0] return { 'id': 0, 'name': '所有项目' if not single_id else '项目阶段与任务', 'children': roots, }