Merge pull request 'feature/share_module' (#22) from feature/share_module into srivyn_test

Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/22
This commit is contained in:
pranay 2026-07-13 17:00:38 +05:30
commit ebe6146e15
26 changed files with 634 additions and 163 deletions

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
{
'name': 'Base Changes',
'category': 'Base',
'version': '1.1',
'author': 'Seshi Kanth',
'summary': 'Changes of the form',
'description': 'This module contains form changes.',
'depends': [
'base',
'web',
],
'data': [
# 'views/user_menu.xml',
],
'assets': {
'web.assets_backend': [
'base_custom/static/src/css/backend.css',
'base_custom/static/src/js/user_menu_patch.js',
'base_custom/static/src/js/hide_user_menu.js',
'base_custom/static/src/xml/user_menu.xml',
],
},
'installable': True,
'application': True,
'license': 'LGPL-3',
}

View File

@ -0,0 +1 @@
from . import ir_http

View File

@ -0,0 +1,17 @@
from odoo import models
class IrHttp(models.AbstractModel):
_inherit = "ir.http"
def session_info(self):
session_info = super().session_info()
employee = self.env.user.employee_id
session_info.update({
"employee_name": employee.name or self.env.user.name,
"employee_designation": employee.job_id.name if employee.job_id else "",
})
return session_info

View File

@ -0,0 +1,23 @@
.o_switch_company_menu .oe_topbar_name{
display: none !important;
}
.oe_topbar_name{
display:flex;
flex-direction:column;
line-height:1.2;
}
.oe_topbar_name .fw-bold{
font-size:13px;
font-weight:600;
}
.oe_topbar_name .designation{
font-size:11px;
color:#7b7b7b;
}
.text-muted {
color: #000000 !important;
}

View File

@ -0,0 +1,18 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
const userMenuRegistry = registry.category("user_menuitems");
const itemsToHide = [
"documentation",
"support",
"shortcuts",
"odoo_account",
];
for (const item of itemsToHide) {
if (userMenuRegistry.contains(item)) {
userMenuRegistry.remove(item);
}
}

View File

@ -0,0 +1,23 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { UserMenu } from "@web/webclient/user_menu/user_menu";
import { session } from "@web/session";
patch(UserMenu.prototype, {
setup() {
super.setup();
this.employeeName = session.employee_name;
this.employeeDesignation = session.employee_designation;
},
get employeeInfo() {
return {
employeeName: this.employeeName,
employeeDesignation: this.employeeDesignation,
};
},
});

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="web.UserMenu"
t-inherit-mode="extension">
<xpath expr="//small[contains(@class,'oe_topbar_name')]"
position="replace">
<small class="oe_topbar_name d-none d-lg-inline-block ms-2 text-start"
style="max-width:220px">
<div class="fw-bold text-truncate">
<t t-esc="employeeInfo.employeeName"/>
</div>
<div class="text-muted text-truncate designation">
<t t-esc="employeeInfo.employeeDesignation"/>
</div>
</small>
</xpath>
</t>
</templates>

View File

@ -52,7 +52,7 @@
</button>
</div>
<div class="hrms-filter-box">
<label>Period</label>
<span>Period</span>
<select class="form-select form-select-sm" t-on-change="onPeriodChange" t-att-value="state.period">
<option value="this_month" t-att-selected="state.period === 'this_month'">This Month</option>
<option value="this_year" t-att-selected="state.period === 'this_year'">This Year</option>

View File

@ -1067,13 +1067,15 @@ class projectTask(models.Model):
}
def _get_stage_responsible_user(self, stage):
self.ensure_one()
if stage.approval_by == 'assigned_team_lead' and stage.team_id.team_lead:
if stage.team_id and stage.team_id.team_lead:
return stage.team_id.team_lead
if stage.approval_by == 'project_manager' and self.project_id.user_id:
return self.project_id.user_id
if stage.approval_by == 'project_lead' and self.project_id.project_lead:
return self.project_id.project_lead
return self.project_id.project_lead or self.project_id.user_id
return self.env['res.users']
def _get_stage_assignee_user(self, stage):
self.ensure_one()
if stage.involved_user_ids:
return stage.involved_user_ids[:1]
return self.env['res.users']
def _ensure_task_timeline_rows(self):
Timeline = self.env['project.task.time.lines'].sudo()
@ -1081,14 +1083,21 @@ class projectTask(models.Model):
stages = task.project_id.type_ids.filtered(lambda s: not s.fold)
for stage in stages:
line = task.assignees_timelines.filtered(lambda t: t.stage_id == stage)[:1]
if line:
continue
responsible_user = task._get_stage_responsible_user(stage)
assignee_user = task._get_stage_assignee_user(stage)
if line:
if not task.timelines_requested:
line.sudo().write({
'team_id': stage.team_id.id if stage.team_id else False,
'responsible_lead': responsible_user.id if responsible_user else False,
'assigned_to': assignee_user.id if assignee_user else False,
})
continue
Timeline.create({
'stage_id': stage.id,
'team_id': stage.team_id.id if stage.team_id else False,
'responsible_lead': responsible_user.id if responsible_user else False,
'assigned_to': responsible_user.id if responsible_user else False,
'assigned_to': assignee_user.id if assignee_user else False,
'estimated_time': 0.0,
'task_id': task.id,
})

View File

@ -10,6 +10,12 @@ class TaskStages(models.Model):
string='Approval Owner',
)
involved_user_ids = fields.Many2many('res.users', string='Related Users')
responsible_user_id = fields.Many2one(
'res.users',
string='Responsible User',
related='team_id.team_lead',
readonly=True,
)
team_related_user_ids = fields.Many2many(related='team_id.all_members_ids', string='Team Users')
is_workflow_template = fields.Boolean(
string='Workflow Template Stage',

View File

@ -38,27 +38,27 @@
<value eval="{'noupdate': True}"/>
</function>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'project'), ('name', '=', 'project_project_manager_rule')]"/>
</function>
<value eval="{'noupdate': False}"/>
</function>
<record id="project.project_project_manager_rule" model="ir.rule">
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'project'), ('name', '=', 'project_project_manager_rule')]"/>
</function>
<value eval="{'noupdate': True}"/>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'project'), ('name', '=', 'project_project_manager_rule')]"/>
</function>
<value eval="{'noupdate': False}"/>
</function>
<record id="project.project_project_manager_rule" model="ir.rule">
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'project'), ('name', '=', 'project_project_manager_rule')]"/>
</function>
<value eval="{'noupdate': True}"/>
</function>
<record id="portfolio_rule_company_projects" model="ir.rule">
<field name="name">company: Own Company</field>
@ -122,27 +122,27 @@
</record>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'project'), ('name', '=', 'project_manager_all_project_tasks_rule')]"/>
</function>
<value eval="{'noupdate': False}"/>
</function>
<record id="project.project_manager_all_project_tasks_rule" model="ir.rule">
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'project'), ('name', '=', 'project_manager_all_project_tasks_rule')]"/>
</function>
<value eval="{'noupdate': True}"/>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'project'), ('name', '=', 'project_manager_all_project_tasks_rule')]"/>
</function>
<value eval="{'noupdate': False}"/>
</function>
<record id="project.project_manager_all_project_tasks_rule" model="ir.rule">
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
<function name="write" model="ir.model.data">
<function name="search" model="ir.model.data">
<value eval="[('module', '=', 'project'), ('name', '=', 'project_manager_all_project_tasks_rule')]"/>
</function>
<value eval="{'noupdate': True}"/>
</function>
<record model="ir.rule" id="project.ir_rule_private_task">
<field name="domain_force">[
@ -279,6 +279,20 @@
]
</field>
</record>
<record model="ir.rule" id="user_task_follower_view_projets_follower_required">
<field name="name">Project: employees: following required for follower-only projects</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="domain_force">
[
('privacy_visibility', '=', 'followers'),
('members_ids', 'in', [user.id])
]
</field>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
</record>
<!-- <record model="ir.rule" id="timesheet_users_normal_timesheets">-->
<!-- <field name="name">timesheet: users: see own tasks</field>-->

View File

@ -117,11 +117,12 @@
<page name="task_stages" string="Task Stages" invisible="not is_project_editor">
<field name="type_ids" context="{'default_project_id': id, 'project_stage_project_id': id}">
<list editable="bottom" open_form_view="True" delete="0">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="team_id" string="Assigned Team"/>
<field name="approval_by" string="Approval Owner"/>
<field name="fold"/>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="team_id" string="Assigned Team"/>
<field name="responsible_user_id" string="Responsible User" readonly="1"/>
<field name="approval_by" string="Approval Owner"/>
<field name="fold"/>
<field name="team_related_user_ids" invisible="1" column_invisible="1"/>
<field name="involved_user_ids" widget="many2many_tags" domain="[('id', 'in', team_related_user_ids)]"/>
<field name="is_workflow_template" invisible="1" column_invisible="1"/>

View File

@ -7,6 +7,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='fold']" position="after">
<field name="team_id"/>
<field name="responsible_user_id"/>
<field name="approval_by"/>
<field name="team_related_user_ids" invisible="1"/>
<field name="involved_user_ids" widget="many2many_tags" domain="[('id', 'in', team_related_user_ids)]"/>
@ -22,6 +23,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="team_id" optional="show"/>
<field name="responsible_user_id" optional="show"/>
<field name="approval_by" optional="show"/>
<field name="team_related_user_ids" invisible="1"/>
<field name="involved_user_ids" optional="show" domain="[('id', 'in', team_related_user_ids)]"/>

View File

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

View File

@ -1,2 +1,2 @@
from . import res_users
from . import user_role
from . import res_users

View File

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

View File

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

View File

@ -10,14 +10,15 @@
appears at the top of Access Rights, just above User Type. -->
<xpath expr="//page[@name='access_rights']/field[@name='groups_id']" position="before">
<group string="Role Access" groups="base.group_system">
<field name="role_id"
string="Assigned Role"
<field name="role_ids"
string="Assigned Roles"
widget="many2many_tags"
options="{'no_create': True, 'no_create_edit': True}"/>
<button name="action_fetch_role_groups"
string="Fetch Groups"
type="object"
class="btn-secondary"
invisible="not role_id"/>
invisible="not role_ids"/>
</group>
</xpath>
</field>

View File

@ -9,11 +9,12 @@
/*}*/
.srivyn_brand_footer{
position: fixed;
position: relative;
bottom: 10px;
right: 20px;
/*right: 20px;*/
left: 260px;
color: #fff;
font-size: 18px;
font-size: 10px;
z-index: 1000;
}

View File

@ -3,7 +3,7 @@
<!-- Increase login form width -->
<xpath expr="//form" position="attributes">
<attribute name="style">max-width:250px;margin:auto;padding-top:70px;;</attribute>
<attribute name="style">max-width:280px;margin:auto;padding-top:95px;;</attribute>
</xpath>
<!-- &lt;!&ndash; Company Logo &ndash;&gt;-->
@ -16,26 +16,24 @@
<!-- </xpath>-->
<!-- Footer -->
<!-- <xpath expr="//select[@name='master_select']" position="after">-->
<!-- <div class="text-center mt-3">-->
<!-- <small class="text-muted">-->
<!-- Powered by-->
<!-- <strong>SRIVYN PLATFORMS</strong>-->
<!-- </small>-->
<!-- </div>-->
<!-- </xpath>-->
<!-- <xpath expr="//select[@name='master_select']" position="after">-->
<!-- <div class="text-center mt-3">-->
<!-- <small class="text-muted">-->
<!-- Powered by-->
<!-- <strong>SRIVYN PLATFORMS</strong>-->
<!-- </small>-->
<!-- </div>-->
<!-- </xpath>-->
</template>
<odoo>
<template id="brand_promotion_inherit"
inherit_id="web.brand_promotion">
<!-- Powered by -->
<template id="brand_promotion_inherit" inherit_id="web.brand_promotion">
<xpath expr="//div[@class='o_brand_promotion']" position="replace">
<div class="o_brand_promotion srivyn_brand_footer">
Powered by <strong>SRIVYN PLATFORMS PVT.LTD</strong>
Powered by
<span>SRIVYN PLATFORMS PVT</span>
</div>
</xpath>
</template>
</odoo>
</odoo>

View File

@ -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,
}
}

View File

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

View File

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

View File

@ -71,16 +71,19 @@
<field name="arch" type="xml">
<xpath expr="//list" position="inside">
<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"
string="Approve"
type="object"
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"
string="Reject"
type="object"
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>
</field>
</record>
@ -175,9 +178,9 @@
</field>
</record>
<record id="action_weekly_timesheet_approval"
<record id="action_project_timesheet_approval"
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="view_mode">list</field>
<field name="view_id"
@ -186,7 +189,8 @@
[
('weekly_submitted','=',True),
('project_id.active','=',True),
('pm_approval_required','=',True)
('pm_approval_required','=',True),
('can_current_user_approve_line','=',True)
]
</field>
<field name="context">
@ -213,7 +217,12 @@
name="Weekly Timesheet Approval"
parent="hr_timesheet.timesheet_menu_root"
action="action_weekly_timesheet_approval"
groups="weekly_timesheets.group_weekly_timesheet_manager"
sequence="200"/>
<record id="menu_project_timesheet_approval" model="ir.ui.menu">
<field name="name">Project Timesheet Approval</field>
<field name="active" eval="False"/>
</record>
</odoo>
</odoo>

View File

@ -17,25 +17,28 @@
string="Reject"
type="object"
class="btn-primary"
invisible="state != 'submitted'"/>
invisible="state != 'submitted' or not can_current_user_approve_weekly"/>
<button name="action_approve"
string="Approve"
type="object"
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"
string="Reset to Draft"
type="object"
class="btn-primary"
invisible="state == 'draft'"/>
invisible="state != 'rejected'"/>
<button name="action_refresh_timesheets"
string="Refresh"
type="object"
class="btn-secondary"
invisible="state in ('submitted','approved')"/>
invisible="context.get('weekly_timesheet_approval') or not can_current_user_refresh_weekly"/>
<field name="state"
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>
@ -43,7 +46,9 @@
<group>
<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"
readonly="state in ('submitted','approved')"/>
@ -77,17 +82,21 @@
<field name="name"/>
<field name="project_id"/>
<field name="task_id"/>
<field name="task_stage_id" string="Stage"/>
<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"
string="Approve"
type="object"
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"
string="Reject"
type="object"
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>
</field>
@ -109,18 +118,36 @@
<field name="employee_id"/>
<field name="year_id"/>
<field name="week_line_id"/>
<field name="total_hours"/>
<field name="state"/>
</list>
</field>
</record>
<record id="action_weekly_period_timesheets" model="ir.actions.act_window">
<field name="res_model">weekly.periodtimesheets</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"
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>
<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>