diff --git a/addons_extensions/project_dashboards_management/views/project_dashboard_actions.xml b/addons_extensions/project_dashboards_management/views/project_dashboard_actions.xml index b045561f4..406922803 100644 --- a/addons_extensions/project_dashboards_management/views/project_dashboard_actions.xml +++ b/addons_extensions/project_dashboards_management/views/project_dashboard_actions.xml @@ -14,6 +14,7 @@ name="Project Dashboard" parent="project.menu_project_management" action="action_project_dashboard_fullscreen" + groups="project.group_project_manager" sequence="10"/> \ No newline at end of file diff --git a/addons_extensions/project_task_timesheet_extended/__manifest__.py b/addons_extensions/project_task_timesheet_extended/__manifest__.py index 2b88f05a3..8ba47b4a5 100644 --- a/addons_extensions/project_task_timesheet_extended/__manifest__.py +++ b/addons_extensions/project_task_timesheet_extended/__manifest__.py @@ -54,7 +54,9 @@ Key Features: 'view/timesheets.xml', 'view/pro_task_gantt.xml', 'view/user_availability.xml', + 'view/project_task_view.xml', # 'view/project_task_gantt.xml', + 'view/stage_approval_wizard.xml', ], 'assets': { 'web.assets_backend':{ diff --git a/addons_extensions/project_task_timesheet_extended/models/__init__.py b/addons_extensions/project_task_timesheet_extended/models/__init__.py index c830dd9e5..992c6beeb 100644 --- a/addons_extensions/project_task_timesheet_extended/models/__init__.py +++ b/addons_extensions/project_task_timesheet_extended/models/__init__.py @@ -22,3 +22,5 @@ from . import timesheets # from . import project_task_gantt from . import user_availability from . import stage_visibility +from . import account_analytic_line +from . import stage_approval_wizard \ No newline at end of file diff --git a/addons_extensions/project_task_timesheet_extended/models/account_analytic_line.py b/addons_extensions/project_task_timesheet_extended/models/account_analytic_line.py new file mode 100644 index 000000000..d855c52df --- /dev/null +++ b/addons_extensions/project_task_timesheet_extended/models/account_analytic_line.py @@ -0,0 +1,13 @@ +from odoo import models, api +from odoo.exceptions import ValidationError + + +class AccountAnalyticLine(models.Model): + _inherit = 'account.analytic.line' + + @api.constrains('unit_amount') + def _check_unit_amount(self): + print("π₯ TIMESHEET VALIDATION TRIGGERED") + for rec in self: + if rec.unit_amount < 0: + raise ValidationError("Hours cannot be negative") \ No newline at end of file diff --git a/addons_extensions/project_task_timesheet_extended/models/project.py b/addons_extensions/project_task_timesheet_extended/models/project.py index f9661eb5d..68618eabf 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project.py +++ b/addons_extensions/project_task_timesheet_extended/models/project.py @@ -7,6 +7,22 @@ import pytz class ProjectProject(models.Model): _inherit = 'project.project' + + @api.constrains('name') + def _check_duplicate_project_name(self): + for rec in self: + if rec.name: + existing = self.search([ + ('name', '=', rec.name), + ('id', '!=', rec.id) + ], limit=1) + + if existing: + raise ValidationError( + "Project name already exists. Please choose a different name." + ) + + sequence_name = fields.Char("Project Number", copy=False, readonly=True) task_sequence_id = fields.Many2one( 'ir.sequence', @@ -127,6 +143,12 @@ class ProjectProject(models.Model): estimated_amount = fields.Float(string="Estimated planned Amount") total_planned_budget_amount = fields.Float(string="Total Estimated planned Budget Amount", compute="_compute_total_budget", store=True) + @api.constrains('estimated_amount') + def _check_estimated_amount(self): + for rec in self: + if rec.estimated_amount < 0: + raise ValidationError("Estimated Amount cannot be negative.") + # Manpower resource_cost_ids = fields.One2many( "project.resource.cost", @@ -950,9 +972,15 @@ class ProjectProject(models.Model): 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=False, tracking=True, + user_id = fields.Many2one('res.users', string='Project Manager', default=False, tracking=True, #required = 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)],) + @api.constrains('user_id', 'selected_employee_id') + def _check_team_lead_before_members(self): + for rec in self: + if rec.selected_employee_id and not rec.user_id: + raise ValidationError("Assign Project Manager before adding members") + type_ids = fields.Many2many(default=lambda self: self._default_type_ids()) @@ -960,7 +988,11 @@ class ProjectProject(models.Model): 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.constrains('estimated_hours') + def _check_project_estimated_hours(self): + for rec in self: + if rec.estimated_hours < 0: + raise ValidationError("Project Estimated Hours cannot be negative") @api.depends('task_ids.estimated_hours') def _compute_task_estimated_hours(self): diff --git a/addons_extensions/project_task_timesheet_extended/models/project_task.py b/addons_extensions/project_task_timesheet_extended/models/project_task.py index ea7cc9098..b2a35ac43 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project_task.py +++ b/addons_extensions/project_task_timesheet_extended/models/project_task.py @@ -11,6 +11,8 @@ from odoo.addons.resource.models.utils import filter_domain_leaf from odoo.osv.expression import is_leaf from odoo.osv import expression from collections import defaultdict +import re +from datetime import date @@ -25,6 +27,56 @@ class projectTask(models.Model): _inherit = 'project.task' _rec_name = 'name' + deadline_status = fields.Selection([ + ('overdue', 'Overdue'), + ('near', 'Near Deadline'), + ('normal', 'Normal'), + ], compute='_compute_deadline_status') + + @api.depends('date_deadline') + def _compute_deadline_status(self): + today = fields.datetime.today() + for rec in self: + if rec.date_deadline: + if rec.date_deadline < today: + rec.deadline_status = 'overdue' + elif (rec.date_deadline - today).days <= 2: + rec.deadline_status = 'near' + else: + rec.deadline_status = 'normal' + else: + rec.deadline_status = 'normal' + + def write(self, vals): + if 'stage_approval' in vals: + raise UserError(_("Are you sure you want to change Stage Approval?")) + + return super().write(vals) + + + def unlink(self): + if not self.env.user.has_group('project.group_project_manager'): + raise UserError("Only Project Manager can delete tasks.") + return super(projectTask, self).unlink() + + def write(self, vals): + # Allow stage update for multiple records + if 'stage_id' in vals: + return super(ProjectTask, self).write(vals) + + return super(ProjectTask, self).write(vals) + + + @api.constrains('name') + def _check_task_name(self): + pattern = r'^[A-Za-z0-9 ]+$' # only letters, numbers, space + + for rec in self: + if rec.name and not re.match(pattern, rec.name): + raise ValidationError( + "Task name can only contain letters, numbers, and spaces." + ) + sequence_name = fields.Char("Sequence", copy=False) is_generic = fields.Boolean(string='Generic', default=True, tracking=True, help='All the followers would be able to see this task if the generic is set to true else only the assigned users would have the access to it') @@ -47,6 +99,34 @@ class projectTask(models.Model): store=True, readonly=False ) + + def write(self, vals): + for rec in self: + if 'is_generic' in vals: + # Allow only creator or project manager + if not ( + self.env.user == rec.create_uid or + self.env.user.has_group('project.group_project_manager') + ): + raise UserError("Only Task Creator or Project Manager can edit Generic field.") + + return super(ProjectTask, self).write(vals) + + @api.constrains('estimated_hours') + def _check_estimated_hours(self): + for rec in self: + if rec.estimated_hours < 0: + raise ValidationError("Estimated Hours cannot be negative") + + @api.constrains('date_deadline') + def _check_date_deadline(self): + print("π₯π₯ DEADLINE CONSTRAINT WORKING π₯π₯") + now = fields.Datetime.now() + for rec in self: + if rec.date_deadline and rec.date_deadline < now: + raise ValidationError("Deadline cannot be set in the past") + + has_supervisor_access = fields.Boolean(compute="_compute_has_supervisor_access") actual_hours = fields.Float( string="Actual Hours", @@ -58,7 +138,7 @@ class projectTask(models.Model): suggested_deadline = fields.Datetime(string="Suggested Deadline", compute="_compute_suggested_deadline", store=True) is_suggested_deadline_warning = fields.Boolean( compute="_compute_deadline_warning", - string="Deadline Warning" + string="Deadline Warning", ) allowed_employee_ids = fields.Many2many( 'hr.employee', @@ -145,6 +225,15 @@ class projectTask(models.Model): task.allowed_employee_ids = employees + @api.onchange('user_ids') + def _onchange_user_ids(self): + if self.project_id and self.project_id.user_id: + if self.project_id.user_id.id != self.env.user.id: + raise ValidationError( + "Only Project Manager can assign/remove assignees" + ) + + @api.depends('suggested_deadline', 'date_deadline','timelines_requested','show_approval_flow') def _compute_deadline_warning(self): for rec in self: @@ -1461,4 +1550,5 @@ class projectTaskTimelines(models.Model): team = rec.stage_id.team_id allowed_teams |= team allowed_teams |= team.child_ids - rec.allowed_team_ids = allowed_teams \ No newline at end of file + rec.allowed_team_ids = allowed_teams + diff --git a/addons_extensions/project_task_timesheet_extended/models/stage_approval_wizard.py b/addons_extensions/project_task_timesheet_extended/models/stage_approval_wizard.py new file mode 100644 index 000000000..58d5cde84 --- /dev/null +++ b/addons_extensions/project_task_timesheet_extended/models/stage_approval_wizard.py @@ -0,0 +1,12 @@ +from odoo import models + +class StageApprovalWizard(models.TransientModel): + _name = 'stage.approval.wizard' + _description = 'Stage Approval Confirmation' + + def action_confirm(self): + active_ids = self.env.context.get('active_ids', []) + projects = self.env['project.project'].browse(active_ids) + + # call original function + projects.action_assign_approval_flow() \ No newline at end of file diff --git a/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv index e8228f7f4..e96120ffc 100644 --- a/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv +++ b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv @@ -90,3 +90,8 @@ access_project_resource_actual_cost_user,project.resource.actual.cost.user,model access_project_resource_actual_cost_manager,project.resource.actual.cost.manager,model_project_resource_actual_cost,project.group_project_manager,1,1,1,1 access_project_resource_contract_period_user,project.resource.contract.period.user,model_project_resource_contract_period,base.group_user,1,0,0,0 access_project_resource_contract_period_manager,project.resource.contract.period.manager,model_project_resource_contract_period,project.group_project_manager,1,1,1,1 + + +access_timesheets_user,timesheets user,model_account_analytic_line,base.group_user,1,1,1,0 + +access_stage_approval_wizard,stage.approval.wizard,model_stage_approval_wizard,,1,1,1,1 diff --git a/addons_extensions/project_task_timesheet_extended/view/project_task_view.xml b/addons_extensions/project_task_timesheet_extended/view/project_task_view.xml new file mode 100644 index 000000000..79cbe8b14 --- /dev/null +++ b/addons_extensions/project_task_timesheet_extended/view/project_task_view.xml @@ -0,0 +1,184 @@ + + + + + project.task.form.progress.inherit + project.task + + + + + + + + + + + + + + + + project.task.form.fix.generic + project.task + + + + + 0 + + + + + + + project.task.hide.deadline + project.task + + 999 + + + + 1 + + + + + + + hide.deadline.warning.safe + project.task + + + + + 9999 + + + + + + + + + + project.task.remove.extra.fields + project.task + + 9999 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + + + + project.placeholder.update.all + project.project + + 9999 + + + + + e.g. Client Project + + + + + e.g. client-support + + + + + + + + project.sort.recent.first + project.project + + + + + create_date desc + + + + + + + + + + + + + + + + + + + project.task.form.estimated.hours.widget + project.task + + + + + 9999 + + + + + + float_time + + + + + + + + project.task.list.color + project.task + + + + + + + + + + + + + date_deadline and date_deadline < context_today() + + + + date_deadline and date_deadline <= (context_today() + 2) + + + + + + + + + \ No newline at end of file diff --git a/addons_extensions/project_task_timesheet_extended/view/stage_approval_wizard.xml b/addons_extensions/project_task_timesheet_extended/view/stage_approval_wizard.xml new file mode 100644 index 000000000..9d2d6cc84 --- /dev/null +++ b/addons_extensions/project_task_timesheet_extended/view/stage_approval_wizard.xml @@ -0,0 +1,56 @@ + + + + + + + stage.approval.wizard.form + stage.approval.wizard + + + + + Are you sure you want to Enable/Disable Stage Approvals? + + + + + + + + + + + + + Enable/Disable Stage Approvals + + + action + code + + + action = { + 'type': 'ir.actions.act_window', + 'name': 'Confirm', + 'res_model': 'stage.approval.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': {'active_ids': records.ids} + } + + + + + + + + + + + + \ No newline at end of file