from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError from markupsafe import Markup from datetime import datetime, timedelta import pytz class ProjectProject(models.Model): _inherit = 'project.project' sequence_name = fields.Char("Project Number", copy=False, readonly=True) task_sequence_id = fields.Many2one( 'ir.sequence', string="Task Sequence", readonly=True, copy=False, help="Sequence for tasks of this project" ) discuss_channel_id = fields.Many2one( 'discuss.channel', string="Channel", domain="[('parent_channel_id', '=', default_projects_channel_id)]", help="Select a channel for project communications. Channels must be sub-channels of the main Projects Channel." ) default_projects_channel_id = fields.Many2one( 'discuss.channel', default=lambda self: self._get_default_projects_channel(), string="Default Projects Channel" ) project_stages = fields.One2many('project.stages.approval.flow', 'project_id') assign_approval_flow = fields.Boolean(default=False) project_sponsor = fields.Many2one('res.users') show_project_chatter = fields.Boolean(default=False) project_vision = fields.Text( string="Project Vision", help="Concise statement describing the project's ultimate goal and purpose" ) # Requirement Documentation description = fields.Html("Requirement Description") requirement_file = fields.Binary("Requirement Document") requirement_file_name = fields.Char("Requirement File Name") # Feasibility Assessment feasibility_html = fields.Html("Feasibility Assessment") feasibility_file = fields.Binary("Feasibility Document") feasibility_file_name = fields.Char("Feasibility File Name") manager_level_edit_access = fields.Boolean(compute="_compute_has_manager_level_edit_access") approval_status = fields.Selection([ ('submitted', 'Submitted'), ('reject', 'Rejected') ]) show_submission_button = fields.Boolean(compute="_compute_access_check") show_approval_button = fields.Boolean(compute="_compute_access_check") show_refuse_button = fields.Boolean(compute="_compute_access_check") show_back_button = fields.Boolean(compute="_compute_access_check") project_activity_log = fields.Html(string="Project Activity Log") project_scope = fields.Html(string="Scope", default=lambda self: """

Scope Description



1. In Scope Items?


2. Out Scope Items?


""") risk_ids = fields.One2many( "project.risk", "project_id", string="Project Risks" ) # stage_id = fields.Many2one(domain="[('id','in',showable_stage_ids or [])]") showable_stage_ids = fields.Many2many('project.project.stage',compute='_compute_project_project_stages') # fields: estimated_amount = fields.Float(string="Estimated Amount") total_budget_amount = fields.Float(string="Total Budget Amount", compute="_compute_total_budget", store=True) # Manpower resource_cost_ids = fields.One2many( "project.resource.cost", "project_id", string="Resource Costs" ) # Material material_cost_ids = fields.One2many( "project.material.cost", "project_id", string="Material Costs" ) # Equipment equipment_cost_ids = fields.One2many( "project.equipment.cost", "project_id", string="Equipment Costs" ) architecture_design_ids = fields.One2many( "project.architecture.design", "project_id", string="Architecture & Design" ) require_sprint = fields.Boolean( string="Require Sprints?", default=False, help="Enable sprint-based planning for this project." ) sprint_ids = fields.One2many( "project.sprint", "project_id", string="Project Sprints" ) commit_step_ids = fields.One2many( 'project.commit.step', 'project_id', string="Commit Steps" ) development_document_ids = fields.One2many( "task.development.document", "project_id", string="Development Documents" ) testing_document_ids = fields.One2many( "task.testing.document", "project_id", string="Testing Documents" ) development_notes = fields.Html() testing_notes = fields.Html() deployment_log_ids = fields.One2many( 'project.deployment.log', 'project_id', string="Deployment Logs" ) maintenance_support_ids = fields.One2many( 'project.maintenance.support', 'project_id', string="Maintenance Logs" ) all_deliverables_submitted = fields.Boolean(string="All Deliverables Submitted") final_qa_done = fields.Boolean(string="Final QA Completed") client_signoff_closure = fields.Boolean(string="Client Closure Sign-Off") billing_completed = fields.Boolean(string="Billing Completed") training_completed = fields.Boolean(string="Training Completed") # ------------------------ # B. Closure Documents # ------------------------ closure_document_ids = fields.One2many( "project.closure.document", "project_id", string="Closure Documents" ) # ------------------------ # C. Learning & Review # ------------------------ lessons_learned = fields.Text(string="Lessons Learned") challenges_faced = fields.Text(string="Challenges Faced") future_recommendations = fields.Text(string="Future Recommendations") @api.depends('require_sprint','project_stages','assign_approval_flow') def _compute_project_project_stages(self): for rec in self: project_stages = self.env['project.project.stage'].sudo().search([]) if rec.assign_approval_flow: project_stages = rec.project_stages.filtered(lambda x: x.activate).stage_id + self.env.ref("project_task_timesheet_extended.project_project_stage_sprint_planning") stage_ids = self.env['project.project.stage'].sudo().search([('id','in',project_stages.ids),('active', '=', True), ('company_id','in',[self.env.company.id,False])]).ids if not rec.require_sprint: stage_ids = self.env['project.project.stage'].sudo().search([ ('id', 'in', project_stages.ids), ('active', '=', True),('company_id','in',[self.env.company.id,False]), ('id', '!=', self.env.ref( "project_task_timesheet_extended.project_project_stage_sprint_planning").id) ]).ids rec.showable_stage_ids = stage_ids @api.depends("resource_cost_ids.total_cost", "material_cost_ids.total_cost", "equipment_cost_ids.total_cost") def _compute_total_budget(self): for project in self: project.total_budget_amount = ( sum(project.resource_cost_ids.mapped("total_cost")) + sum(project.material_cost_ids.mapped("total_cost")) + sum(project.equipment_cost_ids.mapped("total_cost")) ) def fetch_project_task_stage_users(self): for project in self: users_list = list() if project.assign_approval_flow: users_list.extend(project.project_stages.involved_users.ids) else: users_list.extend(project.showable_stage_ids.user_ids.ids) if project.project_sponsor: users_list.append(project.project_sponsor.id) if project.user_id: users_list.append(project.user_id.id) if project.project_lead: users_list.append(project.project_lead.id) if project.type_ids: for task_stage in project.type_ids: if task_stage.team_id: users_list.append(task_stage.team_id.team_lead.id) if task_stage.involved_user_ids: users_list.extend(task_stage.involved_user_ids.ids) users_list = list(set(users_list)) base_users = project.members_ids.ids removed_ids = set(base_users).difference(users_list) newly_added_ids = set(users_list).difference(base_users) project.update({ "members_ids": [(6, 0, users_list)], "message_partner_ids": [(4, user_id.partner_id.id) for user_id in self.env['res.users'].sudo().search([('id', 'in', list(newly_added_ids))])], }) project.update({ "message_partner_ids": [(3, user_id.partner_id.id) for user_id in self.env['res.users'].sudo().search([('id', 'in', list(removed_ids))])], }) # -------------------------------------------------------- # Fetch Resource Data Button # -------------------------------------------------------- def action_fetch_resource_data(self): """Fetch all members' employee records and create manpower cost lines with full auto-fill.""" for project in self: # Project users = members + project manager + project lead users = project.members_ids | project.user_id | project.project_lead # Fetch employees linked to those users employees = self.env["hr.employee"].search([("user_id", "in", users.ids)]) for emp in employees: # Avoid duplicate manpower lines existing = project.resource_cost_ids.filtered(lambda r: r.employee_id == emp) if existing: continue # Get active contract for salary details contract = emp.contract_id monthly_salary = contract.wage if contract else 0.0 daily_rate = (contract.wage / 30) if contract and contract.wage else 0.0 # Project Dates start_date = project.date_start or fields.Date.today() end_date = project.date or start_date # Duration duration = (end_date - start_date).days + 1 if start_date and end_date else 0 # Total Cost total_cost = daily_rate * duration if daily_rate else 0 # Create manpower line self.env["project.resource.cost"].create({ "project_id": project.id, "employee_id": emp.id, "monthly_salary": monthly_salary, "daily_rate": daily_rate, "start_date": start_date, "end_date": end_date, "duration_days": duration, "total_cost": total_cost, }) @api.depends("project_stages", "stage_id", "approval_status") def _compute_access_check(self): """Compute visibility of action buttons based on user permissions and project state""" for project in self: project.show_submission_button = False project.show_approval_button = False project.show_refuse_button = False project.show_back_button = False if not project.assign_approval_flow: continue user = self.env.user project_manager = project.user_id project_sponsor = project.project_sponsor # Current approval timeline for this stage current_approval_timeline = project.project_stages.filtered( lambda s: s.stage_id == project.stage_id ) # Compute button visibility based on approval flow if current_approval_timeline: line = current_approval_timeline[0] assigned_to = line.assigned_to responsible_lead = line.approval_by # Submission button for assigned users if (assigned_to == user and project.approval_status != "submitted" and assigned_to != responsible_lead): project.show_submission_button = True # Approval/refusal buttons for responsible leads if (project.approval_status == "submitted" and responsible_lead == user): project.show_approval_button = True project.show_refuse_button = True # Direct approval when no assigned user if not assigned_to and responsible_lead == user: project.show_approval_button = True # Direct approval when assigned user is also responsible if (assigned_to == responsible_lead and user == assigned_to): project.show_approval_button = True else: # Managers can approve without specific flow if user.has_group("project.group_project_manager"): project.show_approval_button = True # Managers get additional permissions if user in [project_manager] or user.has_group("project.group_project_manager"): project.show_back_button = True if user.has_group("project.group_project_manager"): project.show_approval_button = True # Stage-specific button visibility if project.stage_id: stages = self.env['project.project.stage'].search([('id','in',project.showable_stage_ids.ids), ('id', '!=', self.env.ref("project.project_project_stage_3").id) ]) if stages: first_stage = stages.sorted('sequence')[0] last_stage = stages.sorted('sequence')[-1] if project.stage_id == first_stage: project.show_back_button = False if project.stage_id == last_stage: project.show_submission_button = False project.show_approval_button = False project.show_refuse_button = False @api.depends("user_id") def _compute_has_manager_level_edit_access(self): """Determine if current user has manager-level edit permissions""" for rec in self: rec.manager_level_edit_access = ( rec.user_id == self.env.user or self.env.user.has_group("project.group_project_manager") ) def action_show_project_chatter(self): """Toggle visibility of project chatter""" for project in self: project.show_project_chatter = not project.show_project_chatter def action_assign_approval_flow(self): """Configure approval flow for project stages""" for project in self: if not project.project_sponsor or not project.user_id: raise ValidationError(_("Sponsor and Manager are required to assign Stage Approvals")) project.assign_approval_flow = not project.assign_approval_flow if project.assign_approval_flow: # Clear existing records project.project_stages.unlink() # Fetch all project stages stages = self.env['project.project.stage'].sudo().search([('active', '=', True), ('company_id', 'in', [self.env.company.id, False])]) for stage in stages: # Determine approval authority based on stage configuration approval_by = ( project.user_id.id if stage.sudo().approval_by == 'project_manager' else project.project_sponsor.id if stage.sudo().approval_by == 'project_sponsor' else False ) assigned_to = (approval_by if approval_by in stage.user_ids.ids else min(stage.user_ids, key=lambda u: u.id).id if stage.user_ids else False ) self.env['project.stages.approval.flow'].sudo().create({ 'project_id': project.id, 'stage_id': stage.id, 'approval_by': approval_by, 'assigned_to': assigned_to, 'involved_users': [(6, 0, stage.user_ids.ids)], 'assigned_date': fields.Datetime.now() if stage == project.stage_id else False, 'submission_date': False, }) # Log approval flow assignment self.sudo()._add_activity_log("Approval flow assigned by %s" % self.env.user.name) self.sudo()._post_to_project_channel( _("Approval flow configured for project %s") % project.name ) else: project.sudo().project_stages.unlink() self.sudo()._add_activity_log("Approval flow removed by %s" % self.env.user.name) self.sudo()._post_to_project_channel( _("Approval flow removed for project %s") % project.name ) def submit_project_for_approval(self): """Submit project for current stage approval""" for project in self: project.sudo().approval_status = "submitted" current_stage = project.sudo().stage_id current_approval_timeline = project.sudo().project_stages.filtered( lambda s: s.stage_id == project.sudo().stage_id ) if current_approval_timeline: current_approval_timeline.sudo().submission_date = fields.Datetime.now() stage_line = project.sudo().project_stages.filtered(lambda s: s.stage_id == current_stage) responsible_user = stage_line.sudo().approval_by if stage_line else False # Create activity log activity_log = "%s : %s submitted for approval to %s" % ( current_stage.sudo().name, self.env.user.name, responsible_user.sudo().name if responsible_user else "N/A" ) project.sudo()._add_activity_log(activity_log) # Post to project channel if responsible_user: channel_message = _("Project %s submitted for approval at stage %s. %s please review.") % ( project.sudo().name, current_stage.sudo().name, project.sudo()._create_odoo_mention(responsible_user.partner_id) ) else: channel_message = _("Project %s submitted for approval at stage %s") % ( project.sudo().name, current_stage.sudo().name ) project.sudo()._post_to_project_channel(channel_message) # Send notification if responsible_user: project.sudo().message_post( body=activity_log, partner_ids=[responsible_user.sudo().partner_id.id], message_type='notification', subtype_xmlid='mail.mt_comment' ) def project_proceed_further(self): """Advance project to next stage after approval""" for project in self: current_stage = project.stage_id next_stage = self.env["project.project.stage"].search([ ('sequence', '>', project.stage_id.sequence), ('id', '!=', self.env.ref("project.project_project_stage_3").id), ('id', 'in', project.showable_stage_ids.ids), ], order="sequence asc", limit=1) current_approval_timeline = project.project_stages.filtered( lambda s: s.stage_id == project.stage_id ) if current_approval_timeline: current_approval_timeline.submission_date = fields.Datetime.now() if not current_approval_timeline.assigned_date: current_approval_timeline.assigned_date = fields.Datetime.now() if next_stage: next_approval_timeline = project.project_stages.filtered( lambda s: s.stage_id == next_stage ) if next_approval_timeline and not next_approval_timeline.assigned_date: next_approval_timeline.assigned_date = fields.Datetime.now() project.stage_id = next_stage project.approval_status = "" # Create activity log activity_log = "%s approved by %s → moved to %s" % ( current_stage.name, self.env.user.name, next_stage.name ) project._add_activity_log(activity_log) # Post to project channel next_user = next_approval_timeline.assigned_to if next_approval_timeline else False if next_user: channel_message = _("Project %s approved at stage %s and moved to %s. %s please proceed.") % ( project.name, current_stage.name, next_stage.name, project._create_odoo_mention(next_user.partner_id) ) else: channel_message = _("Project %s approved at stage %s and moved to %s") % ( project.name, current_stage.name, next_stage.name ) project._post_to_project_channel(channel_message) # Send notification if next_user: project.message_post( body=activity_log, partner_ids=[next_user.partner_id.id], message_type='notification', subtype_xmlid='mail.mt_comment' ) else: # Last stage completed project.approval_status = "" activity_log = "%s fully approved and completed" % project.name project._add_activity_log(activity_log) project._post_to_project_channel( _("Project %s completed and fully approved") % project.name ) project.message_post(body=activity_log) def reject_and_return(self, reason=None): """Reject project at current stage with optional reason""" for project in self: reason = reason or "" current_stage = project.stage_id project.approval_status = "reject" # Create activity log activity_log = "%s rejected by %s — %s" % ( current_stage.name, self.env.user.name, reason ) project._add_activity_log(activity_log) # Update approval timeline current_approval_timeline = project.project_stages.filtered( lambda s: s.stage_id == project.stage_id ) if current_approval_timeline: current_approval_timeline.note = f"Reject Reason: {reason}" # Post to project channel channel_message = _("Project %s rejected at stage %s. Reason: %s") % ( project.name, current_stage.name, reason ) project._post_to_project_channel(channel_message) # Send notification project.message_post(body=activity_log) # Notify responsible users if current_approval_timeline: responsible_user = ( current_approval_timeline.assigned_to or current_approval_timeline.approval_by ) if responsible_user: project.message_post( body=_("Project %s has been rejected and returned to you") % project.name, partner_ids=[responsible_user.partner_id.id], message_type='notification', subtype_xmlid='mail.mt_comment' ) def project_back_button(self): """Revert project to previous stage""" for project in self: prev_stage = self.env["project.project.stage"].search([ ('sequence', '<', project.stage_id.sequence), ('id', 'in', project.showable_stage_ids.ids) ], order="sequence desc", limit=1) if not prev_stage: raise ValidationError(_("No previous stage available.")) # Create activity log activity_log = "%s reverted back to %s by %s" % ( project.stage_id.name, prev_stage.name, self.env.user.name ) project._add_activity_log(activity_log) # Post to project channel channel_message = _("Project %s reverted from %s back to %s") % ( project.name, project.stage_id.name, prev_stage.name ) project._post_to_project_channel(channel_message) # Update stage project.stage_id = prev_stage project.message_post(body=activity_log) def action_open_reject_wizard(self): """Open rejection wizard for projects""" self.ensure_one() return { "type": "ir.actions.act_window", "name": _("Reject Project"), "res_model": "project.reject.reason.wizard", "view_mode": "form", "target": "new", "context": {"default_project_id": self.id}, } # Activity Log Helper Methods def _get_current_datetime_formatted(self): """Get current datetime in 'DD-MMM-YYYY HH:MM AM/PM' format""" now = fields.Datetime.context_timestamp(self, fields.datetime.now()) formatted_date = now.strftime('%d-%b-%Y %I:%M %p').upper() return formatted_date[1:] if formatted_date.startswith('0') else formatted_date def _add_activity_log(self, activity_text): """Add formatted entry to project activity log""" formatted_datetime = self._get_current_datetime_formatted() for project in self: log_entry = f"[{formatted_datetime}] {activity_text}" if project.project_activity_log: project.project_activity_log = Markup(project.project_activity_log) + Markup('
') + Markup(log_entry) else: project.project_activity_log = Markup(log_entry) def _post_to_project_channel(self, message_body, mention_partners=None): """Post message to project's discuss channel with proper mentions""" for project in self: if not project.id: continue # Get project channel channel = ( project.discuss_channel_id or project.default_projects_channel_id ) if channel: formatted_message = self._format_message_with_odoo_mentions( message_body, mention_partners ) channel.message_post( body=Markup(formatted_message), message_type='comment', subtype_xmlid='mail.mt_comment', author_id=self.env.user.partner_id.id ) def _format_message_with_odoo_mentions(self, message_body, mention_partners=None): """Format message with proper Odoo @mentions""" if not mention_partners: return f'
{message_body}
' message_parts = ['
', message_body] for partner in mention_partners: if partner and partner.name: mention_html = f'@{partner.name}' message_parts.append(mention_html) message_parts.append('
') return ' '.join(message_parts) def _create_odoo_mention(self, partner): """Create Odoo mention link for a partner""" if not partner: return "" return f'@{partner.name}' @api.model def _get_default_projects_channel(self): """Get or create the default Projects Channel""" channel = self.env['discuss.channel'].search([ ('name', '=', 'Projects Channel'), ('channel_type', '=', 'channel') ], limit=1) if not channel: channel = self.env['discuss.channel'].create({ 'name': 'Projects Channel', 'description': 'Main channel for all project communications', 'channel_type': 'channel', }) return channel def action_create_project_channel(self): """Create a new channel for this project under the Projects Channel""" self.ensure_one() if self.discuss_channel_id: raise UserError(_("This project already has a channel assigned.")) # Create new channel channel_vals = { 'name': self.name, 'description': _("Communication channel for project %s") % self.name, 'channel_type': 'channel', 'parent_channel_id': self.default_projects_channel_id.id, } new_channel = self.env['discuss.channel'].create(channel_vals) self.discuss_channel_id = new_channel.id # Add project members to the channel self._add_project_members_to_channel() return { 'type': 'ir.actions.act_window', 'res_model': 'discuss.channel', 'res_id': new_channel.id, 'view_mode': 'form', 'target': 'current', 'context': {'create': False} } def _add_project_members_to_channel(self): """Add all project members as followers of the channel""" if not self.discuss_channel_id: return # Get all users related to this project members_to_add = self.env['res.users'] # Add project members if self.members_ids: members_to_add |= self.members_ids # Add project manager if self.user_id: members_to_add |= self.user_id # Add project lead if exists if hasattr(self, 'project_lead') and self.project_lead: members_to_add |= self.project_lead # Add members to channel for member in members_to_add: self.discuss_channel_id.add_members(member.partner_id.ids) def write(self, vals): """Override write to update channel members when project members change""" result = super().write(vals) # If members changed, update channel members if any(field in vals for field in ['members_ids', 'user_id', 'project_lead']): for project in self: if project.discuss_channel_id: project._add_project_members_to_channel() return result @api.model def _get_shared_project_sequence(self): """Get or create a shared sequence for all projects""" sequence = self.env['ir.sequence'].sudo().search([('code', '=', 'project.project.sequence')], limit=1) if not sequence: sequence = self.env['ir.sequence'].sudo().create({ 'name': _("Project Sequence"), 'implementation': 'no_gap', 'padding': 3, 'use_date_range': False, 'prefix': 'PROJ-', 'code': 'project.project.sequence', }) return sequence @api.model_create_multi def create(self, vals_list): projects = super().create(vals_list) sequence = self._get_shared_project_sequence() for project in projects: if not project.sequence_name: project.sequence_name = sequence.next_by_id() if project.discuss_channel_id: project._add_project_members_to_channel() return projects def _default_type_ids(self): default_stage_ids = [ self.env.ref('project_task_timesheet_extended.task_type_backlog').id, self.env.ref('project_task_timesheet_extended.task_type_development').id, self.env.ref('project_task_timesheet_extended.task_type_code_review_and_merging').id, self.env.ref('project_task_timesheet_extended.task_type_testing').id, self.env.ref('project_task_timesheet_extended.task_type_deployment').id, self.env.ref('project_task_timesheet_extended.task_type_completed').id, ] # self.env.ref('project_task_timesheet_extended.task_type_cancelled').id, # self.env.ref('project_task_timesheet_extended.task_type_hold').id, return self.env['project.task.type'].browse(default_stage_ids) project_lead = fields.Many2one("res.users", string="Project Lead", domain=lambda self: [('id','in',self.env.ref('project_task_timesheet_extended.role_project_lead').user_ids.ids)]) members_ids = fields.Many2many('res.users', 'project_user_rel', 'project_id', 'user_id', 'Project Members', help="""Project's members are users who can have an access to the tasks related to this project.""" ) user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True, domain=lambda self: [('id','in',self.env.ref('project_task_timesheet_extended.role_project_manager').user_ids.ids),('groups_id', 'in', [self.env.ref('project.group_project_manager').id,self.env.ref('project_task_timesheet_extended.group_project_supervisor').id]),('share','=',False)],) type_ids = fields.Many2many(default=lambda self: self._default_type_ids()) estimated_hours = fields.Float(string="Estimated Hours") task_estimated_hours = fields.Float(string="Task Estimated Hours", compute="_compute_task_estimated_hours", store=True) actual_hours = fields.Float(string="Actual Hours", compute="_compute_actual_hours", store=True) @api.depends('task_ids.estimated_hours') def _compute_task_estimated_hours(self): for project in self: project.task_estimated_hours = sum(project.task_ids.mapped('estimated_hours')) @api.depends('task_ids.timesheet_ids.unit_amount') def _compute_actual_hours(self): for project in self: project.actual_hours = sum(project.task_ids.timesheet_ids.mapped('unit_amount')) def add_users(self): return { 'type': 'ir.actions.act_window', 'name': 'Add Users', 'res_model': 'project.user.assign.wizard', 'view_mode': 'form', 'view_id': self.env.ref('project_task_timesheet_extended.project_user_assignment_form_view').id, 'target': 'new', 'context': {'default_members_ids':[(6, 0, self.members_ids.ids)], }, } class ProjectTask(models.Model): _inherit = 'project.task' def _default_sprint_id(self): """Return the current active (in-progress) sprint of the project.""" if 'project_id' in self._context: project_id = self._context.get('project_id') sprint = self.env['project.sprint'].search([ ('project_id', '=', project_id), ('status', '=', 'in_progress') ], limit=1) return sprint.id return False sprint_id = fields.Many2one( "project.sprint", string="Sprint", default=_default_sprint_id ) require_sprint = fields.Boolean( related="project_id.require_sprint", store=False ) commit_step_ids = fields.One2many( 'project.commit.step', 'task_id', string="Commit Steps" ) show_task_chatter = fields.Boolean(default=False) development_document_ids = fields.One2many( "task.development.document", "task_id", string="Development Documents" ) testing_document_ids = fields.One2many( "task.testing.document", "task_id", string="Testing Documents" ) @api.onchange("project_id") def _onchange_project_id_sprint_required(self): for task in self: if task.project_id and not task.project_id.require_sprint: task.sprint_id = False else: if task.project_id and task.project_id.require_sprint: sprint = self.env['project.sprint'].search([ ('project_id', '=', task.project_id.id), ('status', '=', 'in_progress') ], limit=1) task.sprint_id = sprint.id def action_show_project_task_chatter(self): """Toggle visibility of project chatter""" for project in self: project.show_task_chatter = not project.show_task_chatter