PMT bug fix

This commit is contained in:
karuna 2026-04-13 17:56:48 +05:30
parent b5b276f552
commit ff806505e2
10 changed files with 401 additions and 4 deletions

View File

@ -14,6 +14,7 @@
name="Project Dashboard" name="Project Dashboard"
parent="project.menu_project_management" parent="project.menu_project_management"
action="action_project_dashboard_fullscreen" action="action_project_dashboard_fullscreen"
groups="project.group_project_manager"
sequence="10"/> sequence="10"/>
</data> </data>
</odoo> </odoo>

View File

@ -54,7 +54,9 @@ Key Features:
'view/timesheets.xml', 'view/timesheets.xml',
'view/pro_task_gantt.xml', 'view/pro_task_gantt.xml',
'view/user_availability.xml', 'view/user_availability.xml',
'view/project_task_view.xml',
# 'view/project_task_gantt.xml', # 'view/project_task_gantt.xml',
'view/stage_approval_wizard.xml',
], ],
'assets': { 'assets': {
'web.assets_backend':{ 'web.assets_backend':{

View File

@ -22,3 +22,5 @@ from . import timesheets
# from . import project_task_gantt # from . import project_task_gantt
from . import user_availability from . import user_availability
from . import stage_visibility from . import stage_visibility
from . import account_analytic_line
from . import stage_approval_wizard

View File

@ -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")

View File

@ -7,6 +7,22 @@ import pytz
class ProjectProject(models.Model): class ProjectProject(models.Model):
_inherit = 'project.project' _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) sequence_name = fields.Char("Project Number", copy=False, readonly=True)
task_sequence_id = fields.Many2one( task_sequence_id = fields.Many2one(
'ir.sequence', 'ir.sequence',
@ -127,6 +143,12 @@ class ProjectProject(models.Model):
estimated_amount = fields.Float(string="Estimated planned Amount") 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) 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 # Manpower
resource_cost_ids = fields.One2many( resource_cost_ids = fields.One2many(
"project.resource.cost", "project.resource.cost",
@ -950,9 +972,15 @@ class ProjectProject(models.Model):
members are users who can have an access to members are users who can have an access to
the tasks related to this project.""" 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)],) 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()) 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) 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) 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') @api.depends('task_ids.estimated_hours')
def _compute_task_estimated_hours(self): def _compute_task_estimated_hours(self):

View File

@ -11,6 +11,8 @@ from odoo.addons.resource.models.utils import filter_domain_leaf
from odoo.osv.expression import is_leaf from odoo.osv.expression import is_leaf
from odoo.osv import expression from odoo.osv import expression
from collections import defaultdict from collections import defaultdict
import re
from datetime import date
@ -25,6 +27,56 @@ class projectTask(models.Model):
_inherit = 'project.task' _inherit = 'project.task'
_rec_name = 'name' _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) sequence_name = fields.Char("Sequence", copy=False)
is_generic = fields.Boolean(string='Generic', default=True, tracking=True, 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') 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, store=True,
readonly=False 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") has_supervisor_access = fields.Boolean(compute="_compute_has_supervisor_access")
actual_hours = fields.Float( actual_hours = fields.Float(
string="Actual Hours", string="Actual Hours",
@ -58,7 +138,7 @@ class projectTask(models.Model):
suggested_deadline = fields.Datetime(string="Suggested Deadline", compute="_compute_suggested_deadline", store=True) suggested_deadline = fields.Datetime(string="Suggested Deadline", compute="_compute_suggested_deadline", store=True)
is_suggested_deadline_warning = fields.Boolean( is_suggested_deadline_warning = fields.Boolean(
compute="_compute_deadline_warning", compute="_compute_deadline_warning",
string="Deadline Warning" string="Deadline Warning",
) )
allowed_employee_ids = fields.Many2many( allowed_employee_ids = fields.Many2many(
'hr.employee', 'hr.employee',
@ -145,6 +225,15 @@ class projectTask(models.Model):
task.allowed_employee_ids = employees 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') @api.depends('suggested_deadline', 'date_deadline','timelines_requested','show_approval_flow')
def _compute_deadline_warning(self): def _compute_deadline_warning(self):
for rec in self: for rec in self:
@ -1462,3 +1551,4 @@ class projectTaskTimelines(models.Model):
allowed_teams |= team allowed_teams |= team
allowed_teams |= team.child_ids allowed_teams |= team.child_ids
rec.allowed_team_ids = allowed_teams rec.allowed_team_ids = allowed_teams

View File

@ -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()

View File

@ -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_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_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_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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
90
91
92
93
94
95
96
97

View File

@ -0,0 +1,184 @@
<odoo>
<!-- Existing progress code -->
<record id="view_task_form_progress_inherit" model="ir.ui.view">
<field name="name">project.task.form.progress.inherit</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<group string="Progress">
<field name="progress" widget="progressbar"/>
</group>
</xpath>
</field>
</record>
<!-- ✅ ADD THIS NEW BLOCK -->
<record id="fix_generic_field_readonly" model="ir.ui.view">
<field name="name">project.task.form.fix.generic</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='is_generic']" position="attributes">
<attribute name="readonly">0</attribute>
</xpath>
</field>
</record>
<record id="hide_deadline_field" model="ir.ui.view">
<field name="name">project.task.hide.deadline</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="priority">999</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='date_deadline']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<record id="hide_deadline_warning_field_final" model="ir.ui.view">
<field name="name">hide.deadline.warning.safe</field>
<field name="model">project.task</field>
<!-- IMPORTANT: change inherit_id -->
<field name="inherit_id" ref="project_task_timesheet_extended.project_task_form_inherit"/>
<field name="priority">9999</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='is_suggested_deadline_warning']" position="replace">
</xpath>
</field>
</record>
<record id="remove_extra_info_fields" model="ir.ui.view">
<field name="name">project.task.remove.extra.fields</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="priority">9999</field>
<field name="arch" type="xml">
<!-- Hide Sequence -->
<xpath expr="//field[@name='sequence']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<!-- Hide Email CC -->
<xpath expr="//field[@name='email_cc']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<!-- Hide Cover Image -->
<xpath expr="//field[@name='displayed_image_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<record id="update_project_placeholder_all" model="ir.ui.view">
<field name="name">project.placeholder.update.all</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.project_project_view_form_simplified"/>
<field name="priority">9999</field>
<field name="arch" type="xml">
<!-- Project Name -->
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="placeholder">e.g. Client Project</attribute>
</xpath>
<!-- Alias Name -->
<xpath expr="//field[@name='alias_name']" position="attributes">
<attribute name="placeholder">e.g. client-support</attribute>
</xpath>
</field>
</record>
<record id="project_sort_recent_first" model="ir.ui.view">
<field name="name">project.sort.recent.first</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_kanban"/>
<field name="arch" type="xml">
<xpath expr="//kanban" position="attributes">
<attribute name="default_order">create_date desc</attribute>
</xpath>
</field>
</record>
<!-- <record id="action_internal_teams" model="ir.actions.act_window">-->
<!-- <field name="name">Internal Teams</field>-->
<!-- <field name="res_model">internal.teams</field>-->
<!-- <field name="view_mode">list,form</field>-->
<!-- &lt;!&ndash; ✅ ADD THIS LINE &ndash;&gt;-->
<!-- <field name="domain">[('parent_id','=',False)]</field>-->
<!-- </record>-->
<record id="task_estimated_hours_widget" model="ir.ui.view">
<field name="name">project.task.form.estimated.hours.widget</field>
<field name="model">project.task</field>
<!-- ALWAYS SAFE BASE VIEW -->
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="priority">9999</field>
<field name="arch" type="xml">
<!-- SAFE XPATH (wont crash if not found) -->
<xpath expr="//field[@name='estimated_hours']" position="attributes">
<attribute name="widget">float_time</attribute>
</xpath>
</field>
</record>
<record id="task_list_color_inherit" model="ir.ui.view">
<field name="name">project.task.list.color</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.open_view_all_tasks_list_view"/>
<field name="arch" type="xml">
<xpath expr="//list" position="inside">
<field name="deadline_status"/>
</xpath>
<xpath expr="//list" position="attributes">
<attribute name="decoration-danger">
date_deadline and date_deadline &lt; context_today()
</attribute>
<attribute name="decoration-warning">
date_deadline and date_deadline &lt;= (context_today() + 2)
</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,56 @@
<odoo>
<!-- ============================= -->
<!-- 1. WIZARD FORM (POPUP) -->
<!-- ============================= -->
<record id="view_stage_approval_wizard_form" model="ir.ui.view">
<field name="name">stage.approval.wizard.form</field>
<field name="model">stage.approval.wizard</field>
<field name="arch" type="xml">
<form string="Confirmation">
<group>
<div class="o_form_label">
Are you sure you want to Enable/Disable Stage Approvals?
</div>
</group>
<footer>
<button name="action_confirm" type="object"
string="Yes" class="btn-primary"/>
<button string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- ============================= -->
<!-- 2. NEW SERVER ACTION -->
<!-- ============================= -->
<record id="action_assign_approval_flow_popup" model="ir.actions.server">
<field name="name">Enable/Disable Stage Approvals</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="binding_model_id" ref="project.model_project_project"/>
<field name="binding_type">action</field>
<field name="state">code</field>
<field name="code">
action = {
'type': 'ir.actions.act_window',
'name': 'Confirm',
'res_model': 'stage.approval.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'active_ids': records.ids}
}
</field>
</record>
<!-- ============================= -->
<!-- 3. HIDE ORIGINAL ACTION -->
<!-- ============================= -->
<record id="action_assign_approval_flow" model="ir.actions.server">
<field name="binding_model_id" eval="False"/>
</record>
</odoo>