107 lines
4.2 KiB
Python
107 lines
4.2 KiB
Python
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,
|
||
}
|