From c2e33753bbe903be399bc6a3bda89f8719612900 Mon Sep 17 00:00:00 2001 From: karuna Date: Tue, 19 May 2026 12:08:41 +0530 Subject: [PATCH] project modifications --- addons/bus/models/bus.py | 2 +- addons/l10n_fr_pos_cert/__manifest__.py | 2 +- addons/survey/controllers/main.py | 2 +- addons/website/__manifest__.py | 2 +- .../security/security.xml | 124 +++---- .../module_selector_sidebar/__manifest__.py | 30 +- .../my_custom_kudo_link/models/__init__.py | 6 +- .../__manifest__.py | 9 +- .../data/data.xml | 1 + .../project_task_timesheet_extended/hooks.py | 26 +- .../models/project.py | 2 - .../models/project_task.py | 83 +++-- .../security/security.xml | 320 +++++++++--------- .../js/involved_assignee_avatar_user_field.js | 34 ++ .../view/project_task.xml | 51 +-- .../view/project_task_gantt.xml | 29 +- odoo/addons/base/models/ir_qweb.py | 2 +- 17 files changed, 380 insertions(+), 345 deletions(-) create mode 100644 addons_extensions/project_task_timesheet_extended/static/src/js/involved_assignee_avatar_user_field.js diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py index a9a312253..fde380d08 100644 --- a/addons/bus/models/bus.py +++ b/addons/bus/models/bus.py @@ -103,7 +103,7 @@ class ImBus(models.Model): """Low-level method to send ``notification_type`` and ``message`` to ``target``. 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 attacker. diff --git a/addons/l10n_fr_pos_cert/__manifest__.py b/addons/l10n_fr_pos_cert/__manifest__.py index 15e4cd110..a2192fb62 100644 --- a/addons/l10n_fr_pos_cert/__manifest__.py +++ b/addons/l10n_fr_pos_cert/__manifest__.py @@ -6,7 +6,7 @@ 'version': '1.0', 'category': 'Accounting/Localizations/Point of Sale', '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. diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 8891b34d3..ec9d4ba0a 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -41,7 +41,7 @@ class Survey(http.Controller): 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 - 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 * survey_wrong: survey does not exist; diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index 175a757bf..a9c52dc0b 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -24,7 +24,7 @@ }, 'installable': True, '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/ir.model.access.csv', 'data/image_library.xml', diff --git a/addons_extensions/hr_employee_appraisal/security/security.xml b/addons_extensions/hr_employee_appraisal/security/security.xml index 042e37683..5c009714b 100644 --- a/addons_extensions/hr_employee_appraisal/security/security.xml +++ b/addons_extensions/hr_employee_appraisal/security/security.xml @@ -1,62 +1,62 @@ - - - - - - - - - Appraisal - 50 - - - - - Appraisal Officer - - - - - - Appraisal HR Manager - - - - - - Appraisal Administrator - - - - - - - - User can only see his/her own appraisals - - - [('user_id','=',user.id),('state','!=','draft')] - - - User can only see the records of people under him/her - - - [('reviewers_name.user_id','=',user.id),('state','!=','draft')] - - - - User can only see the all the appraisal records where he/she is set as HR - - - [('appraisal_hr_id','=',user.id)] - - - User can only see the all the appraisal records where he/she is set as MD - - - [('appraisal_md_id','=',user.id)] - - - - - + + + + + + + + + Appraisal + 50 + + + + + Appraisal Officer + + + + + + Appraisal HR Manager + + + + + + Appraisal Administrator + + + + + + + + User can only see his/her own appraisals + + + [('user_id','=',user.id),('state','!=','draft')] + + + User can only see the records of people under him/her + + + [('reviewers_name.user_id','=',user.id),('state','!=','draft')] + + + + User can only see the all the appraisal records where he/she is set as HR + + + [('appraisal_hr_id','=',user.id)] + + + User can only see the all the appraisal records where he/she is set as MD + + + [('appraisal_md_id','=',user.id)] + + + + + diff --git a/addons_extensions/module_selector_sidebar/__manifest__.py b/addons_extensions/module_selector_sidebar/__manifest__.py index b50d9a133..53da8e514 100644 --- a/addons_extensions/module_selector_sidebar/__manifest__.py +++ b/addons_extensions/module_selector_sidebar/__manifest__.py @@ -1,15 +1,15 @@ -{ - "name": "Custom Module Switcher", - "version": "1.0", - "depends": ["web","menu_control_center"], - 'data': [ - 'security/ir.model.access.csv', - ], - "assets": { - "web.assets_backend": [ - "module_selector_sidebar/static/src/js/module_switcher.js", - "module_selector_sidebar/static/src/xml/module_switcher.xml", - ], - }, - "installable": True, -} +{ + "name": "Custom Module Switcher", + "version": "1.0", + "depends": ["web","menu_control_center"], + 'data': [ + 'security/ir.model.access.csv', + ], + "assets": { + "web.assets_backend": [ + "module_selector_sidebar/static/src/js/module_switcher.js", + "module_selector_sidebar/static/src/xml/module_switcher.xml", + ], + }, + "installable": True, +} \ No newline at end of file diff --git a/addons_extensions/my_custom_kudo_link/models/__init__.py b/addons_extensions/my_custom_kudo_link/models/__init__.py index af5d37357..be9c095ce 100644 --- a/addons_extensions/my_custom_kudo_link/models/__init__.py +++ b/addons_extensions/my_custom_kudo_link/models/__init__.py @@ -1,4 +1,4 @@ -from . import project_kudo_extend -from . import task_assignee_domain -from . import project_task +from . import project_kudo_extend +from . import task_assignee_domain +from . import project_task from . import account_analytic_line \ No newline at end of file diff --git a/addons_extensions/project_task_timesheet_extended/__manifest__.py b/addons_extensions/project_task_timesheet_extended/__manifest__.py index 47537a154..11c586806 100644 --- a/addons_extensions/project_task_timesheet_extended/__manifest__.py +++ b/addons_extensions/project_task_timesheet_extended/__manifest__.py @@ -49,9 +49,9 @@ Key Features: 'view/maintenance_support.xml', 'view/project_closer.xml', 'view/project_actual_costings.xml', + 'view/project_task.xml', 'view/project.xml', 'view/project_portfolio.xml', - 'view/project_task.xml', 'view/timesheets.xml', 'view/pro_task_gantt.xml', 'view/user_availability.xml', @@ -61,9 +61,10 @@ Key Features: 'view/stage_approval_wizard.xml', ], 'assets': { - 'web.assets_backend':{ - 'project_task_timesheet_extended/static/src/css/delopyment.css' - } + 'web.assets_backend': [ + 'project_task_timesheet_extended/static/src/css/delopyment.css', + 'project_task_timesheet_extended/static/src/js/involved_assignee_avatar_user_field.js', + ] }, 'installable': True, 'application': False, diff --git a/addons_extensions/project_task_timesheet_extended/data/data.xml b/addons_extensions/project_task_timesheet_extended/data/data.xml index 3cf6427e4..8c09610d1 100644 --- a/addons_extensions/project_task_timesheet_extended/data/data.xml +++ b/addons_extensions/project_task_timesheet_extended/data/data.xml @@ -263,4 +263,5 @@ + diff --git a/addons_extensions/project_task_timesheet_extended/hooks.py b/addons_extensions/project_task_timesheet_extended/hooks.py index 9e82a6274..dcba4fd6f 100644 --- a/addons_extensions/project_task_timesheet_extended/hooks.py +++ b/addons_extensions/project_task_timesheet_extended/hooks.py @@ -120,14 +120,18 @@ def post_init_hook(env): '|', '&', ('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), ('user_id.partner_id', 'in', 'project_id.message_partner_ids'), '&', '&', ('project_id.privacy_visibility', '!=', 'followers'), ('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].append(task) - # Assign sequence numbers to tasks - for project_id, task_list in project_tasks.items(): - project = env['project.project'].browse(project_id) - if project.task_sequence_id: - for task in task_list: - task.sequence_name = project.task_sequence_id.next_by_id() - - # Normalize task stages so each project owns its workflow configuration. - env['project.project'].search([])._ensure_project_owned_task_stages() + # Assign sequence numbers to tasks + for project_id, task_list in project_tasks.items(): + project = env['project.project'].browse(project_id) + if project.task_sequence_id: + for task in task_list: + task.sequence_name = project.task_sequence_id.next_by_id() + + # Normalize task stages so each project owns its workflow configuration. + env['project.project'].search([])._ensure_project_owned_task_stages() diff --git a/addons_extensions/project_task_timesheet_extended/models/project.py b/addons_extensions/project_task_timesheet_extended/models/project.py index 3b2df1b50..08f65d2d7 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project.py +++ b/addons_extensions/project_task_timesheet_extended/models/project.py @@ -333,8 +333,6 @@ class ProjectProject(models.Model): users_list = list() if project.assign_approval_flow: users_list.extend(project.project_stages.involved_users.ids) - else: - users_list.extend(project.showable_stage_ids.user_ids.ids) if project.project_sponsor: users_list.append(project.project_sponsor.id) diff --git a/addons_extensions/project_task_timesheet_extended/models/project_task.py b/addons_extensions/project_task_timesheet_extended/models/project_task.py index acff3037b..2d0566b26 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project_task.py +++ b/addons_extensions/project_task_timesheet_extended/models/project_task.py @@ -167,12 +167,12 @@ class projectTask(models.Model): task.assignee_domain_ids = all_internal_users continue - # # GENERIC → all internal + # # GENERIC: all internal # if getattr(task, 'is_generic', False): # task.assignee_domain_ids = all_internal_users # continue - # PRIVATE → invited users only + # PRIVATE: invited users only if task.project_id.privacy_visibility == 'followers': task.assignee_domain_ids = ( task.project_id.message_partner_ids @@ -196,18 +196,18 @@ class projectTask(models.Model): for task in self: employees = Employee.browse() - # 1️⃣ GENERIC TASK + # GENERIC TASK if task.is_generic and task.project_id: project = task.project_id - # 🔐 Private → followers only + # Private: followers only if project.privacy_visibility == 'followers': users = ( project.message_partner_ids .mapped('user_ids') .filtered(lambda u: u and not u.share) ) - # 🌍 Internal / Public → all internal users + # Internal / Public: all internal users else: users = self.env['res.users'].search([ ('share', '=', False), @@ -216,7 +216,7 @@ class projectTask(models.Model): employees = users.mapped('employee_id').filtered(lambda e: e) - # 2️⃣ NORMAL TASK → task assignees only + # NORMAL TASK: assignees and involved collaborators else: employees = ( task.user_ids @@ -393,7 +393,7 @@ class projectTask(models.Model): if start_dt.tzinfo is None: start_dt = pytz.UTC.localize(start_dt) - # Convert UTC → calendar timezone + # Convert UTC to calendar timezone start_dt_tz = start_dt.astimezone(tz) # Call plan_hours @@ -459,8 +459,8 @@ class projectTask(models.Model): self.env.user.name, 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): administrative_users = self.env['project.role'].search([ ('role_level', '=', 'administrative') @@ -478,26 +478,13 @@ class projectTask(models.Model): stages = project.type_ids.sorted("sequence") - if not stages: - continue + if first_stage: + 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 = ( - 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 - ) - ): + 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): task.has_supervisor_access = True @api.depends('assignees_timelines.estimated_time', 'show_approval_flow') @@ -637,11 +624,11 @@ class projectTask(models.Model): task.show_approval_button = True 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): 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 ( assigned_to and assigned_to == responsible_lead @@ -797,7 +784,7 @@ class projectTask(models.Model): task.stage_id = n_stage 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, self.env.user.employee_id.name, n_stage.name) @@ -838,9 +825,9 @@ class projectTask(models.Model): ) else: 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, self.env.user.employee_id.name) @@ -876,9 +863,9 @@ class projectTask(models.Model): # Optional: find previous stage if you want to send back 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, self.env.user.employee_id.name, 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." )) - 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: - if task.assignees_timelines: - users_list = list( - 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)] + if task.is_generic: + continue - # Post to project channel about assignee update - channel_message = _("Assignees updated for task %s") % (task.sequence_name or task.name) - task._post_to_project_channel(channel_message) + timeline_user_ids = set( + task.assignees_timelines.responsible_lead.ids + + 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): use_timeline_logic = any( @@ -1419,7 +1414,7 @@ class projectTask(models.Model): FROM project_task T1 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 U2 + INNER JOIN project_task_user_rel U2 ON T2.id = U2.task_id AND U1.user_id = U2.user_id WHERE diff --git a/addons_extensions/project_task_timesheet_extended/security/security.xml b/addons_extensions/project_task_timesheet_extended/security/security.xml index 6aa216cd7..6d8077944 100644 --- a/addons_extensions/project_task_timesheet_extended/security/security.xml +++ b/addons_extensions/project_task_timesheet_extended/security/security.xml @@ -1,161 +1,161 @@ - - - - Manager - - - - - - Project Lead - - - - - - - - - - - company: Own Company - - [('company_id', 'in', company_ids + [False])] - - - - Manager: Own Projects - - - [('user_id', '=', user.id)] - - - - - - - - Project/Task: project supervisor: see all tasks linked to his assigned project or its own tasks - - [ - ('project_id.user_id','=',user.id), - '|', ('project_id', '!=', False), - ('user_ids', 'in', user.id), - ] - - - - - Project/Task: project users: don't see non generic tasks - - [ - '&', '&', - ('project_id', '!=', False), - ('is_generic', '=', False), - ('user_ids', 'not in', user.id), - ] - - - - - - - Project/Task: project lead: see all tasks - - [ - '&', '&', '&', - ('project_id', '!=', False), - ('project_id.project_lead', '=', user.id), - '|', ('is_generic', '=', True), ('is_generic', '=', False), - '|', ('user_ids', 'in', user.id), ('user_ids', 'not in', user.id) - ] - - - - - - - - - - Task Availability: project lead: see all user tasks - - - [ - '|', '|', - ('project_id.project_lead', '=', user.id), - ('user_id', '=', user.id), - ('project_id.user_id', '=', user.id), - ] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + Manager + + + + + + Project Lead + + + + + + + + + + + company: Own Company + + [('company_id', 'in', company_ids + [False])] + + + + Manager: Own Projects + + + [('user_id', '=', user.id)] + + + + + + + + Project/Task: project supervisor: see all tasks linked to his assigned project or its own tasks + + [ + ('project_id.user_id','=',user.id), + '|', ('project_id', '!=', False), + ('user_ids', 'in', user.id), + ] + + + + + Project/Task: project users: don't see non generic tasks + + [ + '&', '&', + ('project_id', '!=', False), + ('is_generic', '=', False), + ('user_ids', 'not in', user.id), + ] + + + + + + + Project/Task: project lead: see all tasks + + [ + '&', '&', '&', + ('project_id', '!=', False), + ('project_id.project_lead', '=', user.id), + '|', ('is_generic', '=', True), ('is_generic', '=', False), + '|', ('user_ids', 'in', user.id), ('user_ids', 'not in', user.id) + ] + + + + + + + + + + Task Availability: project lead: see all user tasks + + + [ + '|', '|', + ('project_id.project_lead', '=', user.id), + ('user_id', '=', user.id), + ('project_id.user_id', '=', user.id), + ] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons_extensions/project_task_timesheet_extended/static/src/js/involved_assignee_avatar_user_field.js b/addons_extensions/project_task_timesheet_extended/static/src/js/involved_assignee_avatar_user_field.js new file mode 100644 index 000000000..fc78ad93f --- /dev/null +++ b/addons_extensions/project_task_timesheet_extended/static/src/js/involved_assignee_avatar_user_field.js @@ -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); diff --git a/addons_extensions/project_task_timesheet_extended/view/project_task.xml b/addons_extensions/project_task_timesheet_extended/view/project_task.xml index 6d956ab68..e2ae059d8 100644 --- a/addons_extensions/project_task_timesheet_extended/view/project_task.xml +++ b/addons_extensions/project_task_timesheet_extended/view/project_task.xml @@ -41,29 +41,31 @@ [('id', 'in', parent.allowed_employee_ids)] - - - - - - + + + + + +

- - - + + + + + - - - - + + + + @@ -132,7 +134,7 @@
- Based on the timelines, the deadline can't be met + Based on the timelines, the deadline can't be met
@@ -143,16 +145,17 @@
- - - - - - - - - - + + + + + + + + + +
diff --git a/addons_extensions/project_task_timesheet_extended/view/project_task_gantt.xml b/addons_extensions/project_task_timesheet_extended/view/project_task_gantt.xml index 383eb986c..f15994f6d 100644 --- a/addons_extensions/project_task_timesheet_extended/view/project_task_gantt.xml +++ b/addons_extensions/project_task_timesheet_extended/view/project_task_gantt.xml @@ -34,7 +34,7 @@ - + @@ -45,7 +45,7 @@ - + @@ -67,21 +67,21 @@ - + - + - + - + @@ -89,7 +89,7 @@ - + @@ -201,7 +201,7 @@
- Project — + Project — @@ -211,7 +211,7 @@
- Performance — + Performance — Good (Actual < Estimated) @@ -230,24 +230,24 @@
- Milestone — + Milestone —
- Assignees — + Assignees —
- Customer — + Customer —
- Timeline — + Timeline — hours estimated / hours actual
- Allocated Time — + Allocated Time —
@@ -302,7 +302,6 @@
diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py index 7af4f0b52..57bc5c9e7 100644 --- a/odoo/addons/base/models/ir_qweb.py +++ b/odoo/addons/base/models/ir_qweb.py @@ -412,7 +412,7 @@ token.QWEB = token.NT_OFFSET - 1 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([ 'MAKE_FUNCTION', 'CALL_FUNCTION', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_EX', 'CALL_METHOD', 'LOAD_METHOD',