from datetime import timedelta from odoo import models, fields, api class ProjectTask(models.Model): _inherit = 'project.task' # 1. Type of KPI type_of_kpi = fields.Selection([ ('percentage', 'Percentage'), ('number', 'Number'), ('other', 'Other') ], string='KPI Type', default='number') # 2. Total Number KPI (The Target) total_number_kpi = fields.Float(string='KPI Target', default=0.0) # 3. Number of KPI (The Achieved Result) # This is the main field used for storage number_of_kpi = fields.Float(string='KPI Achieved', default=0.0) # 4. Dateline of KPI dateline_of_kpi = fields.Date(string='KPI Deadline') # 5. Computed Field: Sum of Sub-tasks KPI # This shows what the total would be based on children kpi_achieved_subtask_sum = fields.Float( string='KPI Sum from Subtasks', compute='_compute_kpi_achieved_subtask_sum', store=True ) # 6. Computed Field: Achievement Rate kpi_achievement_rate = fields.Float( string='KPI Achievement (%)', compute='_compute_kpi_achievement_rate', store=True ) @api.depends('child_ids.number_of_kpi') def _compute_kpi_achieved_subtask_sum(self): """Sums the number_of_kpi from all direct sub-tasks""" for task in self: task.kpi_achieved_subtask_sum = sum(task.child_ids.mapped('number_of_kpi')) @api.depends('number_of_kpi', 'total_number_kpi') def _compute_kpi_achievement_rate(self): for task in self: if task.total_number_kpi > 0: task.kpi_achievement_rate = (task.number_of_kpi / task.total_number_kpi) * 100 else: task.kpi_achievement_rate = 0.0 @api.model_create_multi def create(self, vals_list): for vals in vals_list: if vals.get('ks_schedule_mode') == 'auto' and vals.get('ks_constraint_task_type') in ['asap', 'alap']: # Example: Set start date to today if auto mode if not vals.get('date_start'): vals['date_start'] = fields.Datetime.now() # Example: Auto-calculate deadline based on constraint if vals.get('ks_constraint_task_type') == 'asap': vals['date_deadline'] = vals.get('date_start') + timedelta(days=7) elif vals.get('ks_constraint_task_type') == 'alap': # ALAP logic here pass return super().create(vals_list) def write(self, vals): """Override write to update parent when child KPI changes""" res = super().write(vals) # Check if number_of_kpi was updated in this write operation if 'number_of_kpi' in vals: for task in self: if task.parent_id: # Update the parent's KPI based on all its children task.parent_id._update_kpi_from_children() return res def _update_kpi_from_children(self): """Calculates the sum of children KPI and updates the parent's number_of_kpi""" # Ensure we don't trigger infinite recursion by using a specific context or logic # Here we directly update the field via sudo to bypass some checks if needed, # but standard write is safer. if self.child_ids: total_achieved = sum(self.child_ids.mapped('number_of_kpi')) # Only update if different to avoid unnecessary triggers if self.number_of_kpi != total_achieved: # We use super().write to avoid triggering this same method again recursively # However, since we check 'number_of_kpi' in vals, we need to be careful. # To prevent recursion, we can check if the caller is already updating. # Simplest way in Odoo for this case: self.write({'number_of_kpi': total_achieved}) # Note: The write above will trigger this method again for the Grandparent. # This is desired behavior (cascade update up the tree). # But we must ensure it doesn't trigger for the children again. # Since we only check 'if task.parent_id', and the parent doesn't have a parent_id relative to children, # it won't loop back down.