Implemented Roles Management and Weekly Timesheet enhancements
This commit is contained in:
parent
92a45a847b
commit
59285440a5
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "Role-Based Group Management",
|
"name": "Role-Based Group Management",
|
||||||
"summary": "Manage reusable security-group roles for users",
|
"summary": "Manage reusable security-group roles for users",
|
||||||
"version": "18.0.1.4.0",
|
"version": "18.0.1.6.0",
|
||||||
"category": "Administration/Access Rights",
|
"category": "Administration/Access Rights",
|
||||||
"license": "LGPL-3",
|
"license": "LGPL-3",
|
||||||
"author": "Custom",
|
"author": "Custom",
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
from . import res_users
|
|
||||||
from . import user_role
|
from . import user_role
|
||||||
|
from . import res_users
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,21 @@ class ResUsers(models.Model):
|
||||||
|
|
||||||
role_id = fields.Many2one(
|
role_id = fields.Many2one(
|
||||||
comodel_name="res.users.role",
|
comodel_name="res.users.role",
|
||||||
string="Role",
|
string="Legacy Role",
|
||||||
groups="base.group_system",
|
groups="base.group_system",
|
||||||
ondelete="set null",
|
ondelete="set null",
|
||||||
copy=False,
|
copy=False,
|
||||||
help="Reusable role whose groups can be synchronized to this user.",
|
help="Legacy single role field kept only to migrate old records.",
|
||||||
|
)
|
||||||
|
role_ids = fields.Many2many(
|
||||||
|
comodel_name="res.users.role",
|
||||||
|
relation="res_users_role_user_rel",
|
||||||
|
column1="user_id",
|
||||||
|
column2="role_id",
|
||||||
|
string="Assigned Roles",
|
||||||
|
groups="base.group_system",
|
||||||
|
copy=False,
|
||||||
|
help="Reusable roles whose groups can be synchronized to this user.",
|
||||||
)
|
)
|
||||||
role_managed_group_ids = fields.Many2many(
|
role_managed_group_ids = fields.Many2many(
|
||||||
comodel_name="res.groups",
|
comodel_name="res.groups",
|
||||||
|
|
@ -28,11 +38,13 @@ class ResUsers(models.Model):
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
users = super().create(vals_list)
|
users = super().create(vals_list)
|
||||||
users.filtered("role_id")._sync_groups_from_role()
|
users.filtered(
|
||||||
|
lambda user: user.role_ids or user.role_id
|
||||||
|
)._sync_groups_from_role()
|
||||||
return users
|
return users
|
||||||
|
|
||||||
def write(self, values):
|
def write(self, values):
|
||||||
role_changed = "role_id" in values
|
role_changed = "role_ids" in values or "role_id" in values
|
||||||
result = super().write(values)
|
result = super().write(values)
|
||||||
if role_changed:
|
if role_changed:
|
||||||
self._sync_groups_from_role()
|
self._sync_groups_from_role()
|
||||||
|
|
@ -61,14 +73,15 @@ class ResUsers(models.Model):
|
||||||
protected_groups = self._get_protected_role_group_ids()
|
protected_groups = self._get_protected_role_group_ids()
|
||||||
|
|
||||||
for user in self:
|
for user in self:
|
||||||
role_groups = user.role_id.group_ids
|
roles = user.role_ids | user.role_id
|
||||||
|
role_groups = roles.mapped("group_ids")
|
||||||
desired_groups = role_groups | role_groups.trans_implied_ids
|
desired_groups = role_groups | role_groups.trans_implied_ids
|
||||||
desired_groups -= protected_groups
|
desired_groups -= protected_groups
|
||||||
|
|
||||||
current_groups = user.groups_id
|
current_groups = user.groups_id
|
||||||
previously_managed = user.role_managed_group_ids
|
previously_managed = user.role_managed_group_ids
|
||||||
|
|
||||||
if user.role_id:
|
if roles:
|
||||||
groups_to_remove = current_groups - desired_groups - protected_groups
|
groups_to_remove = current_groups - desired_groups - protected_groups
|
||||||
direct_groups_to_add = role_groups - current_groups - protected_groups
|
direct_groups_to_add = role_groups - current_groups - protected_groups
|
||||||
managed_groups = desired_groups
|
managed_groups = desired_groups
|
||||||
|
|
@ -94,8 +107,8 @@ class ResUsers(models.Model):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
if not self.env.user.has_group("base.group_system"):
|
if not self.env.user.has_group("base.group_system"):
|
||||||
raise AccessError(_("Only Settings administrators can synchronize roles."))
|
raise AccessError(_("Only Settings administrators can synchronize roles."))
|
||||||
if not self.role_id:
|
if not (self.role_ids or self.role_id):
|
||||||
raise UserError(_("Select a role before fetching groups."))
|
raise UserError(_("Select at least one role before fetching groups."))
|
||||||
self._sync_groups_from_role()
|
self._sync_groups_from_role()
|
||||||
return {
|
return {
|
||||||
"type": "ir.actions.client",
|
"type": "ir.actions.client",
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,12 @@ class ResUsersRole(models.Model):
|
||||||
domain=lambda self: self._get_allowed_group_domain(),
|
domain=lambda self: self._get_allowed_group_domain(),
|
||||||
help="Security groups granted by this role. User-type groups are intentionally excluded.",
|
help="Security groups granted by this role. User-type groups are intentionally excluded.",
|
||||||
)
|
)
|
||||||
user_ids = fields.One2many(
|
|
||||||
comodel_name="res.users",
|
|
||||||
inverse_name="role_id",
|
|
||||||
string="Users",
|
|
||||||
readonly=True,
|
|
||||||
)
|
|
||||||
assigned_user_ids = fields.Many2many(
|
assigned_user_ids = fields.Many2many(
|
||||||
comodel_name="res.users",
|
comodel_name="res.users",
|
||||||
|
relation="res_users_role_user_rel",
|
||||||
|
column1="role_id",
|
||||||
|
column2="user_id",
|
||||||
string="Assigned Users",
|
string="Assigned Users",
|
||||||
compute="_compute_assigned_user_ids",
|
|
||||||
inverse="_inverse_assigned_user_ids",
|
|
||||||
domain=[("share", "=", False)],
|
domain=[("share", "=", False)],
|
||||||
help="Select internal users to assign this role. Their groups are synchronized automatically.",
|
help="Select internal users to assign this role. Their groups are synchronized automatically.",
|
||||||
)
|
)
|
||||||
|
|
@ -49,33 +44,15 @@ class ResUsersRole(models.Model):
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.depends("user_ids")
|
@api.depends("assigned_user_ids")
|
||||||
def _compute_user_count(self):
|
def _compute_user_count(self):
|
||||||
counts = self.env["res.users"]._read_group(
|
|
||||||
[("role_id", "in", self.ids)], ["role_id"], ["__count"]
|
|
||||||
)
|
|
||||||
count_by_role = {role.id: count for role, count in counts}
|
|
||||||
for role in self:
|
for role in self:
|
||||||
role.user_count = count_by_role.get(role.id, 0)
|
role.user_count = self.env["res.users"].with_context(
|
||||||
|
active_test=False
|
||||||
def _compute_assigned_user_ids(self):
|
).search_count(
|
||||||
users = self.env["res.users"].with_context(active_test=False).search(
|
["|", ("role_ids", "in", [role.id]), ("role_id", "=", role.id)]
|
||||||
[("role_id", "in", self.ids)]
|
|
||||||
)
|
|
||||||
for role in self:
|
|
||||||
role.assigned_user_ids = users.filtered(
|
|
||||||
lambda user: user.role_id == role
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _inverse_assigned_user_ids(self):
|
|
||||||
"""Apply additions/removals made through the role's user selector."""
|
|
||||||
Users = self.env["res.users"].with_context(active_test=False)
|
|
||||||
for role in self.filtered("id"):
|
|
||||||
current_users = Users.search([("role_id", "=", role.id)])
|
|
||||||
selected_users = role.assigned_user_ids
|
|
||||||
(current_users - selected_users).write({"role_id": False})
|
|
||||||
(selected_users - current_users).write({"role_id": role.id})
|
|
||||||
|
|
||||||
@api.constrains("group_ids")
|
@api.constrains("group_ids")
|
||||||
def _check_group_ids_do_not_include_user_types(self):
|
def _check_group_ids_do_not_include_user_types(self):
|
||||||
user_type_category = self.env.ref(
|
user_type_category = self.env.ref(
|
||||||
|
|
@ -97,19 +74,24 @@ class ResUsersRole(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
def write(self, values):
|
def write(self, values):
|
||||||
groups_changed = "group_ids" in values
|
sync_needed = "group_ids" in values or "assigned_user_ids" in values
|
||||||
result = super().write(values)
|
users_before = self.env["res.users"]
|
||||||
if groups_changed:
|
if sync_needed:
|
||||||
users = self.env["res.users"].with_context(active_test=False).search(
|
users_before = self.env["res.users"].with_context(active_test=False).search(
|
||||||
[("role_id", "in", self.ids)]
|
["|", ("role_ids", "in", self.ids), ("role_id", "in", self.ids)]
|
||||||
)
|
)
|
||||||
users._sync_groups_from_role()
|
result = super().write(values)
|
||||||
|
if sync_needed:
|
||||||
|
users_after = self.env["res.users"].with_context(active_test=False).search(
|
||||||
|
["|", ("role_ids", "in", self.ids), ("role_id", "in", self.ids)]
|
||||||
|
)
|
||||||
|
(users_before | users_after)._sync_groups_from_role()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def action_update_users(self):
|
def action_update_users(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
users = self.env["res.users"].with_context(active_test=False).search(
|
users = self.env["res.users"].with_context(active_test=False).search(
|
||||||
[("role_id", "=", self.id)]
|
["|", ("role_ids", "in", [self.id]), ("role_id", "=", self.id)]
|
||||||
)
|
)
|
||||||
users._sync_groups_from_role()
|
users._sync_groups_from_role()
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,15 @@
|
||||||
appears at the top of Access Rights, just above User Type. -->
|
appears at the top of Access Rights, just above User Type. -->
|
||||||
<xpath expr="//page[@name='access_rights']/field[@name='groups_id']" position="before">
|
<xpath expr="//page[@name='access_rights']/field[@name='groups_id']" position="before">
|
||||||
<group string="Role Access" groups="base.group_system">
|
<group string="Role Access" groups="base.group_system">
|
||||||
<field name="role_id"
|
<field name="role_ids"
|
||||||
string="Assigned Role"
|
string="Assigned Roles"
|
||||||
|
widget="many2many_tags"
|
||||||
options="{'no_create': True, 'no_create_edit': True}"/>
|
options="{'no_create': True, 'no_create_edit': True}"/>
|
||||||
<button name="action_fetch_role_groups"
|
<button name="action_fetch_role_groups"
|
||||||
string="Fetch Groups"
|
string="Fetch Groups"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
invisible="not role_id"/>
|
invisible="not role_ids"/>
|
||||||
</group>
|
</group>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@
|
||||||
'hr_holidays',
|
'hr_holidays',
|
||||||
'hr_timesheet',
|
'hr_timesheet',
|
||||||
'project',
|
'project',
|
||||||
|
'project_task_timesheet_extended'
|
||||||
],
|
],
|
||||||
|
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
|
'security/weekly_timesheet_rules.xml',
|
||||||
'views/hr_timesheet_inherit.xml',
|
'views/hr_timesheet_inherit.xml',
|
||||||
'views/weekly_period_timesheets.xml',
|
'views/weekly_period_timesheets.xml',
|
||||||
'views/week_timesheet.xml',
|
'views/week_timesheet.xml',
|
||||||
|
|
@ -22,4 +24,4 @@
|
||||||
|
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from datetime import timedelta
|
from datetime import date, timedelta
|
||||||
from odoo.exceptions import ValidationError, UserError
|
from odoo.exceptions import ValidationError, UserError
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ class WeeklyPeriodTimesheets(models.Model):
|
||||||
'hr.employee',
|
'hr.employee',
|
||||||
string="Employee",
|
string="Employee",
|
||||||
required=True,
|
required=True,
|
||||||
|
default=lambda self: self.env.user.employee_id.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
year_id = fields.Many2one(
|
year_id = fields.Many2one(
|
||||||
|
|
@ -54,6 +55,102 @@ class WeeklyPeriodTimesheets(models.Model):
|
||||||
('rejected', 'Rejected'),
|
('rejected', 'Rejected'),
|
||||||
], string="Status", default='draft')
|
], string="Status", default='draft')
|
||||||
|
|
||||||
|
can_current_user_refresh_weekly = fields.Boolean(
|
||||||
|
compute='_compute_can_current_user_refresh_weekly',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_can_current_user_refresh_weekly(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.can_current_user_refresh_weekly = (
|
||||||
|
rec.employee_id.user_id == self.env.user
|
||||||
|
and rec.state in ('draft', 'rejected')
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_current_week_defaults(self):
|
||||||
|
today = date.today()
|
||||||
|
year_name = str(today.year)
|
||||||
|
year = self.env['week.timesheet'].sudo().search([
|
||||||
|
('year', '=', year_name),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if not year:
|
||||||
|
year = self.env['week.timesheet'].sudo().create({
|
||||||
|
'year': year_name,
|
||||||
|
})
|
||||||
|
year.action_generate_weeks()
|
||||||
|
elif not year.week_line_ids:
|
||||||
|
year.action_generate_weeks()
|
||||||
|
|
||||||
|
week = self.env['week.timesheet.line'].sudo().search([
|
||||||
|
('week_timesheet_id', '=', year.id),
|
||||||
|
('date_from', '<=', today),
|
||||||
|
('date_to', '>=', today),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
return year, week
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields_list):
|
||||||
|
vals = super().default_get(fields_list)
|
||||||
|
|
||||||
|
if 'employee_id' in fields_list and not vals.get('employee_id'):
|
||||||
|
vals['employee_id'] = self.env.user.employee_id.id
|
||||||
|
|
||||||
|
year, week = self._get_current_week_defaults()
|
||||||
|
|
||||||
|
if 'year_id' in fields_list and not vals.get('year_id') and year:
|
||||||
|
vals['year_id'] = year.id
|
||||||
|
|
||||||
|
if 'week_line_id' in fields_list and not vals.get('week_line_id') and week:
|
||||||
|
vals['week_line_id'] = week.id
|
||||||
|
|
||||||
|
return vals
|
||||||
|
|
||||||
|
@api.onchange('year_id')
|
||||||
|
def _onchange_year_id(self):
|
||||||
|
if not self.year_id:
|
||||||
|
self.week_line_id = False
|
||||||
|
return
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
week = self.env['week.timesheet.line'].search([
|
||||||
|
('week_timesheet_id', '=', self.year_id.id),
|
||||||
|
('date_from', '<=', today),
|
||||||
|
('date_to', '>=', today),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if week:
|
||||||
|
self.week_line_id = week
|
||||||
|
|
||||||
|
def _is_weekly_admin(self):
|
||||||
|
return self.env.user.has_group('base.group_system')
|
||||||
|
|
||||||
|
def _is_employee_manager_for(self, employee):
|
||||||
|
return employee.parent_id.user_id == self.env.user
|
||||||
|
|
||||||
|
def _check_employee_can_create(self, vals_list):
|
||||||
|
if self._is_weekly_admin():
|
||||||
|
return
|
||||||
|
|
||||||
|
current_employee = self.env.user.employee_id
|
||||||
|
for vals in vals_list:
|
||||||
|
employee_id = vals.get('employee_id') or current_employee.id
|
||||||
|
if not current_employee or employee_id != current_employee.id:
|
||||||
|
raise UserError(_("Employees can create only their own weekly timesheets."))
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
self._check_employee_can_create(vals_list)
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if not self._is_weekly_admin() and 'employee_id' in vals:
|
||||||
|
current_employee = self.env.user.employee_id
|
||||||
|
if vals['employee_id'] != current_employee.id:
|
||||||
|
raise UserError(_("Employees can update only their own weekly timesheets."))
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
def action_submit(self):
|
def action_submit(self):
|
||||||
|
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -96,6 +193,11 @@ class WeeklyPeriodTimesheets(models.Model):
|
||||||
def action_reject(self):
|
def action_reject(self):
|
||||||
|
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
if not rec.can_current_user_approve_weekly:
|
||||||
|
raise UserError(
|
||||||
|
_("Only Employee Manager or Administrator can reject.")
|
||||||
|
)
|
||||||
|
|
||||||
analytic_lines = self.env[
|
analytic_lines = self.env[
|
||||||
'account.analytic.line'
|
'account.analytic.line'
|
||||||
].search([
|
].search([
|
||||||
|
|
@ -137,17 +239,15 @@ class WeeklyPeriodTimesheets(models.Model):
|
||||||
employee_manager = (
|
employee_manager = (
|
||||||
rec.employee_id.parent_id.user_id
|
rec.employee_id.parent_id.user_id
|
||||||
)
|
)
|
||||||
# Project Administrator
|
# Administrator
|
||||||
is_admin = self.env.user.has_group(
|
is_admin = self._is_weekly_admin()
|
||||||
'project.group_project_manager'
|
|
||||||
)
|
|
||||||
# Validation
|
# Validation
|
||||||
if (
|
if (
|
||||||
self.env.user != employee_manager
|
self.env.user != employee_manager
|
||||||
and not is_admin
|
and not is_admin
|
||||||
):
|
):
|
||||||
raise UserError(
|
raise UserError(
|
||||||
_("Only Employee Manager or Project Administrator can approve.")
|
_("Only Employee Manager or Administrator can approve.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure all PM approvals completed
|
# Ensure all PM approvals completed
|
||||||
|
|
@ -196,13 +296,19 @@ class WeeklyPeriodTimesheets(models.Model):
|
||||||
def action_refresh_timesheets(self):
|
def action_refresh_timesheets(self):
|
||||||
|
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
if not rec.can_current_user_refresh_weekly:
|
||||||
rec.timesheet_line_ids = [(5, 0, 0)]
|
raise UserError(
|
||||||
|
_("Only the employee can refresh their own draft or rejected weekly timesheet.")
|
||||||
|
)
|
||||||
|
|
||||||
if not rec.employee_id or not rec.week_line_id:
|
if not rec.employee_id or not rec.week_line_id:
|
||||||
return
|
continue
|
||||||
|
|
||||||
lines = []
|
lines_to_create = []
|
||||||
|
existing_lines_by_date = {
|
||||||
|
line.date: line
|
||||||
|
for line in rec.timesheet_line_ids
|
||||||
|
}
|
||||||
|
|
||||||
current_date = rec.week_line_id.date_from
|
current_date = rec.week_line_id.date_from
|
||||||
|
|
||||||
|
|
@ -218,15 +324,25 @@ class WeeklyPeriodTimesheets(models.Model):
|
||||||
analytic_lines.mapped('unit_amount')
|
analytic_lines.mapped('unit_amount')
|
||||||
)
|
)
|
||||||
|
|
||||||
lines.append((0, 0, {
|
existing_line = existing_lines_by_date.get(current_date)
|
||||||
|
values = {
|
||||||
'date': current_date,
|
'date': current_date,
|
||||||
'day_name': current_date.strftime("%A"),
|
'day_name': current_date.strftime("%A"),
|
||||||
'hours': total_hours,
|
'hours': total_hours,
|
||||||
}))
|
}
|
||||||
|
|
||||||
|
if analytic_lines:
|
||||||
|
if existing_line:
|
||||||
|
existing_line.write(values)
|
||||||
|
else:
|
||||||
|
lines_to_create.append((0, 0, values))
|
||||||
|
elif not existing_line:
|
||||||
|
lines_to_create.append((0, 0, values))
|
||||||
|
|
||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
rec.timesheet_line_ids = lines
|
if lines_to_create:
|
||||||
|
rec.timesheet_line_ids = lines_to_create
|
||||||
|
|
||||||
@api.onchange('employee_id', 'week_line_id')
|
@api.onchange('employee_id', 'week_line_id')
|
||||||
def _onchange_employee_week(self):
|
def _onchange_employee_week(self):
|
||||||
|
|
@ -261,7 +377,6 @@ class WeeklyPeriodTimesheets(models.Model):
|
||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
self.timesheet_line_ids = lines
|
self.timesheet_line_ids = lines
|
||||||
self.action_refresh_timesheets()
|
|
||||||
|
|
||||||
analytic_line_ids = fields.One2many(
|
analytic_line_ids = fields.One2many(
|
||||||
'account.analytic.line',
|
'account.analytic.line',
|
||||||
|
|
@ -290,6 +405,33 @@ class WeeklyPeriodTimesheets(models.Model):
|
||||||
all_pm_approved = fields.Boolean(
|
all_pm_approved = fields.Boolean(
|
||||||
compute='_compute_all_pm_approved'
|
compute='_compute_all_pm_approved'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
can_current_user_approve_weekly = fields.Boolean(
|
||||||
|
compute='_compute_can_current_user_approve_weekly',
|
||||||
|
search='_search_can_current_user_approve_weekly',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_can_current_user_approve_weekly(self):
|
||||||
|
is_admin = self._is_weekly_admin()
|
||||||
|
for rec in self:
|
||||||
|
rec.can_current_user_approve_weekly = (
|
||||||
|
is_admin
|
||||||
|
or rec._is_employee_manager_for(rec.employee_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _search_can_current_user_approve_weekly(self, operator, value):
|
||||||
|
if operator not in ('=', '!='):
|
||||||
|
return []
|
||||||
|
|
||||||
|
match_true = (operator == '=' and value) or (operator == '!=' and not value)
|
||||||
|
if self._is_weekly_admin():
|
||||||
|
return [] if match_true else [('id', '=', 0)]
|
||||||
|
|
||||||
|
domain = [('employee_id.parent_id.user_id', '=', self.env.uid)]
|
||||||
|
if match_true:
|
||||||
|
return domain
|
||||||
|
return ['!', domain[0]]
|
||||||
|
|
||||||
def _compute_all_pm_approved(self):
|
def _compute_all_pm_approved(self):
|
||||||
|
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -344,6 +486,17 @@ class AccountAnalyticLine(models.Model):
|
||||||
compute='_compute_pm_approval_required',
|
compute='_compute_pm_approval_required',
|
||||||
store=True
|
store=True
|
||||||
)
|
)
|
||||||
|
task_stage_id = fields.Many2one(
|
||||||
|
'project.task.type',
|
||||||
|
string="Stage",
|
||||||
|
related='task_id.stage_id',
|
||||||
|
store=True,
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
can_current_user_approve_line = fields.Boolean(
|
||||||
|
compute='_compute_can_current_user_approve_line',
|
||||||
|
search='_search_can_current_user_approve_line',
|
||||||
|
)
|
||||||
|
|
||||||
@api.depends(
|
@api.depends(
|
||||||
'task_id.is_generic',
|
'task_id.is_generic',
|
||||||
|
|
@ -357,6 +510,44 @@ class AccountAnalyticLine(models.Model):
|
||||||
or rec.project_id.privacy_visibility != 'followers'
|
or rec.project_id.privacy_visibility != 'followers'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _compute_can_current_user_approve_line(self):
|
||||||
|
is_admin = (
|
||||||
|
self.env.user.has_group('project.group_project_manager')
|
||||||
|
or self.env.user.has_group('base.group_system')
|
||||||
|
)
|
||||||
|
for rec in self:
|
||||||
|
rec.can_current_user_approve_line = (
|
||||||
|
is_admin
|
||||||
|
or rec.project_id.user_id == self.env.user
|
||||||
|
or (
|
||||||
|
'project_lead' in rec.project_id._fields
|
||||||
|
and rec.project_id.project_lead == self.env.user
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _search_can_current_user_approve_line(self, operator, value):
|
||||||
|
if operator not in ('=', '!='):
|
||||||
|
return []
|
||||||
|
|
||||||
|
match_true = (operator == '=' and value) or (operator == '!=' and not value)
|
||||||
|
if (
|
||||||
|
self.env.user.has_group('project.group_project_manager')
|
||||||
|
or self.env.user.has_group('base.group_system')
|
||||||
|
):
|
||||||
|
return [] if match_true else [('id', '=', 0)]
|
||||||
|
|
||||||
|
if 'project_lead' in self.env['project.project']._fields:
|
||||||
|
domain = [
|
||||||
|
'|',
|
||||||
|
('project_id.user_id', '=', self.env.uid),
|
||||||
|
('project_id.project_lead', '=', self.env.uid),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
domain = [('project_id.user_id', '=', self.env.uid)]
|
||||||
|
if match_true:
|
||||||
|
return domain
|
||||||
|
return ['!'] + domain
|
||||||
|
|
||||||
def action_pm_approve(self):
|
def action_pm_approve(self):
|
||||||
|
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -387,13 +578,19 @@ class AccountAnalyticLine(models.Model):
|
||||||
rec.project_id.user_id == self.env.user
|
rec.project_id.user_id == self.env.user
|
||||||
)
|
)
|
||||||
|
|
||||||
is_admin = self.env.user.has_group(
|
is_project_lead = (
|
||||||
'project.group_project_manager'
|
'project_lead' in rec.project_id._fields
|
||||||
|
and rec.project_id.project_lead == self.env.user
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (is_project_manager or is_admin):
|
is_admin = (
|
||||||
|
self.env.user.has_group('project.group_project_manager')
|
||||||
|
or self.env.user.has_group('base.group_system')
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (is_project_manager or is_project_lead or is_admin):
|
||||||
raise UserError(
|
raise UserError(
|
||||||
_("Only Project Manager or Administrator can approve.")
|
_("Only Project Manager, Project Lead or Administrator can approve.")
|
||||||
)
|
)
|
||||||
|
|
||||||
rec.approval_state = 'approved'
|
rec.approval_state = 'approved'
|
||||||
|
|
@ -424,13 +621,19 @@ class AccountAnalyticLine(models.Model):
|
||||||
rec.project_id.user_id == self.env.user
|
rec.project_id.user_id == self.env.user
|
||||||
)
|
)
|
||||||
|
|
||||||
is_admin = self.env.user.has_group(
|
is_project_lead = (
|
||||||
'project.group_project_manager'
|
'project_lead' in rec.project_id._fields
|
||||||
|
and rec.project_id.project_lead == self.env.user
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (is_project_manager or is_admin):
|
is_admin = (
|
||||||
|
self.env.user.has_group('project.group_project_manager')
|
||||||
|
or self.env.user.has_group('base.group_system')
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (is_project_manager or is_project_lead or is_admin):
|
||||||
raise UserError(
|
raise UserError(
|
||||||
_("Only Project Manager or Administrator can reject.")
|
_("Only Project Manager, Project Lead or Administrator can reject.")
|
||||||
)
|
)
|
||||||
|
|
||||||
rec.approval_state = 'rejected'
|
rec.approval_state = 'rejected'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="group_weekly_timesheet_manager" model="res.groups">
|
||||||
|
<field name="name">Weekly Timesheet Manager</field>
|
||||||
|
<field name="category_id" ref="base.module_category_hidden"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_weekly_periodtimesheets_employee_manager" model="ir.rule">
|
||||||
|
<field name="name">Weekly timesheets: own and managed employees</field>
|
||||||
|
<field name="model_id" ref="model_weekly_periodtimesheets"/>
|
||||||
|
<field name="domain_force">
|
||||||
|
['|',
|
||||||
|
('employee_id.user_id', '=', user.id),
|
||||||
|
('employee_id.parent_id.user_id', '=', user.id)
|
||||||
|
]
|
||||||
|
</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_weekly_periodtimesheets_admin" model="ir.rule">
|
||||||
|
<field name="name">Weekly timesheets: admin all</field>
|
||||||
|
<field name="model_id" ref="model_weekly_periodtimesheets"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_weekly_periodtimesheets_line_employee_manager" model="ir.rule">
|
||||||
|
<field name="name">Weekly timesheet lines: own and managed employees</field>
|
||||||
|
<field name="model_id" ref="model_weekly_periodtimesheets_line"/>
|
||||||
|
<field name="domain_force">
|
||||||
|
['|',
|
||||||
|
('weekly_timesheet_id.employee_id.user_id', '=', user.id),
|
||||||
|
('weekly_timesheet_id.employee_id.parent_id.user_id', '=', user.id)
|
||||||
|
]
|
||||||
|
</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_weekly_periodtimesheets_line_admin" model="ir.rule">
|
||||||
|
<field name="name">Weekly timesheet lines: admin all</field>
|
||||||
|
<field name="model_id" ref="model_weekly_periodtimesheets_line"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -71,16 +71,19 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//list" position="inside">
|
<xpath expr="//list" position="inside">
|
||||||
<field name="approval_state" optional="hide"/>
|
<field name="approval_state" optional="hide"/>
|
||||||
|
<field name="task_stage_id" string="Stage" optional="show"/>
|
||||||
|
<field name="weekly_submitted" optional="hide"/>
|
||||||
|
<field name="can_current_user_approve_line" optional="hide"/>
|
||||||
<button name="action_pm_approve"
|
<button name="action_pm_approve"
|
||||||
string="Approve"
|
string="Approve"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-success"
|
class="btn-success"
|
||||||
invisible="approval_state == 'approved' or not project_id "/>
|
invisible="approval_state == 'approved' or not project_id or not can_current_user_approve_line"/>
|
||||||
<button name="action_pm_reject"
|
<button name="action_pm_reject"
|
||||||
string="Reject"
|
string="Reject"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-danger"
|
class="btn-danger"
|
||||||
invisible="approval_state == 'rejected' or not project_id "/>
|
invisible="approval_state == 'rejected' or not project_id or not can_current_user_approve_line"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
@ -175,9 +178,9 @@
|
||||||
</field>
|
</field>
|
||||||
|
|
||||||
</record>
|
</record>
|
||||||
<record id="action_weekly_timesheet_approval"
|
<record id="action_project_timesheet_approval"
|
||||||
model="ir.actions.act_window">
|
model="ir.actions.act_window">
|
||||||
<field name="name">Weekly Timesheet Approval</field>
|
<field name="name">Project Timesheet Approval</field>
|
||||||
<field name="res_model">account.analytic.line</field>
|
<field name="res_model">account.analytic.line</field>
|
||||||
<field name="view_mode">list</field>
|
<field name="view_mode">list</field>
|
||||||
<field name="view_id"
|
<field name="view_id"
|
||||||
|
|
@ -186,7 +189,8 @@
|
||||||
[
|
[
|
||||||
('weekly_submitted','=',True),
|
('weekly_submitted','=',True),
|
||||||
('project_id.active','=',True),
|
('project_id.active','=',True),
|
||||||
('pm_approval_required','=',True)
|
('pm_approval_required','=',True),
|
||||||
|
('can_current_user_approve_line','=',True)
|
||||||
]
|
]
|
||||||
</field>
|
</field>
|
||||||
<field name="context">
|
<field name="context">
|
||||||
|
|
@ -213,7 +217,11 @@
|
||||||
name="Weekly Timesheet Approval"
|
name="Weekly Timesheet Approval"
|
||||||
parent="hr_timesheet.timesheet_menu_root"
|
parent="hr_timesheet.timesheet_menu_root"
|
||||||
action="action_weekly_timesheet_approval"
|
action="action_weekly_timesheet_approval"
|
||||||
|
groups="weekly_timesheets.group_weekly_timesheet_manager,base.group_system"
|
||||||
sequence="200"/>
|
sequence="200"/>
|
||||||
|
<record id="menu_project_timesheet_approval" model="ir.ui.menu">
|
||||||
|
<field name="active" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -17,25 +17,28 @@
|
||||||
string="Reject"
|
string="Reject"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
invisible="state != 'submitted'"/>
|
invisible="state != 'submitted' or not can_current_user_approve_weekly"/>
|
||||||
<button name="action_approve"
|
<button name="action_approve"
|
||||||
string="Approve"
|
string="Approve"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
invisible="state != 'submitted' or not all_pm_approved"/>
|
invisible="state != 'submitted' or not all_pm_approved or not can_current_user_approve_weekly"/>
|
||||||
<button name="action_reset_to_draft"
|
<button name="action_reset_to_draft"
|
||||||
string="Reset to Draft"
|
string="Reset to Draft"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
invisible="state == 'draft'"/>
|
invisible="state != 'rejected'"/>
|
||||||
<button name="action_refresh_timesheets"
|
<button name="action_refresh_timesheets"
|
||||||
string="Refresh"
|
string="Refresh"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
invisible="state in ('submitted','approved')"/>
|
invisible="context.get('weekly_timesheet_approval') or not can_current_user_refresh_weekly"/>
|
||||||
|
|
||||||
<field name="state"
|
<field name="state"
|
||||||
widget="statusbar"/>
|
widget="statusbar"/>
|
||||||
|
<field name="all_pm_approved" invisible="1"/>
|
||||||
|
<field name="can_current_user_approve_weekly" invisible="1"/>
|
||||||
|
<field name="can_current_user_refresh_weekly" invisible="1"/>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -43,7 +46,9 @@
|
||||||
|
|
||||||
<group>
|
<group>
|
||||||
<field name="employee_id"
|
<field name="employee_id"
|
||||||
readonly="state in ('submitted','approved')"/>
|
readonly="1"
|
||||||
|
domain="[('user_id', '=', uid)]"
|
||||||
|
options="{'no_open': True, 'no_create': True}"/>
|
||||||
|
|
||||||
<field name="year_id"
|
<field name="year_id"
|
||||||
readonly="state in ('submitted','approved')"/>
|
readonly="state in ('submitted','approved')"/>
|
||||||
|
|
@ -77,17 +82,21 @@
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="project_id"/>
|
<field name="project_id"/>
|
||||||
<field name="task_id"/>
|
<field name="task_id"/>
|
||||||
|
<field name="task_stage_id" string="Stage"/>
|
||||||
<field name="unit_amount" string="Time Spent"/>
|
<field name="unit_amount" string="Time Spent"/>
|
||||||
|
<field name="approval_state"/>
|
||||||
|
<field name="weekly_submitted" column_invisible="1"/>
|
||||||
|
<field name="can_current_user_approve_line" column_invisible="1"/>
|
||||||
<button name="action_pm_approve"
|
<button name="action_pm_approve"
|
||||||
string="Approve"
|
string="Approve"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-success"
|
class="btn-success"
|
||||||
invisible="approval_state == 'approved' or not project_id or not weekly_submitted"/>
|
invisible="approval_state == 'approved' or not project_id or not weekly_submitted or not can_current_user_approve_line"/>
|
||||||
<button name="action_pm_reject"
|
<button name="action_pm_reject"
|
||||||
string="Reject"
|
string="Reject"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-danger"
|
class="btn-danger"
|
||||||
invisible="approval_state == 'rejected' or not project_id or not weekly_submitted"/>
|
invisible="approval_state == 'rejected' or not project_id or not weekly_submitted or not can_current_user_approve_line"/>
|
||||||
|
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
|
|
@ -109,18 +118,36 @@
|
||||||
<field name="employee_id"/>
|
<field name="employee_id"/>
|
||||||
<field name="year_id"/>
|
<field name="year_id"/>
|
||||||
<field name="week_line_id"/>
|
<field name="week_line_id"/>
|
||||||
|
<field name="total_hours"/>
|
||||||
|
<field name="state"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="action_weekly_period_timesheets" model="ir.actions.act_window">
|
<record id="action_weekly_period_timesheets" model="ir.actions.act_window">
|
||||||
<field name="res_model">weekly.periodtimesheets</field>
|
<field name="res_model">weekly.periodtimesheets</field>
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
|
||||||
|
<field name="context">{'weekly_timesheet_approval': 0}</field>
|
||||||
<field name="view_ids"
|
<field name="view_ids"
|
||||||
eval="[(5, 0, 0),
|
eval="[(5, 0, 0),
|
||||||
(0, 0, {'view_mode': 'list', 'view_id': ref('view_weekly_period_timesheets_list')}),
|
(0, 0, {'view_mode': 'list', 'view_id': ref('view_weekly_period_timesheets_list')}),
|
||||||
(0, 0, {'view_mode': 'form', 'view_id': ref('view_weekly_period_timesheets_form')})]"/>
|
(0, 0, {'view_mode': 'form', 'view_id': ref('view_weekly_period_timesheets_form')})]"/>
|
||||||
|
|
||||||
</record>
|
</record>
|
||||||
|
<record id="action_weekly_timesheet_approval" model="ir.actions.act_window">
|
||||||
|
<field name="name">Weekly Timesheet Approval</field>
|
||||||
|
<field name="res_model">weekly.periodtimesheets</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">[
|
||||||
|
('state', '=', 'submitted'),
|
||||||
|
('can_current_user_approve_weekly', '=', True)
|
||||||
|
]</field>
|
||||||
|
<field name="context">{'weekly_timesheet_approval': 1}</field>
|
||||||
|
<field name="view_ids"
|
||||||
|
eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'list', 'view_id': ref('view_weekly_period_timesheets_list')}),
|
||||||
|
(0, 0, {'view_mode': 'form', 'view_id': ref('view_weekly_period_timesheets_form')})]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue