diff --git a/addons_extensions/role_based_group_management/__manifest__.py b/addons_extensions/role_based_group_management/__manifest__.py
index a447ce883..465bfede1 100644
--- a/addons_extensions/role_based_group_management/__manifest__.py
+++ b/addons_extensions/role_based_group_management/__manifest__.py
@@ -1,7 +1,7 @@
{
"name": "Role-Based Group Management",
"summary": "Manage reusable security-group roles for users",
- "version": "18.0.1.4.0",
+ "version": "18.0.1.6.0",
"category": "Administration/Access Rights",
"license": "LGPL-3",
"author": "Custom",
diff --git a/addons_extensions/role_based_group_management/models/__init__.py b/addons_extensions/role_based_group_management/models/__init__.py
index cccafcc0b..beaab4982 100644
--- a/addons_extensions/role_based_group_management/models/__init__.py
+++ b/addons_extensions/role_based_group_management/models/__init__.py
@@ -1,2 +1,2 @@
-from . import res_users
from . import user_role
+from . import res_users
diff --git a/addons_extensions/role_based_group_management/models/res_users.py b/addons_extensions/role_based_group_management/models/res_users.py
index cf2c3926e..47b7fe46d 100644
--- a/addons_extensions/role_based_group_management/models/res_users.py
+++ b/addons_extensions/role_based_group_management/models/res_users.py
@@ -7,11 +7,21 @@ class ResUsers(models.Model):
role_id = fields.Many2one(
comodel_name="res.users.role",
- string="Role",
+ string="Legacy Role",
groups="base.group_system",
ondelete="set null",
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(
comodel_name="res.groups",
@@ -28,11 +38,13 @@ class ResUsers(models.Model):
@api.model_create_multi
def create(self, 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
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)
if role_changed:
self._sync_groups_from_role()
@@ -61,14 +73,15 @@ class ResUsers(models.Model):
protected_groups = self._get_protected_role_group_ids()
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 -= protected_groups
current_groups = user.groups_id
previously_managed = user.role_managed_group_ids
- if user.role_id:
+ if roles:
groups_to_remove = current_groups - desired_groups - protected_groups
direct_groups_to_add = role_groups - current_groups - protected_groups
managed_groups = desired_groups
@@ -94,8 +107,8 @@ class ResUsers(models.Model):
self.ensure_one()
if not self.env.user.has_group("base.group_system"):
raise AccessError(_("Only Settings administrators can synchronize roles."))
- if not self.role_id:
- raise UserError(_("Select a role before fetching groups."))
+ if not (self.role_ids or self.role_id):
+ raise UserError(_("Select at least one role before fetching groups."))
self._sync_groups_from_role()
return {
"type": "ir.actions.client",
diff --git a/addons_extensions/role_based_group_management/models/user_role.py b/addons_extensions/role_based_group_management/models/user_role.py
index bc8f96293..3ddbdf683 100644
--- a/addons_extensions/role_based_group_management/models/user_role.py
+++ b/addons_extensions/role_based_group_management/models/user_role.py
@@ -17,17 +17,12 @@ class ResUsersRole(models.Model):
domain=lambda self: self._get_allowed_group_domain(),
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(
comodel_name="res.users",
+ relation="res_users_role_user_rel",
+ column1="role_id",
+ column2="user_id",
string="Assigned Users",
- compute="_compute_assigned_user_ids",
- inverse="_inverse_assigned_user_ids",
domain=[("share", "=", False)],
help="Select internal users to assign this role. Their groups are synchronized automatically.",
)
@@ -49,33 +44,15 @@ class ResUsersRole(models.Model):
else []
)
- @api.depends("user_ids")
+ @api.depends("assigned_user_ids")
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:
- role.user_count = count_by_role.get(role.id, 0)
-
- def _compute_assigned_user_ids(self):
- users = self.env["res.users"].with_context(active_test=False).search(
- [("role_id", "in", self.ids)]
- )
- for role in self:
- role.assigned_user_ids = users.filtered(
- lambda user: user.role_id == role
+ role.user_count = self.env["res.users"].with_context(
+ active_test=False
+ ).search_count(
+ ["|", ("role_ids", "in", [role.id]), ("role_id", "=", role.id)]
)
- 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")
def _check_group_ids_do_not_include_user_types(self):
user_type_category = self.env.ref(
@@ -97,19 +74,24 @@ class ResUsersRole(models.Model):
)
def write(self, values):
- groups_changed = "group_ids" in values
- result = super().write(values)
- if groups_changed:
- users = self.env["res.users"].with_context(active_test=False).search(
- [("role_id", "in", self.ids)]
+ sync_needed = "group_ids" in values or "assigned_user_ids" in values
+ users_before = self.env["res.users"]
+ if sync_needed:
+ users_before = self.env["res.users"].with_context(active_test=False).search(
+ ["|", ("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
def action_update_users(self):
self.ensure_one()
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()
return {
diff --git a/addons_extensions/role_based_group_management/views/res_users_views.xml b/addons_extensions/role_based_group_management/views/res_users_views.xml
index 53be917c0..4a7d71fab 100644
--- a/addons_extensions/role_based_group_management/views/res_users_views.xml
+++ b/addons_extensions/role_based_group_management/views/res_users_views.xml
@@ -10,14 +10,15 @@
appears at the top of Access Rights, just above User Type. -->
-
+ invisible="not role_ids"/>
diff --git a/addons_extensions/weekly_timesheets/__manifest__.py b/addons_extensions/weekly_timesheets/__manifest__.py
index 2a3b55ccd..4f67f574d 100644
--- a/addons_extensions/weekly_timesheets/__manifest__.py
+++ b/addons_extensions/weekly_timesheets/__manifest__.py
@@ -10,10 +10,12 @@
'hr_holidays',
'hr_timesheet',
'project',
+ 'project_task_timesheet_extended'
],
'data': [
'security/ir.model.access.csv',
+ 'security/weekly_timesheet_rules.xml',
'views/hr_timesheet_inherit.xml',
'views/weekly_period_timesheets.xml',
'views/week_timesheet.xml',
@@ -22,4 +24,4 @@
'installable': True,
'application': False,
-}
\ No newline at end of file
+}
diff --git a/addons_extensions/weekly_timesheets/models/weekly_period_timesheets.py b/addons_extensions/weekly_timesheets/models/weekly_period_timesheets.py
index 2b9e6ac93..abf4dd159 100644
--- a/addons_extensions/weekly_timesheets/models/weekly_period_timesheets.py
+++ b/addons_extensions/weekly_timesheets/models/weekly_period_timesheets.py
@@ -1,5 +1,5 @@
from odoo import api, fields, models, _
-from datetime import timedelta
+from datetime import date, timedelta
from odoo.exceptions import ValidationError, UserError
@@ -12,6 +12,7 @@ class WeeklyPeriodTimesheets(models.Model):
'hr.employee',
string="Employee",
required=True,
+ default=lambda self: self.env.user.employee_id.id,
)
year_id = fields.Many2one(
@@ -54,6 +55,102 @@ class WeeklyPeriodTimesheets(models.Model):
('rejected', 'Rejected'),
], 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):
for rec in self:
@@ -96,6 +193,11 @@ class WeeklyPeriodTimesheets(models.Model):
def action_reject(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[
'account.analytic.line'
].search([
@@ -137,17 +239,15 @@ class WeeklyPeriodTimesheets(models.Model):
employee_manager = (
rec.employee_id.parent_id.user_id
)
- # Project Administrator
- is_admin = self.env.user.has_group(
- 'project.group_project_manager'
- )
+ # Administrator
+ is_admin = self._is_weekly_admin()
# Validation
if (
self.env.user != employee_manager
and not is_admin
):
raise UserError(
- _("Only Employee Manager or Project Administrator can approve.")
+ _("Only Employee Manager or Administrator can approve.")
)
# Ensure all PM approvals completed
@@ -196,13 +296,19 @@ class WeeklyPeriodTimesheets(models.Model):
def action_refresh_timesheets(self):
for rec in self:
-
- rec.timesheet_line_ids = [(5, 0, 0)]
+ if not rec.can_current_user_refresh_weekly:
+ 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:
- 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
@@ -218,15 +324,25 @@ class WeeklyPeriodTimesheets(models.Model):
analytic_lines.mapped('unit_amount')
)
- lines.append((0, 0, {
+ existing_line = existing_lines_by_date.get(current_date)
+ values = {
'date': current_date,
'day_name': current_date.strftime("%A"),
'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)
- rec.timesheet_line_ids = lines
+ if lines_to_create:
+ rec.timesheet_line_ids = lines_to_create
@api.onchange('employee_id', 'week_line_id')
def _onchange_employee_week(self):
@@ -261,7 +377,6 @@ class WeeklyPeriodTimesheets(models.Model):
current_date += timedelta(days=1)
self.timesheet_line_ids = lines
- self.action_refresh_timesheets()
analytic_line_ids = fields.One2many(
'account.analytic.line',
@@ -290,6 +405,33 @@ class WeeklyPeriodTimesheets(models.Model):
all_pm_approved = fields.Boolean(
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):
for rec in self:
@@ -344,6 +486,17 @@ class AccountAnalyticLine(models.Model):
compute='_compute_pm_approval_required',
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(
'task_id.is_generic',
@@ -357,6 +510,44 @@ class AccountAnalyticLine(models.Model):
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):
for rec in self:
@@ -387,13 +578,19 @@ class AccountAnalyticLine(models.Model):
rec.project_id.user_id == self.env.user
)
- is_admin = self.env.user.has_group(
- 'project.group_project_manager'
+ is_project_lead = (
+ '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(
- _("Only Project Manager or Administrator can approve.")
+ _("Only Project Manager, Project Lead or Administrator can approve.")
)
rec.approval_state = 'approved'
@@ -424,13 +621,19 @@ class AccountAnalyticLine(models.Model):
rec.project_id.user_id == self.env.user
)
- is_admin = self.env.user.has_group(
- 'project.group_project_manager'
+ is_project_lead = (
+ '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(
- _("Only Project Manager or Administrator can reject.")
+ _("Only Project Manager, Project Lead or Administrator can reject.")
)
rec.approval_state = 'rejected'
diff --git a/addons_extensions/weekly_timesheets/security/weekly_timesheet_rules.xml b/addons_extensions/weekly_timesheets/security/weekly_timesheet_rules.xml
new file mode 100644
index 000000000..9e4585bc1
--- /dev/null
+++ b/addons_extensions/weekly_timesheets/security/weekly_timesheet_rules.xml
@@ -0,0 +1,61 @@
+
+
+
+ Weekly Timesheet Manager
+
+
+
+
+ Weekly timesheets: own and managed employees
+
+
+ ['|',
+ ('employee_id.user_id', '=', user.id),
+ ('employee_id.parent_id.user_id', '=', user.id)
+ ]
+
+
+
+
+
+
+
+
+
+ Weekly timesheets: admin all
+
+ [(1, '=', 1)]
+
+
+
+
+
+
+
+
+ Weekly timesheet lines: own and managed employees
+
+
+ ['|',
+ ('weekly_timesheet_id.employee_id.user_id', '=', user.id),
+ ('weekly_timesheet_id.employee_id.parent_id.user_id', '=', user.id)
+ ]
+
+
+
+
+
+
+
+
+
+ Weekly timesheet lines: admin all
+
+ [(1, '=', 1)]
+
+
+
+
+
+
+
diff --git a/addons_extensions/weekly_timesheets/views/week_timesheet.xml b/addons_extensions/weekly_timesheets/views/week_timesheet.xml
index 94acfce36..df9aeb18a 100644
--- a/addons_extensions/weekly_timesheets/views/week_timesheet.xml
+++ b/addons_extensions/weekly_timesheets/views/week_timesheet.xml
@@ -71,16 +71,19 @@
+
+
+
+ invisible="approval_state == 'approved' or not project_id or not can_current_user_approve_line"/>
+ invisible="approval_state == 'rejected' or not project_id or not can_current_user_approve_line"/>
@@ -175,9 +178,9 @@
-
- Weekly Timesheet Approval
+ Project Timesheet Approval
account.analytic.line
list
@@ -213,7 +217,11 @@
name="Weekly Timesheet Approval"
parent="hr_timesheet.timesheet_menu_root"
action="action_weekly_timesheet_approval"
+ groups="weekly_timesheets.group_weekly_timesheet_manager,base.group_system"
sequence="200"/>
+
-
\ No newline at end of file
+
diff --git a/addons_extensions/weekly_timesheets/views/weekly_period_timesheets.xml b/addons_extensions/weekly_timesheets/views/weekly_period_timesheets.xml
index 8759880fa..ed1762792 100644
--- a/addons_extensions/weekly_timesheets/views/weekly_period_timesheets.xml
+++ b/addons_extensions/weekly_timesheets/views/weekly_period_timesheets.xml
@@ -17,25 +17,28 @@
string="Reject"
type="object"
class="btn-primary"
- invisible="state != 'submitted'"/>
+ invisible="state != 'submitted' or not can_current_user_approve_weekly"/>
+ invisible="state != 'submitted' or not all_pm_approved or not can_current_user_approve_weekly"/>
+ invisible="state != 'rejected'"/>
+ invisible="context.get('weekly_timesheet_approval') or not can_current_user_refresh_weekly"/>
+
+
+
@@ -43,7 +46,9 @@
+ readonly="1"
+ domain="[('user_id', '=', uid)]"
+ options="{'no_open': True, 'no_create': True}"/>
@@ -77,17 +82,21 @@
+
+
+
+
+ invisible="approval_state == 'approved' or not project_id or not weekly_submitted or not can_current_user_approve_line"/>
+ invisible="approval_state == 'rejected' or not project_id or not weekly_submitted or not can_current_user_approve_line"/>
@@ -109,18 +118,36 @@
+
+
weekly.periodtimesheets
list,form
+ [('employee_id.user_id', '=', uid)]
+ {'weekly_timesheet_approval': 0}
+
+ Weekly Timesheet Approval
+ weekly.periodtimesheets
+ list,form
+ [
+ ('state', '=', 'submitted'),
+ ('can_current_user_approve_weekly', '=', True)
+ ]
+ {'weekly_timesheet_approval': 1}
+
+
-
\ No newline at end of file
+