169 lines
6.7 KiB
Python
169 lines
6.7 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
from odoo import models, fields, api
|
||
|
|
import logging
|
||
|
|
|
||
|
|
_logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
class Task(models.Model):
|
||
|
|
_inherit = 'project.task'
|
||
|
|
|
||
|
|
# --- Fields ---
|
||
|
|
weight = fields.Float(string="Weight (%)", default=0.0, tracking=True)
|
||
|
|
is_manual_weight = fields.Boolean(string="Manual Weight?", default=False)
|
||
|
|
progress = fields.Float(string="Progress (%)", default=0.0, tracking=True)
|
||
|
|
contribution = fields.Float(string="Contribution", compute="_compute_contribution")
|
||
|
|
|
||
|
|
# Task Completed checkbox (ADJUST FIELD NAME IF DIFFERENT)
|
||
|
|
task_completed = fields.Boolean(string="Task Completed", default=False, tracking=True)
|
||
|
|
|
||
|
|
# --- Compute Methods ---
|
||
|
|
|
||
|
|
@api.depends('progress', 'weight')
|
||
|
|
def _compute_contribution(self):
|
||
|
|
for task in self:
|
||
|
|
task.contribution = (task.progress or 0.0) * (task.weight or 0.0) / 100.0
|
||
|
|
|
||
|
|
# --- Core Logic ---
|
||
|
|
|
||
|
|
def _rebalance_weights(self, changed_task):
|
||
|
|
"""Rebalance weights among siblings"""
|
||
|
|
if changed_task.parent_id:
|
||
|
|
siblings = changed_task.parent_id.child_ids
|
||
|
|
else:
|
||
|
|
siblings = changed_task.project_id.task_ids.filtered(lambda t: not t.parent_id)
|
||
|
|
|
||
|
|
if len(siblings) == 1:
|
||
|
|
siblings.with_context(bypass_weight_balance=True).write({
|
||
|
|
'weight': 100.0,
|
||
|
|
'is_manual_weight': False
|
||
|
|
})
|
||
|
|
return
|
||
|
|
|
||
|
|
manual_tasks = siblings.filtered(lambda t: t.is_manual_weight and t != changed_task)
|
||
|
|
auto_tasks = siblings.filtered(lambda t: not t.is_manual_weight)
|
||
|
|
|
||
|
|
total_manual_weight = sum(manual_tasks.mapped('weight'))
|
||
|
|
if changed_task.is_manual_weight:
|
||
|
|
total_manual_weight += changed_task.weight
|
||
|
|
|
||
|
|
remaining_for_auto = 100.0 - total_manual_weight
|
||
|
|
new_auto_weight = max(0.0, remaining_for_auto / len(auto_tasks)) if auto_tasks else 0.0
|
||
|
|
|
||
|
|
if auto_tasks:
|
||
|
|
auto_tasks.with_context(bypass_weight_balance=True).write({
|
||
|
|
'weight': new_auto_weight,
|
||
|
|
'is_manual_weight': False
|
||
|
|
})
|
||
|
|
|
||
|
|
def _rollup_progress(self):
|
||
|
|
"""
|
||
|
|
Roll up progress from subtasks to parent tasks.
|
||
|
|
Call this on subtasks to update their parents.
|
||
|
|
"""
|
||
|
|
for task in self:
|
||
|
|
if task.parent_id:
|
||
|
|
parent = task.parent_id
|
||
|
|
siblings = parent.child_ids
|
||
|
|
|
||
|
|
if siblings:
|
||
|
|
# Calculate average progress of all subtasks
|
||
|
|
new_progress = sum(s.progress for s in siblings) / len(siblings)
|
||
|
|
|
||
|
|
# Update parent progress (with bypass to prevent recursion)
|
||
|
|
if abs(parent.progress - new_progress) > 0.01:
|
||
|
|
parent.with_context(bypass_weight_balance=True).write({
|
||
|
|
'progress': new_progress
|
||
|
|
})
|
||
|
|
|
||
|
|
# Continue rolling up to grandparent
|
||
|
|
parent._rollup_progress()
|
||
|
|
|
||
|
|
# --- Overrides ---
|
||
|
|
|
||
|
|
@api.model_create_multi
|
||
|
|
def create(self, vals_list):
|
||
|
|
_logger.info(f"CREATE called with {len(vals_list)} tasks")
|
||
|
|
|
||
|
|
tasks = super(Task, self).create(vals_list)
|
||
|
|
|
||
|
|
for task, vals in zip(tasks, vals_list):
|
||
|
|
# Initialize weight if not set
|
||
|
|
if 'weight' not in vals:
|
||
|
|
task._rebalance_weights(task)
|
||
|
|
|
||
|
|
# If this is a subtask, roll up progress to parent
|
||
|
|
if task.parent_id:
|
||
|
|
task._rollup_progress()
|
||
|
|
|
||
|
|
_logger.info(f"CREATE completed for tasks: {[t.name for t in tasks]}")
|
||
|
|
return tasks
|
||
|
|
|
||
|
|
def write(self, vals):
|
||
|
|
_logger.info(f"WRITE called on {self.mapped('name')} with {vals}")
|
||
|
|
_logger.info(f"Bypass context: {self.env.context.get('bypass_weight_balance')}")
|
||
|
|
|
||
|
|
# CRITICAL: Check bypass flag FIRST to prevent recursion
|
||
|
|
if self.env.context.get('bypass_weight_balance'):
|
||
|
|
_logger.info("WRITE bypassed due to context flag")
|
||
|
|
return super(Task, self).write(vals)
|
||
|
|
|
||
|
|
is_weight_change = 'weight' in vals
|
||
|
|
is_progress_change = 'progress' in vals
|
||
|
|
is_completion_change = 'task_completed' in vals
|
||
|
|
is_stage_change = 'stage_id' in vals
|
||
|
|
|
||
|
|
res = super(Task, self).write(vals)
|
||
|
|
|
||
|
|
for task in self:
|
||
|
|
_logger.info(f"Processing task: {task.name}, completion: {task.task_completed}")
|
||
|
|
|
||
|
|
# 1. HANDLE TASK COMPLETION → Set Progress to 100%
|
||
|
|
if is_completion_change:
|
||
|
|
if task.task_completed:
|
||
|
|
_logger.info(f"Task {task.name} marked complete, setting progress to 100%")
|
||
|
|
task.with_context(bypass_weight_balance=True).write({
|
||
|
|
'progress': 100.0
|
||
|
|
})
|
||
|
|
else:
|
||
|
|
# If unchecked and no subtasks, reset progress to 0
|
||
|
|
if not task.child_ids:
|
||
|
|
task.with_context(bypass_weight_balance=True).write({
|
||
|
|
'progress': 0.0
|
||
|
|
})
|
||
|
|
|
||
|
|
# 2. HANDLE STAGE CHANGE → If stage is "Done", set progress to 100%
|
||
|
|
if is_stage_change:
|
||
|
|
if task.stage_id and task.stage_id.name.lower() in ['done', 'completed', 'closed']:
|
||
|
|
task.with_context(bypass_weight_balance=True).write({
|
||
|
|
'progress': 100.0
|
||
|
|
})
|
||
|
|
elif not task.child_ids:
|
||
|
|
task.with_context(bypass_weight_balance=True).write({
|
||
|
|
'progress': 0.0
|
||
|
|
})
|
||
|
|
|
||
|
|
# 3. Handle weight changes
|
||
|
|
if is_weight_change:
|
||
|
|
task.is_manual_weight = True
|
||
|
|
task._rebalance_weights(task)
|
||
|
|
|
||
|
|
# 4. Handle progress rollup (if task has children)
|
||
|
|
# This ensures parent task progress updates when subtasks change
|
||
|
|
if task.child_ids and (is_progress_change or is_weight_change or is_completion_change or is_stage_change):
|
||
|
|
children = task.child_ids
|
||
|
|
if children:
|
||
|
|
calc_progress = sum(c.progress for c in children) / len(children)
|
||
|
|
_logger.info(f"Task {task.name} has {len(children)} children, calculated progress: {calc_progress}")
|
||
|
|
if abs(task.progress - calc_progress) > 0.01:
|
||
|
|
task.with_context(bypass_weight_balance=True).write({
|
||
|
|
'progress': calc_progress
|
||
|
|
})
|
||
|
|
|
||
|
|
# 5. Roll up progress to parent (CRITICAL FOR YOUR ISSUE)
|
||
|
|
if (is_progress_change or is_completion_change or is_stage_change) and task.parent_id:
|
||
|
|
_logger.info(f"Rolling up progress from {task.name} to parent {task.parent_id.name}")
|
||
|
|
task._rollup_progress()
|
||
|
|
|
||
|
|
_logger.info("WRITE completed")
|
||
|
|
return res
|