project modifications

This commit is contained in:
karuna 2026-05-19 12:08:41 +05:30
parent f6bfd46f2c
commit c2e33753bb
17 changed files with 380 additions and 345 deletions

View File

@ -103,7 +103,7 @@ class ImBus(models.Model):
"""Low-level method to send ``notification_type`` and ``message`` to ``target``. """Low-level method to send ``notification_type`` and ``message`` to ``target``.
Using ``_bus_send()`` from ``bus.listener.mixin`` is recommended for simplicity and Using ``_bus_send()`` from ``bus.listener.mixin`` is recommended for simplicity and
security. security.
When using ``_sendone`` directly, ``target`` (if str) should not be guessable by an When using ``_sendone`` directly, ``target`` (if str) should not be guessable by an
attacker. attacker.

View File

@ -6,7 +6,7 @@
'version': '1.0', 'version': '1.0',
'category': 'Accounting/Localizations/Point of Sale', 'category': 'Accounting/Localizations/Point of Sale',
'description': """ 'description': """
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C). This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability,security, storage and archiving of data related to sales to private individuals (B2C).
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Install it if you use the Point of Sale app to sell to individuals. Install it if you use the Point of Sale app to sell to individuals.

View File

@ -41,7 +41,7 @@ class Survey(http.Controller):
def _check_validity(self, survey_token, answer_token, ensure_token=True, check_partner=True): def _check_validity(self, survey_token, answer_token, ensure_token=True, check_partner=True):
""" Check survey is open and can be taken. This does not checks for """ Check survey is open and can be taken. This does not checks for
security rules, only functional / business rules. It returns a string key security rules, only functional / business rules. It returns a string key
allowing further manipulation of validity issues allowing further manipulation of validity issues
* survey_wrong: survey does not exist; * survey_wrong: survey does not exist;

View File

@ -24,7 +24,7 @@
}, },
'installable': True, 'installable': True,
'data': [ 'data': [
# security.xml first, data.xml need the group to exist (checking it) #security.xml first, data.xml need the group to exist (checking it)
'security/website_security.xml', 'security/website_security.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/image_library.xml', 'data/image_library.xml',

View File

@ -1,62 +1,62 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<!--security.xml --> <!--security.xml -->
<odoo> <odoo>
<!--security.xml --> <!--security.xml -->
<odoo> <odoo>
<record id="category_employee_appraisal" model="ir.module.category"> <record id="category_employee_appraisal" model="ir.module.category">
<field name="name">Appraisal</field> <field name="name">Appraisal</field>
<field name="sequence">50</field> <field name="sequence">50</field>
</record> </record>
<!-- Define the user groups --> <!-- Define the user groups -->
<record id="group_appraisal_officer" model="res.groups"> <record id="group_appraisal_officer" model="res.groups">
<field name="name">Appraisal Officer</field> <field name="name">Appraisal Officer</field>
<field name="category_id" ref="category_employee_appraisal"/> <field name="category_id" ref="category_employee_appraisal"/>
</record> </record>
<record id="group_appraisal_manager" model="res.groups"> <record id="group_appraisal_manager" model="res.groups">
<field name="name">Appraisal HR Manager</field> <field name="name">Appraisal HR Manager</field>
<field name="category_id" ref="category_employee_appraisal"/> <field name="category_id" ref="category_employee_appraisal"/>
<field name="implied_ids" eval="[(4, ref('group_appraisal_officer'))]"/> <!-- Inherit Appraisal User permissions --> <field name="implied_ids" eval="[(4, ref('group_appraisal_officer'))]"/> <!-- Inherit Appraisal User permissions -->
</record> </record>
<record id="group_appraisal_administrator" model="res.groups"> <record id="group_appraisal_administrator" model="res.groups">
<field name="name">Appraisal Administrator</field> <field name="name">Appraisal Administrator</field>
<field name="category_id" ref="category_employee_appraisal"/> <field name="category_id" ref="category_employee_appraisal"/>
<field name="implied_ids" eval="[(4, ref('group_appraisal_officer'))]"/> <!-- Inherit Appraisal User permissions --> <field name="implied_ids" eval="[(4, ref('group_appraisal_officer'))]"/> <!-- Inherit Appraisal User permissions -->
</record> </record>
<record id="employee_appraisal_base_user_rule" model="ir.rule"> <record id="employee_appraisal_base_user_rule" model="ir.rule">
<field name="name">User can only see his/her own appraisals</field> <field name="name">User can only see his/her own appraisals</field>
<field name="model_id" ref="model_employee_appraisal"/> <field name="model_id" ref="model_employee_appraisal"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/> <field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="domain_force">[('user_id','=',user.id),('state','!=','draft')]</field> <field name="domain_force">[('user_id','=',user.id),('state','!=','draft')]</field>
</record> </record>
<record id="employee_appraisal_officer_rule" model="ir.rule"> <record id="employee_appraisal_officer_rule" model="ir.rule">
<field name="name">User can only see the records of people under him/her</field> <field name="name">User can only see the records of people under him/her</field>
<field name="model_id" ref="model_employee_appraisal"/> <field name="model_id" ref="model_employee_appraisal"/>
<field name="groups" eval="[(4, ref('group_appraisal_officer'))]"/> <field name="groups" eval="[(4, ref('group_appraisal_officer'))]"/>
<field name="domain_force">[('reviewers_name.user_id','=',user.id),('state','!=','draft')]</field> <field name="domain_force">[('reviewers_name.user_id','=',user.id),('state','!=','draft')]</field>
</record> </record>
<record id="employee_appraisal_manager_rule" model="ir.rule"> <record id="employee_appraisal_manager_rule" model="ir.rule">
<field name="name">User can only see the all the appraisal records where he/she is set as HR</field> <field name="name">User can only see the all the appraisal records where he/she is set as HR</field>
<field name="model_id" ref="model_employee_appraisal"/> <field name="model_id" ref="model_employee_appraisal"/>
<field name="groups" eval="[(4, ref('group_appraisal_manager'))]"/> <field name="groups" eval="[(4, ref('group_appraisal_manager'))]"/>
<field name="domain_force">[('appraisal_hr_id','=',user.id)]</field> <field name="domain_force">[('appraisal_hr_id','=',user.id)]</field>
</record> </record>
<record id="employee_appraisal_md_rule" model="ir.rule"> <record id="employee_appraisal_md_rule" model="ir.rule">
<field name="name">User can only see the all the appraisal records where he/she is set as MD</field> <field name="name">User can only see the all the appraisal records where he/she is set as MD</field>
<field name="model_id" ref="model_employee_appraisal"/> <field name="model_id" ref="model_employee_appraisal"/>
<field name="groups" eval="[(4, ref('group_appraisal_administrator'))]"/> <field name="groups" eval="[(4, ref('group_appraisal_administrator'))]"/>
<field name="domain_force">[('appraisal_md_id','=',user.id)]</field> <field name="domain_force">[('appraisal_md_id','=',user.id)]</field>
</record> </record>
</odoo> </odoo>
</odoo> </odoo>

View File

@ -1,15 +1,15 @@
{ {
"name": "Custom Module Switcher", "name": "Custom Module Switcher",
"version": "1.0", "version": "1.0",
"depends": ["web","menu_control_center"], "depends": ["web","menu_control_center"],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
], ],
"assets": { "assets": {
"web.assets_backend": [ "web.assets_backend": [
"module_selector_sidebar/static/src/js/module_switcher.js", "module_selector_sidebar/static/src/js/module_switcher.js",
"module_selector_sidebar/static/src/xml/module_switcher.xml", "module_selector_sidebar/static/src/xml/module_switcher.xml",
], ],
}, },
"installable": True, "installable": True,
} }

View File

@ -1,4 +1,4 @@
from . import project_kudo_extend from . import project_kudo_extend
from . import task_assignee_domain from . import task_assignee_domain
from . import project_task from . import project_task
from . import account_analytic_line from . import account_analytic_line

View File

@ -49,9 +49,9 @@ Key Features:
'view/maintenance_support.xml', 'view/maintenance_support.xml',
'view/project_closer.xml', 'view/project_closer.xml',
'view/project_actual_costings.xml', 'view/project_actual_costings.xml',
'view/project_task.xml',
'view/project.xml', 'view/project.xml',
'view/project_portfolio.xml', 'view/project_portfolio.xml',
'view/project_task.xml',
'view/timesheets.xml', 'view/timesheets.xml',
'view/pro_task_gantt.xml', 'view/pro_task_gantt.xml',
'view/user_availability.xml', 'view/user_availability.xml',
@ -61,9 +61,10 @@ Key Features:
'view/stage_approval_wizard.xml', 'view/stage_approval_wizard.xml',
], ],
'assets': { 'assets': {
'web.assets_backend':{ 'web.assets_backend': [
'project_task_timesheet_extended/static/src/css/delopyment.css' 'project_task_timesheet_extended/static/src/css/delopyment.css',
} 'project_task_timesheet_extended/static/src/js/involved_assignee_avatar_user_field.js',
]
}, },
'installable': True, 'installable': True,
'application': False, 'application': False,

View File

@ -263,4 +263,5 @@
</record> </record>
</data> </data>
<function model="project.task" name="_sync_all_involved_assignees_from_timelines"/>
</odoo> </odoo>

View File

@ -120,14 +120,18 @@ def post_init_hook(env):
'|', '|',
'&', '&',
('task_id.is_generic', '=', False), ('task_id.is_generic', '=', False),
('user_id', 'in', 'task_id.user_ids'), '|',
('user_id', 'in', 'task_id.user_ids'),
('user_id', 'in', 'task_id.involved_user_ids'),
'&', '&',
('task_id.is_generic', '=', True), ('task_id.is_generic', '=', True),
('user_id.partner_id', 'in', 'project_id.message_partner_ids'), ('user_id.partner_id', 'in', 'project_id.message_partner_ids'),
'&', '&', '&', '&',
('project_id.privacy_visibility', '!=', 'followers'), ('project_id.privacy_visibility', '!=', 'followers'),
('task_id.is_generic', '=', False), ('task_id.is_generic', '=', False),
('user_id', 'in', 'task_id.user_ids') '|',
('user_id', 'in', 'task_id.user_ids'),
('user_id', 'in', 'task_id.involved_user_ids')
] ]
""" """
}) })
@ -162,12 +166,12 @@ def post_init_hook(env):
project_tasks[task.project_id.id] = [] project_tasks[task.project_id.id] = []
project_tasks[task.project_id.id].append(task) project_tasks[task.project_id.id].append(task)
# Assign sequence numbers to tasks # Assign sequence numbers to tasks
for project_id, task_list in project_tasks.items(): for project_id, task_list in project_tasks.items():
project = env['project.project'].browse(project_id) project = env['project.project'].browse(project_id)
if project.task_sequence_id: if project.task_sequence_id:
for task in task_list: for task in task_list:
task.sequence_name = project.task_sequence_id.next_by_id() task.sequence_name = project.task_sequence_id.next_by_id()
# Normalize task stages so each project owns its workflow configuration. # Normalize task stages so each project owns its workflow configuration.
env['project.project'].search([])._ensure_project_owned_task_stages() env['project.project'].search([])._ensure_project_owned_task_stages()

View File

@ -333,8 +333,6 @@ class ProjectProject(models.Model):
users_list = list() users_list = list()
if project.assign_approval_flow: if project.assign_approval_flow:
users_list.extend(project.project_stages.involved_users.ids) users_list.extend(project.project_stages.involved_users.ids)
else:
users_list.extend(project.showable_stage_ids.user_ids.ids)
if project.project_sponsor: if project.project_sponsor:
users_list.append(project.project_sponsor.id) users_list.append(project.project_sponsor.id)

View File

@ -167,12 +167,12 @@ class projectTask(models.Model):
task.assignee_domain_ids = all_internal_users task.assignee_domain_ids = all_internal_users
continue continue
# # GENERIC all internal # # GENERIC: all internal
# if getattr(task, 'is_generic', False): # if getattr(task, 'is_generic', False):
# task.assignee_domain_ids = all_internal_users # task.assignee_domain_ids = all_internal_users
# continue # continue
# PRIVATE invited users only # PRIVATE: invited users only
if task.project_id.privacy_visibility == 'followers': if task.project_id.privacy_visibility == 'followers':
task.assignee_domain_ids = ( task.assignee_domain_ids = (
task.project_id.message_partner_ids task.project_id.message_partner_ids
@ -196,18 +196,18 @@ class projectTask(models.Model):
for task in self: for task in self:
employees = Employee.browse() employees = Employee.browse()
# 1GENERIC TASK # GENERIC TASK
if task.is_generic and task.project_id: if task.is_generic and task.project_id:
project = task.project_id project = task.project_id
# 🔐 Private → followers only # Private: followers only
if project.privacy_visibility == 'followers': if project.privacy_visibility == 'followers':
users = ( users = (
project.message_partner_ids project.message_partner_ids
.mapped('user_ids') .mapped('user_ids')
.filtered(lambda u: u and not u.share) .filtered(lambda u: u and not u.share)
) )
# 🌍 Internal / Public → all internal users # Internal / Public: all internal users
else: else:
users = self.env['res.users'].search([ users = self.env['res.users'].search([
('share', '=', False), ('share', '=', False),
@ -216,7 +216,7 @@ class projectTask(models.Model):
employees = users.mapped('employee_id').filtered(lambda e: e) employees = users.mapped('employee_id').filtered(lambda e: e)
# 2⃣ NORMAL TASK → task assignees only # NORMAL TASK: assignees and involved collaborators
else: else:
employees = ( employees = (
task.user_ids task.user_ids
@ -393,7 +393,7 @@ class projectTask(models.Model):
if start_dt.tzinfo is None: if start_dt.tzinfo is None:
start_dt = pytz.UTC.localize(start_dt) start_dt = pytz.UTC.localize(start_dt)
# Convert UTC calendar timezone # Convert UTC to calendar timezone
start_dt_tz = start_dt.astimezone(tz) start_dt_tz = start_dt.astimezone(tz)
# Call plan_hours # Call plan_hours
@ -459,8 +459,8 @@ class projectTask(models.Model):
self.env.user.name, self.env.user.name,
task.suggested_deadline.strftime('%Y-%m-%d %H:%M') if task.suggested_deadline else _('Not available') task.suggested_deadline.strftime('%Y-%m-%d %H:%M') if task.suggested_deadline else _('Not available')
)) ))
@api.depends("project_id", "stage_id") @api.depends("project_id")
def _compute_has_supervisor_access(self): def _compute_has_supervisor_access(self):
administrative_users = self.env['project.role'].search([ administrative_users = self.env['project.role'].search([
('role_level', '=', 'administrative') ('role_level', '=', 'administrative')
@ -478,26 +478,13 @@ class projectTask(models.Model):
stages = project.type_ids.sorted("sequence") stages = project.type_ids.sorted("sequence")
if not stages: if first_stage:
continue create_access_users = first_stage.team_id.team_lead + first_stage.involved_user_ids + administrative_users.user_ids
else:
create_access_users = administrative_users.user_ids
first_stage = stages[0]
create_access_users = ( if current_user.has_group("project.group_project_manager") or current_user == task.project_id.user_id or current_user == task.project_id.project_lead or (current_user.id in list(set(create_access_users.ids)) and task.stage_id.id == first_stage.id):
first_stage.team_id.team_lead
+ first_stage.involved_user_ids
+ administrative_users
)
if (
current_user.has_group("project.group_project_manager")
or current_user == project.user_id
or current_user == project.project_lead
or (
current_user in create_access_users
and task.stage_id == first_stage
)
):
task.has_supervisor_access = True task.has_supervisor_access = True
@api.depends('assignees_timelines.estimated_time', 'show_approval_flow') @api.depends('assignees_timelines.estimated_time', 'show_approval_flow')
@ -637,11 +624,11 @@ class projectTask(models.Model):
task.show_approval_button = True task.show_approval_button = True
task.show_refuse_button = True # both approve & refuse in review state task.show_refuse_button = True # both approve & refuse in review state
# b) No assigned user directly approvable # b) No assigned user: directly approvable
elif not assigned_to and (responsible_lead == user or project_manager == user): elif not assigned_to and (responsible_lead == user or project_manager == user):
task.show_approval_button = True task.show_approval_button = True
# c) Assigned_to == responsible_lead no submission needed, direct approve # c) Assigned_to == responsible_lead: no submission needed, direct approve
elif ( elif (
assigned_to assigned_to
and assigned_to == responsible_lead and assigned_to == responsible_lead
@ -797,7 +784,7 @@ class projectTask(models.Model):
task.stage_id = n_stage task.stage_id = n_stage
task.approval_status = "approved" task.approval_status = "approved"
activity_log = "%s: approved by %s and moved to %s" % ( activity_log = "%s: approved by %s and moved to %s" % (
current_stage.name, current_stage.name,
self.env.user.employee_id.name, self.env.user.employee_id.name,
n_stage.name) n_stage.name)
@ -838,9 +825,9 @@ class projectTask(models.Model):
) )
else: else:
task.approval_status = "approved" task.approval_status = "approved"
notes = "%s: Task approved and completed by %s" % (task.sequence_name, self.env.user.employee_id.name) notes = "%s: Task approved and completed by %s" % (task.sequence_name, self.env.user.employee_id.name)
activity_log = "%s: approved by %s" % ( activity_log = "%s: approved by %s" % (
current_stage.name, current_stage.name,
self.env.user.employee_id.name) self.env.user.employee_id.name)
@ -876,9 +863,9 @@ class projectTask(models.Model):
# Optional: find previous stage if you want to send back # Optional: find previous stage if you want to send back
stage = task.assignees_timelines.filtered(lambda s: s.stage_id == task.stage_id) stage = task.assignees_timelines.filtered(lambda s: s.stage_id == task.stage_id)
notes = "%s: %s rejected by %s" % (task.sequence_name, current_stage.name, self.env.user.employee_id.name) notes = "%s: %s rejected by %s" % (task.sequence_name, current_stage.name, self.env.user.employee_id.name)
activity_log = "%s: rejected by %s: %s" % ( activity_log = "%s: rejected by %s: %s" % (
current_stage.name, current_stage.name,
self.env.user.employee_id.name, self.env.user.employee_id.name,
reason) reason)
@ -1017,19 +1004,27 @@ class projectTask(models.Model):
"You are not allowed to change the stage of this task because stage editing is restricted." "You are not allowed to change the stage of this task because stage editing is restricted."
)) ))
return super(projectTask, self).write(vals) result = super(projectTask, self).write(vals)
if any(field in vals for field in ['allocation_start_date', 'allocation_end_date']):
self._sync_allocated_hours_from_allocation_dates()
if any(field in vals for field in ['user_ids', 'is_generic']):
self._sync_involved_assignees_from_timelines()
return result
def button_update_assignees(self): def _sync_involved_assignees_from_timelines(self):
for task in self: for task in self:
if task.assignees_timelines: if task.is_generic:
users_list = list( continue
set(task.assignees_timelines.responsible_lead.ids + task.assignees_timelines.assigned_to.ids + task.assignees_timelines.team_id.team_lead.ids))
task.user_ids = [(6, 0, users_list)]
# Post to project channel about assignee update timeline_user_ids = set(
channel_message = _("Assignees updated for task %s") % (task.sequence_name or task.name) task.assignees_timelines.responsible_lead.ids
task._post_to_project_channel(channel_message) + task.assignees_timelines.assigned_to.ids
+ task.assignees_timelines.team_id.team_lead.ids
)
existing_user_ids = set(task.involved_user_ids.ids)
involved_users = list((existing_user_ids | timeline_user_ids) - set(task.user_ids.ids))
task.involved_user_ids = [(6, 0, involved_users)]
def _fetch_planning_overlap(self, additional_domain=None): def _fetch_planning_overlap(self, additional_domain=None):
use_timeline_logic = any( use_timeline_logic = any(
@ -1419,7 +1414,7 @@ class projectTask(models.Model):
FROM project_task T1 FROM project_task T1
INNER JOIN project_task T2 ON T1.id <> T2.id INNER JOIN project_task T2 ON T1.id <> T2.id
INNER JOIN project_task_user_rel U1 ON T1.id = U1.task_id INNER JOIN project_task_user_rel U1 ON T1.id = U1.task_id
INNER JOIN project_task_user_rel U2 INNER JOIN project_task_user_rel U2
ON T2.id = U2.task_id ON T2.id = U2.task_id
AND U1.user_id = U2.user_id AND U1.user_id = U2.user_id
WHERE WHERE

View File

@ -1,161 +1,161 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<record id="group_project_supervisor" model="res.groups"> <record id="group_project_supervisor" model="res.groups">
<field name="name">Manager</field> <field name="name">Manager</field>
<field name="category_id" ref="base.module_category_services_project"/> <field name="category_id" ref="base.module_category_services_project"/>
<field name="implied_ids" eval="[(4, ref('project.group_project_user'))]"/> <field name="implied_ids" eval="[(4, ref('project.group_project_user'))]"/>
</record> </record>
<record id="group_project_lead" model="res.groups"> <record id="group_project_lead" model="res.groups">
<field name="name">Project Lead</field> <field name="name">Project Lead</field>
<field name="category_id" ref="base.module_category_services_project"/> <field name="category_id" ref="base.module_category_services_project"/>
</record> </record>
<record id="project.group_project_manager" model="res.groups"> <record id="project.group_project_manager" model="res.groups">
<field name="implied_ids" eval="[(4, ref('project.group_project_user')),(4, ref('group_project_supervisor')), (4, ref('mail.group_mail_canned_response_admin'))]"/> <field name="implied_ids" eval="[(4, ref('project.group_project_user')),(4, ref('group_project_supervisor')), (4, ref('mail.group_mail_canned_response_admin'))]"/>
</record> </record>
<data> <data>
<record id="portfolio_rule_company_projects" model="ir.rule"> <record id="portfolio_rule_company_projects" model="ir.rule">
<field name="name">company: Own Company</field> <field name="name">company: Own Company</field>
<field name="model_id" ref="model_project_portfolio"/> <field name="model_id" ref="model_project_portfolio"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field> <field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record> </record>
<record id="project_rule_manager_own_projects" model="ir.rule"> <record id="project_rule_manager_own_projects" model="ir.rule">
<field name="name">Manager: Own Projects</field> <field name="name">Manager: Own Projects</field>
<field name="model_id" ref="project.model_project_project"/> <field name="model_id" ref="project.model_project_project"/>
<field name="groups" eval="[(4, ref('project_task_timesheet_extended.group_project_supervisor'))]"/> <field name="groups" eval="[(4, ref('project_task_timesheet_extended.group_project_supervisor'))]"/>
<field name="domain_force">[('user_id', '=', user.id)]</field> <field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="perm_read" eval="1"/> <field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/> <field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/> <field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/> <field name="perm_unlink" eval="0"/>
</record> </record>
<record model="ir.rule" id="project_supervisor_all_project_tasks_rule"> <record model="ir.rule" id="project_supervisor_all_project_tasks_rule">
<field name="name">Project/Task: project supervisor: see all tasks linked to his assigned project or its own tasks</field> <field name="name">Project/Task: project supervisor: see all tasks linked to his assigned project or its own tasks</field>
<field name="model_id" ref="project.model_project_task"/> <field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[ <field name="domain_force">[
('project_id.user_id','=',user.id), ('project_id.user_id','=',user.id),
'|', ('project_id', '!=', False), '|', ('project_id', '!=', False),
('user_ids', 'in', user.id), ('user_ids', 'in', user.id),
]</field> ]</field>
<field name="groups" eval="[(4,ref('project_task_timesheet_extended.group_project_supervisor'))]"/> <field name="groups" eval="[(4,ref('project_task_timesheet_extended.group_project_supervisor'))]"/>
</record> </record>
<record model="ir.rule" id="project_users_project_tasks_rule"> <record model="ir.rule" id="project_users_project_tasks_rule">
<field name="name">Project/Task: project users: don't see non generic tasks</field> <field name="name">Project/Task: project users: don't see non generic tasks</field>
<field name="model_id" ref="project.model_project_task"/> <field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[ <field name="domain_force">[
'&amp;', '&amp;', '&amp;', '&amp;',
('project_id', '!=', False), ('project_id', '!=', False),
('is_generic', '=', False), ('is_generic', '=', False),
('user_ids', 'not in', user.id), ('user_ids', 'not in', user.id),
] ]
</field> </field>
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/> <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
<field name="perm_read" eval="0"/> <field name="perm_read" eval="0"/>
</record> </record>
<record model="ir.rule" id="project_users_project_lead_rule"> <record model="ir.rule" id="project_users_project_lead_rule">
<field name="name">Project/Task: project lead: see all tasks</field> <field name="name">Project/Task: project lead: see all tasks</field>
<field name="model_id" ref="project.model_project_task"/> <field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[ <field name="domain_force">[
'&amp;', '&amp;', '&amp;', '&amp;', '&amp;', '&amp;',
('project_id', '!=', False), ('project_id', '!=', False),
('project_id.project_lead', '=', user.id), ('project_id.project_lead', '=', user.id),
'|', ('is_generic', '=', True), ('is_generic', '=', False), '|', ('is_generic', '=', True), ('is_generic', '=', False),
'|', ('user_ids', 'in', user.id), ('user_ids', 'not in', user.id) '|', ('user_ids', 'in', user.id), ('user_ids', 'not in', user.id)
] ]
</field> </field>
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/> <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
<field name="perm_read" eval="1"/> <field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/> <field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/> <field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/> <field name="perm_unlink" eval="0"/>
</record> </record>
<record model="ir.rule" id="user_task_availability_project_lead_rule"> <record model="ir.rule" id="user_task_availability_project_lead_rule">
<field name="name">Task Availability: project lead: see all user tasks</field> <field name="name">Task Availability: project lead: see all user tasks</field>
<field name="model_id" ref="model_user_task_availability"/> <field name="model_id" ref="model_user_task_availability"/>
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/> <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
<field name="domain_force">[ <field name="domain_force">[
'|', '|', '|', '|',
('project_id.project_lead', '=', user.id), ('project_id.project_lead', '=', user.id),
('user_id', '=', user.id), ('user_id', '=', user.id),
('project_id.user_id', '=', user.id), ('project_id.user_id', '=', user.id),
] ]
</field> </field>
</record> </record>
<!-- <record model="ir.rule" id="timesheet_users_normal_timesheets">--> <!-- <record model="ir.rule" id="timesheet_users_normal_timesheets">-->
<!-- <field name="name">timesheet: users: see own tasks</field>--> <!-- <field name="name">timesheet: users: see own tasks</field>-->
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>--> <!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
<!-- <field name="domain_force">[--> <!-- <field name="domain_force">[-->
<!-- '&amp;','&amp;', '&amp;', '&amp;', '&amp;','&amp;',--> <!-- '&amp;','&amp;', '&amp;', '&amp;', '&amp;','&amp;',-->
<!-- ('project_id.privacy_visibility','=','followers'),--> <!-- ('project_id.privacy_visibility','=','followers'),-->
<!-- ('task_id', '!=', False),--> <!-- ('task_id', '!=', False),-->
<!-- ('project_id', '!=', False),--> <!-- ('project_id', '!=', False),-->
<!-- ('project_id.project_lead', '!=', user.id),--> <!-- ('project_id.project_lead', '!=', user.id),-->
<!-- ('project_id.user_id', '!=', user.id),--> <!-- ('project_id.user_id', '!=', user.id),-->
<!-- ('user_id','not in',[user.id]),--> <!-- ('user_id','not in',[user.id]),-->
<!-- '|',--> <!-- '|',-->
<!-- '&amp;',--> <!-- '&amp;',-->
<!-- ('task_id.is_generic', '=', False),--> <!-- ('task_id.is_generic', '=', False),-->
<!-- ('task_id.user_ids', 'not in', [user.id]),--> <!-- ('task_id.user_ids', 'not in', [user.id]),-->
<!-- '&amp;',--> <!-- '&amp;',-->
<!-- ('task_id.is_generic', '=', True),--> <!-- ('task_id.is_generic', '=', True),-->
<!-- ('project_id.message_partner_ids', 'not in', [user.partner_id.id]),--> <!-- ('project_id.message_partner_ids', 'not in', [user.partner_id.id]),-->
<!-- ]</field>--> <!-- ]</field>-->
<!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>--> <!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>-->
<!-- <field name="perm_read" eval="0"/>--> <!-- <field name="perm_read" eval="0"/>-->
<!-- <field name="perm_unlink" eval="1"/>--> <!-- <field name="perm_unlink" eval="1"/>-->
<!-- <field name="perm_write" eval="1"/>--> <!-- <field name="perm_write" eval="1"/>-->
<!-- <field name="perm_create" eval="1"/>--> <!-- <field name="perm_create" eval="1"/>-->
<!-- </record>--> <!-- </record>-->
<!-- <record model="ir.rule" id="timesheet_users_non_generic_timesheets">--> <!-- <record model="ir.rule" id="timesheet_users_non_generic_timesheets">-->
<!-- <field name="name">timesheet: users: see own tasks</field>--> <!-- <field name="name">timesheet: users: see own tasks</field>-->
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>--> <!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
<!-- <field name="domain_force">[--> <!-- <field name="domain_force">[-->
<!-- ('project_id.privacy_visibility','!=','followers'),--> <!-- ('project_id.privacy_visibility','!=','followers'),-->
<!-- ('task_id', '!=', False),--> <!-- ('task_id', '!=', False),-->
<!-- ('project_id', '!=', False),--> <!-- ('project_id', '!=', False),-->
<!-- ('project_id.project_lead', '!=', user.id),--> <!-- ('project_id.project_lead', '!=', user.id),-->
<!-- ('project_id.user_id', '!=', user.id),--> <!-- ('project_id.user_id', '!=', user.id),-->
<!-- ('task_id.is_generic', '=', False),--> <!-- ('task_id.is_generic', '=', False),-->
<!-- ('task_id.user_ids', 'not in', [user.id]),--> <!-- ('task_id.user_ids', 'not in', [user.id]),-->
<!-- ('user_id','not in',[user.id]),--> <!-- ('user_id','not in',[user.id]),-->
<!-- ]</field>--> <!-- ]</field>-->
<!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>--> <!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>-->
<!-- <field name="perm_read" eval="0"/>--> <!-- <field name="perm_read" eval="0"/>-->
<!-- <field name="perm_unlink" eval="1"/>--> <!-- <field name="perm_unlink" eval="1"/>-->
<!-- <field name="perm_write" eval="1"/>--> <!-- <field name="perm_write" eval="1"/>-->
<!-- <field name="perm_create" eval="1"/>--> <!-- <field name="perm_create" eval="1"/>-->
<!-- </record>--> <!-- </record>-->
<!-- <record model="ir.rule" id="timesheet_team_lead_normal_timesheets">--> <!-- <record model="ir.rule" id="timesheet_team_lead_normal_timesheets">-->
<!-- <field name="name">timesheet: Lead: see related tasks</field>--> <!-- <field name="name">timesheet: Lead: see related tasks</field>-->
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>--> <!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
<!-- <field name="domain_force">[--> <!-- <field name="domain_force">[-->
<!-- '&amp;', '&amp;', '&amp;',--> <!-- '&amp;', '&amp;', '&amp;',-->
<!-- ('project_id', '!=', False),--> <!-- ('project_id', '!=', False),-->
<!-- ('project_id.project_lead', '=', user.id),--> <!-- ('project_id.project_lead', '=', user.id),-->
<!-- '|', ('task_id.is_generic', '=', True), ('task_id.is_generic', '=', False),--> <!-- '|', ('task_id.is_generic', '=', True), ('task_id.is_generic', '=', False),-->
<!-- '|', ('task_id.user_ids', 'in', user.id), ('task_id.user_ids', 'not in', user.id)--> <!-- '|', ('task_id.user_ids', 'in', user.id), ('task_id.user_ids', 'not in', user.id)-->
<!-- ]--> <!-- ]-->
<!-- </field>--> <!-- </field>-->
<!-- <field name="perm_read" eval="1"/>--> <!-- <field name="perm_read" eval="1"/>-->
<!-- <field name="perm_write" eval="1"/>--> <!-- <field name="perm_write" eval="1"/>-->
<!-- <field name="perm_create" eval="1"/>--> <!-- <field name="perm_create" eval="1"/>-->
<!-- <field name="perm_unlink" eval="0"/>--> <!-- <field name="perm_unlink" eval="0"/>-->
<!-- </record>--> <!-- </record>-->
</data> </data>
<data> <data>
</data> </data>
</odoo> </odoo>

View File

@ -0,0 +1,34 @@
/** @odoo-module **/
import {
Many2ManyTagsAvatarUserField,
many2ManyTagsAvatarUserField,
} from "@mail/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field";
import { registry } from "@web/core/registry";
export class InvolvedAssigneeAvatarUserField extends Many2ManyTagsAvatarUserField {
getDomain() {
const involved = this.props.record.data.involved_user_ids;
const involvedIds = involved?.records?.map((record) => record.resId).filter(Boolean) || [];
if (involvedIds.length) {
return [["id", "in", involvedIds]];
}
return [["id", "=", false]];
}
}
export const involvedAssigneeAvatarUserField = {
...many2ManyTagsAvatarUserField,
component: InvolvedAssigneeAvatarUserField,
extractProps(fieldInfo, dynamicInfo) {
const props = many2ManyTagsAvatarUserField.extractProps(fieldInfo, dynamicInfo);
return {
...props,
canCreate: false,
canQuickCreate: false,
canCreateEdit: false,
};
},
};
registry.category("fields").add("involved_assignee_avatar_user", involvedAssigneeAvatarUserField);

View File

@ -41,29 +41,31 @@
[('id', 'in', parent.allowed_employee_ids)] [('id', 'in', parent.allowed_employee_ids)]
</attribute> </attribute>
</xpath> </xpath>
<!-- <xpath expr="//field[@name='timesheet_ids']//field[@name='stage_id']"--> <!-- <xpath expr="//field[@name='timesheet_ids']//field[@name='stage_id']"-->
<!-- position="attributes">--> <!-- position="attributes">-->
<!-- <attribute name="domain">--> <!-- <attribute name="domain">-->
<!-- [('assigned_user_ids.employee_id', '=', employee_id)]--> <!-- [('assigned_user_ids.employee_id', '=', employee_id)]-->
<!-- </attribute>--> <!-- </attribute>-->
<!-- </xpath>--> <!-- </xpath>-->
<xpath expr="//div[hasclass('oe_title','pe-0')]" position="after"> <xpath expr="//div[hasclass('oe_title','pe-0')]" position="after">
<group> <group>
<h1><field name="sequence_name" readonly="1"/></h1> <h1><field name="sequence_name" readonly="1"/></h1>
</group> </group>
</xpath> </xpath>
<!-- <xpath expr="//field[@name='user_ids']" position="before">--> <!-- <xpath expr="//field[@name='user_ids']" position="before">-->
<!-- <field name="assigned_team"/>--> <!-- <field name="assigned_team"/>-->
<!-- </xpath>--> <!-- </xpath>-->
<xpath expr="//field[@name='user_ids']" position="after"> <xpath expr="//field[@name='user_ids']" position="after">
<field name="is_generic" readonly="not has_supervisor_access"/> <field name="is_generic" readonly="not has_supervisor_access"/>
<field name="record_paused" invisible="1"/> <field name="record_paused" invisible="1"/>
<field name="model_id" readonly="not has_supervisor_access" options="{'no_open': True}"/> <field name="model_id" readonly="not has_supervisor_access" options="{'no_open': True}"/>
</xpath> </xpath>
<!-- <xpath expr="//field[@name='allocated_hours']" position="after">--> <!-- <xpath expr="//field[@name='allocated_hours']" position="after">-->
<!-- <field name="estimated_hours"/>--> <!-- <field name="estimated_hours"/>-->
<!-- <field name="actual_hours"/>--> <!-- <field name="actual_hours"/>-->
<!-- </xpath>--> <!-- </xpath>-->
<xpath expr="//sheet/notebook" position="inside"> <xpath expr="//sheet/notebook" position="inside">
<page string="Assignees Timelines" invisible="not show_approval_flow"> <page string="Assignees Timelines" invisible="not show_approval_flow">
@ -132,7 +134,7 @@
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<i class="fa fa-exclamation-triangle text-danger me-2" role="img" title="Deadline Issue"/> <i class="fa fa-exclamation-triangle text-danger me-2" role="img" title="Deadline Issue"/>
Based on the timelines, the deadline can't be met Based on the timelines, the deadline can't be met
</div> </div>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@ -143,16 +145,17 @@
</div> </div>
<!-- <div>--> <!-- <div>-->
<!-- <field name="suggested_deadline" invisible="not suggested_deadline or not show_approval_flow or not timelines_requested"/>--> <!-- <field name="suggested_deadline" invisible="not suggested_deadline or not show_approval_flow or not timelines_requested"/>-->
<!-- <field name="is_suggested_deadline_warning" invisible="1"/>--> <!-- <field name="is_suggested_deadline_warning" invisible="1"/>-->
<!-- <label for="suggested_deadline"--> <!-- <label for="suggested_deadline"-->
<!-- class="text-warning"--> <!-- class="text-warning"-->
<!-- invisible="not is_suggested_deadline_warning">--> <!-- invisible="not is_suggested_deadline_warning">-->
<!-- ⚠ Based on the timelines, the deadline can't be met--> <!-- Based on the timelines, the deadline can't be met -->
<!-- </label>--> <!-- </label>-->
<!-- </div>--> <!-- </div>-->
<field name="estimated_hours" widget="timesheet_uom_no_toggle" readonly="show_approval_flow and timelines_requested"/> <field name="estimated_hours" widget="timesheet_uom_no_toggle"
readonly="show_approval_flow and timelines_requested"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle"/> <field name="actual_hours" widget="timesheet_uom_no_toggle"/>
<field name="is_suggested_deadline_warning" /> <field name="is_suggested_deadline_warning" />
</xpath> </xpath>

View File

@ -34,7 +34,7 @@
<!-- </div>--> <!-- </div>-->
<!-- <div name="project_id">--> <!-- <div name="project_id">-->
<!-- <strong>Project </strong>--> <!-- <strong>Project —</strong>-->
<!-- <t t-if="project_id" t-esc="project_id[1]"/>--> <!-- <t t-if="project_id" t-esc="project_id[1]"/>-->
<!-- <t t-else="">--> <!-- <t t-else="">-->
<!-- <span class="fst-italic text-muted">--> <!-- <span class="fst-italic text-muted">-->
@ -45,7 +45,7 @@
<!-- </div>--> <!-- </div>-->
<!-- <div t-if="show_approval_flow and current_stage_performance">--> <!-- <div t-if="show_approval_flow and current_stage_performance">-->
<!-- <strong>Performance </strong>--> <!-- <strong>Performance —</strong>-->
<!-- <t t-if="current_stage_performance == 'good'">--> <!-- <t t-if="current_stage_performance == 'good'">-->
<!-- <span class="text-success">--> <!-- <span class="text-success">-->
<!-- <i class="fa fa-smile-o"></i>--> <!-- <i class="fa fa-smile-o"></i>-->
@ -67,21 +67,21 @@
<!-- </div>--> <!-- </div>-->
<!-- <div t-if="allow_milestones and milestone_id" groups="project.group_project_milestone">--> <!-- <div t-if="allow_milestones and milestone_id" groups="project.group_project_milestone">-->
<!-- <strong>Milestone </strong>--> <!-- <strong>Milestone —</strong>-->
<!-- <t t-esc="milestone_id[1]"/>--> <!-- <t t-esc="milestone_id[1]"/>-->
<!-- </div>--> <!-- </div>-->
<!-- <div t-if="user_names">--> <!-- <div t-if="user_names">-->
<!-- <strong>Assignees </strong>--> <!-- <strong>Assignees —</strong>-->
<!-- <t t-esc="user_names"/>--> <!-- <t t-esc="user_names"/>-->
<!-- </div>--> <!-- </div>-->
<!-- <div t-if="partner_id">--> <!-- <div t-if="partner_id">-->
<!-- <strong>Customer </strong>--> <!-- <strong>Customer —</strong>-->
<!-- <t t-esc="partner_id[1]"/>--> <!-- <t t-esc="partner_id[1]"/>-->
<!-- </div>--> <!-- </div>-->
<!-- <div t-if="show_approval_flow">--> <!-- <div t-if="show_approval_flow">-->
<!-- <strong>Timeline </strong>--> <!-- <strong>Timeline —</strong>-->
<!-- <t t-esc="estimated_hours"/>--> <!-- <t t-esc="estimated_hours"/>-->
<!-- hours estimated /--> <!-- hours estimated /-->
<!-- <t t-esc="actual_hours"/>--> <!-- <t t-esc="actual_hours"/>-->
@ -89,7 +89,7 @@
<!-- </div>--> <!-- </div>-->
<!-- <div t-if="project_id" name="allocated_hours">--> <!-- <div t-if="project_id" name="allocated_hours">-->
<!-- <strong>Allocated Time </strong>--> <!-- <strong>Allocated Time —</strong>-->
<!-- <t t-esc="allocated_hours"/>--> <!-- <t t-esc="allocated_hours"/>-->
<!-- </div>--> <!-- </div>-->
@ -201,7 +201,7 @@
<div class="mt-2"> <div class="mt-2">
<div name="project_id"> <div name="project_id">
<strong>Project </strong> <strong>Project —</strong>
<t t-if="project_id" t-esc="project_id[1]"/> <t t-if="project_id" t-esc="project_id[1]"/>
<t t-else=""> <t t-else="">
<span class="fst-italic text-muted"> <span class="fst-italic text-muted">
@ -211,7 +211,7 @@
</div> </div>
<div t-if="show_approval_flow and current_stage_performance and not is_completed" class="mt-1"> <div t-if="show_approval_flow and current_stage_performance and not is_completed" class="mt-1">
<strong>Performance </strong> <strong>Performance —</strong>
<t t-if="current_stage_performance == 'good'"> <t t-if="current_stage_performance == 'good'">
<span class="text-success"> <span class="text-success">
<i class="fa fa-smile-o"></i> Good (Actual &lt; Estimated) <i class="fa fa-smile-o"></i> Good (Actual &lt; Estimated)
@ -230,24 +230,24 @@
</div> </div>
<div t-if="allow_milestones and milestone_id" groups="project.group_project_milestone" class="mt-1"> <div t-if="allow_milestones and milestone_id" groups="project.group_project_milestone" class="mt-1">
<strong>Milestone </strong> <t t-esc="milestone_id[1]"/> <strong>Milestone —</strong> <t t-esc="milestone_id[1]"/>
</div> </div>
<div t-if="user_names" class="mt-1"> <div t-if="user_names" class="mt-1">
<strong>Assignees </strong> <t t-esc="user_names"/> <strong>Assignees —</strong> <t t-esc="user_names"/>
</div> </div>
<div t-if="partner_id" class="mt-1"> <div t-if="partner_id" class="mt-1">
<strong>Customer </strong> <t t-esc="partner_id[1]"/> <strong>Customer —</strong> <t t-esc="partner_id[1]"/>
</div> </div>
<div t-if="show_approval_flow and not is_completed" class="mt-1"> <div t-if="show_approval_flow and not is_completed" class="mt-1">
<strong>Timeline </strong> <strong>Timeline —</strong>
<t t-esc="estimated_hours"/> hours estimated / <t t-esc="estimated_hours"/> hours estimated /
<t t-esc="actual_hours"/> hours actual <t t-esc="actual_hours"/> hours actual
</div> </div>
<div t-if="project_id" name="allocated_hours" class="mt-1"> <div t-if="project_id" name="allocated_hours" class="mt-1">
<strong>Allocated Time </strong> <t t-esc="allocated_hours"/> <strong>Allocated Time —</strong> <t t-esc="allocated_hours"/>
</div> </div>
<div class="mt-2"> <div class="mt-2">
@ -302,7 +302,6 @@
<footer class="mt-3"> <footer class="mt-3">
<button name="action_unschedule_task" type="object" string="Unschedule" class="btn btn-sm btn-secondary"/> <button name="action_unschedule_task" type="object" string="Unschedule" class="btn btn-sm btn-secondary"/>
<button name="button_update_assignees" type="object" string="Update Assignees" class="btn btn-sm btn-primary"/>
</footer> </footer>
</div> </div>

View File

@ -412,7 +412,7 @@ token.QWEB = token.NT_OFFSET - 1
token.tok_name[token.QWEB] = 'QWEB' token.tok_name[token.QWEB] = 'QWEB'
# security safe eval opcodes for generated expression validation, used in `_compile_expr` #security safe eval opcodes for generated expression validation, used in `_compile_expr`
_SAFE_QWEB_OPCODES = _EXPR_OPCODES.union(to_opcodes([ _SAFE_QWEB_OPCODES = _EXPR_OPCODES.union(to_opcodes([
'MAKE_FUNCTION', 'CALL_FUNCTION', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_EX', 'MAKE_FUNCTION', 'CALL_FUNCTION', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_EX',
'CALL_METHOD', 'LOAD_METHOD', 'CALL_METHOD', 'LOAD_METHOD',