diff --git a/addons/hr_timesheet/controllers/portal.py b/addons/hr_timesheet/controllers/portal.py index a9dcb714ba5..98109496d6c 100644 --- a/addons/hr_timesheet/controllers/portal.py +++ b/addons/hr_timesheet/controllers/portal.py @@ -33,6 +33,11 @@ class TimesheetCustomerPortal(CustomerPortal): 'task': {'input': 'task', 'label': _('Search in Task')} } + def _task_get_searchbar_sortings(self): + values = super()._task_get_searchbar_sortings() + values['progress'] = {'label': _('Progress'), 'order': 'progress asc'} + return values + def _get_searchbar_groupby(self): return { 'none': {'input': 'none', 'label': _('None')}, diff --git a/addons/hr_timesheet/views/project_portal_templates.xml b/addons/hr_timesheet/views/project_portal_templates.xml index 627d482389f..a4626d1933a 100644 --- a/addons/hr_timesheet/views/project_portal_templates.xml +++ b/addons/hr_timesheet/views/project_portal_templates.xml @@ -8,10 +8,20 @@
Timesheets
- - - + +
Progress: %
+
+ + + + + + - - diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index ab3a61858d0..f5bb7cc91c9 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -9,14 +9,13 @@ from markupsafe import Markup from odoo import http, _ from odoo.exceptions import AccessError, MissingError from odoo.http import request -from odoo.addons.portal.controllers import portal -from odoo.addons.portal.controllers.portal import pager as portal_pager +from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager from odoo.tools import groupby as groupbyelem from odoo.osv.expression import OR -class CustomerPortal(portal.CustomerPortal): +class ProjectCustomerPortal(CustomerPortal): def _prepare_home_portal_values(self, counters): values = super()._prepare_home_portal_values(counters) @@ -94,39 +93,104 @@ class CustomerPortal(portal.CustomerPortal): # My Task # ------------------------------------------------------------ def _task_get_page_view_values(self, task, access_token, **kwargs): + try: + project_accessible = bool(task.project_id.id and self._document_check_access('project.project', task.project_id.id)) + except (AccessError, MissingError): + project_accessible = False + values = { 'page_name': 'task', 'task': task, - 'user': request.env.user + 'user': request.env.user, + 'project_accessible': project_accessible, } return self._get_page_view_values(task, access_token, values, 'my_tasks_history', False, **kwargs) - @http.route(['/my/tasks', '/my/tasks/page/'], type='http', auth="user", website=True) - def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', groupby=None, **kw): - values = self._prepare_portal_layout_values() - searchbar_sortings = { + def _task_get_searchbar_sortings(self): + return { 'date': {'label': _('Newest'), 'order': 'create_date desc'}, 'name': {'label': _('Title'), 'order': 'name'}, 'stage': {'label': _('Stage'), 'order': 'stage_id, project_id'}, 'project': {'label': _('Project'), 'order': 'project_id, stage_id'}, 'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc'}, + 'date_deadline': {'label': _('Deadline'), 'order': 'date_deadline asc'}, } + + def _task_get_searchbar_groupby(self): + values = { + 'none': {'input': 'none', 'label': _('None'), 'order': 1}, + 'project': {'input': 'project', 'label': _('Project'), 'order': 2}, + 'stage': {'input': 'stage', 'label': _('Stage'), 'order': 3}, + 'user': {'input': 'user', 'label': _('Assigned to'), 'order': 6}, + 'customer': {'input': 'customer', 'label': _('Customer'), 'order': 7}, + } + return dict(sorted(values.items(), key=lambda item: item[1]["order"])) + + def _task_get_groupby_mapping(self): + return { + 'project': 'project_id', + 'stage': 'stage_id', + 'user': 'user_id', + 'customer': 'partner_id', + } + + def _task_get_order(self, order, groupby): + groupby_mapping = self._task_get_groupby_mapping() + field_name = groupby_mapping.get(groupby, '') + if not field_name: + return order + return '%s, %s' % (field_name, order) + + def _task_get_grouped_tasks(self, groupby, tasks): + if groupby: + grouped_tasks = [request.env['project.task'].concat(*g) for k, g in groupbyelem(tasks, itemgetter(groupby))] + else: + grouped_tasks = [tasks] + return grouped_tasks + + def _task_get_searchbar_inputs(self): + values = { + 'all': {'input': 'all', 'label': _('Search in All'), 'order': 1}, + 'content': {'input': 'content', 'label': Markup(_('Search (in Content)')), 'order': 1}, + 'ref': {'input': 'ref', 'label': _('Search in Ref'), 'order': 1}, + 'project': {'input': 'project', 'label': _('Search in Project'), 'order': 2}, + 'stage': {'input': 'stage', 'label': _('Search in Stages'), 'order': 3}, + 'user': {'input': 'user', 'label': _('Search in Assigned to'), 'order': 7}, + 'message': {'input': 'message', 'label': _('Search in Messages'), 'order': 8}, + } + return dict(sorted(values.items(), key=lambda item: item[1]["order"])) + + def _task_get_search_domain(self, search_in, search): + search_domain = [] + if search_in in ('content', 'all'): + search_domain.append([('name', 'ilike', search)]) + search_domain.append([('description', 'ilike', search)]) + if search_in in ('customer', 'all'): + search_domain.append([('partner_id', 'ilike', search)]) + if search_in in ('message', 'all'): + search_domain.append([('message_ids.body', 'ilike', search)]) + if search_in in ('stage', 'all'): + search_domain.append([('stage_id', 'ilike', search)]) + if search_in in ('project', 'all'): + search_domain.append([('project_id', 'ilike', search)]) + if search_in in ('ref', 'all'): + search_domain.append([('id', 'ilike', search)]) + if search_in in ('user', 'all'): + user_ids = request.env['res.users'].sudo().search([('name', 'ilike', search)]) + search_domain.append([('user_id', 'in', user_ids.ids)]) + return OR(search_domain) + + @http.route(['/my/tasks', '/my/tasks/page/'], type='http', auth="user", website=True) + def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', groupby=None, **kw): + values = self._prepare_portal_layout_values() + searchbar_sortings = self._task_get_searchbar_sortings() + searchbar_filters = { 'all': {'label': _('All'), 'domain': []}, } - searchbar_inputs = { - 'content': {'input': 'content', 'label': Markup(_('Search (in Content)'))}, - 'message': {'input': 'message', 'label': _('Search in Messages')}, - 'customer': {'input': 'customer', 'label': _('Search in Customer')}, - 'stage': {'input': 'stage', 'label': _('Search in Stages')}, - 'project': {'input': 'project', 'label': _('Search in Project')}, - 'all': {'input': 'all', 'label': _('Search in All')}, - } - searchbar_groupby = { - 'none': {'input': 'none', 'label': _('None')}, - 'project': {'input': 'project', 'label': _('Project')}, - 'stage': {'input': 'stage', 'label': _('Stage')}, - } + + searchbar_inputs = self._task_get_searchbar_inputs() + searchbar_groupby = self._task_get_searchbar_groupby() # extends filterby criteria with project the customer has access to projects = request.env['project.project'].search([]) @@ -165,18 +229,7 @@ class CustomerPortal(portal.CustomerPortal): # search if search and search_in: - search_domain = [] - if search_in in ('content', 'all'): - search_domain = OR([search_domain, ['|', ('name', 'ilike', search), ('description', 'ilike', search)]]) - if search_in in ('customer', 'all'): - search_domain = OR([search_domain, [('partner_id', 'ilike', search)]]) - if search_in in ('message', 'all'): - search_domain = OR([search_domain, [('message_ids.body', 'ilike', search)]]) - if search_in in ('stage', 'all'): - search_domain = OR([search_domain, [('stage_id', 'ilike', search)]]) - if search_in in ('project', 'all'): - search_domain = OR([search_domain, [('project_id', 'ilike', search)]]) - domain += search_domain + domain += self._task_get_search_domain(search_in, search) # task count task_count = request.env['project.task'].search_count(domain) @@ -189,20 +242,13 @@ class CustomerPortal(portal.CustomerPortal): step=self._items_per_page ) # content according to pager and archive selected - if groupby == 'project': - order = "project_id, %s" % order # force sort on project first to group by project in view - elif groupby == 'stage': - order = "stage_id, %s" % order # force sort on stage first to group by stage in view + order = self._task_get_order(order, groupby) tasks = request.env['project.task'].search(domain, order=order, limit=self._items_per_page, offset=pager['offset']) request.session['my_tasks_history'] = tasks.ids[:100] - if groupby == 'project': - grouped_tasks = [request.env['project.task'].concat(*g) for k, g in groupbyelem(tasks, itemgetter('project_id'))] - elif groupby == 'stage': - grouped_tasks = [request.env['project.task'].concat(*g) for k, g in groupbyelem(tasks, itemgetter('stage_id'))] - else: - grouped_tasks = [tasks] + groupby_mapping = self._task_get_groupby_mapping() + grouped_tasks = self._task_get_grouped_tasks(groupby_mapping.get(groupby), tasks) values.update({ 'date': date_begin, diff --git a/addons/project/views/project_portal_templates.xml b/addons/project/views/project_portal_templates.xml index d4a2d5b7d9c..44d010fc39b 100644 --- a/addons/project/views/project_portal_templates.xml +++ b/addons/project/views/project_portal_templates.xml @@ -140,7 +140,13 @@ in stage: - Project + + assigned to: + + + for customer: + + Project Stage @@ -181,10 +187,12 @@
- - (#) -
- Status: +
+ + (#) +
+
+ Stage:
@@ -192,43 +200,47 @@
-
- Project: -
- -
-
- Date: -
-
- Deadline: -
-
- -
+
Assigned to
-
+
Contact
-
+
- Reported by + Customer
-
+
Contact
-
+
+
+
+ + +

@@ -266,4 +278,8 @@
+ + diff --git a/addons/sale_project/__init__.py b/addons/sale_project/__init__.py index dc5e6b693d1..8c67f893b37 100644 --- a/addons/sale_project/__init__.py +++ b/addons/sale_project/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import models +from . import controllers diff --git a/addons/sale_project/controllers/__init__.py b/addons/sale_project/controllers/__init__.py new file mode 100644 index 00000000000..903b755e71e --- /dev/null +++ b/addons/sale_project/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import portal diff --git a/addons/sale_project/controllers/portal.py b/addons/sale_project/controllers/portal.py new file mode 100644 index 00000000000..bc308b9280a --- /dev/null +++ b/addons/sale_project/controllers/portal.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from operator import itemgetter + +from odoo import _ +from odoo.http import request +from odoo.tools import groupby as groupbyelem + +from odoo.osv.expression import OR + +from odoo.addons.project.controllers.portal import ProjectCustomerPortal + + +class SaleProjectCustomerPortal(ProjectCustomerPortal): + + def _task_get_searchbar_groupby(self): + values = super()._task_get_searchbar_groupby() + values['sale_order'] = {'input': 'sale_order', 'label': _('Sales Order'), 'order': 4} + values['sale_line'] = {'input': 'sale_line', 'label': _('Sales Order Item'), 'order': 5} + return dict(sorted(values.items(), key=lambda item: item[1]["order"])) + + def _task_get_groupby_mapping(self): + groupby_mapping = super()._task_get_groupby_mapping() + groupby_mapping.update(sale_order='sale_order_id', sale_line='sale_line_id') + return groupby_mapping + + def _task_get_grouped_tasks(self, groupby, tasks): + if groupby: + grouped_tasks = [request.env['project.task'].concat(*g) for k, g in groupbyelem(tasks, itemgetter(groupby))] + else: + grouped_tasks = super()._task_get_grouped_tasks(groupby, tasks) + return grouped_tasks + + def _task_get_searchbar_inputs(self): + values = super()._task_get_searchbar_inputs() + values['sale_order'] = {'input': 'sale_order', 'label': _('Search in Sales Order'), 'order': 4} + values['sale_line'] = {'input': 'sale_line', 'label': _('Search in Sales Order Item'), 'order': 5} + values['invoice'] = {'input': 'invoice', 'label': _('Search in Invoice'), 'order': 6} + return dict(sorted(values.items(), key=lambda item: item[1]["order"])) + + def _task_get_search_domain(self, search_in, search): + search_domain = [super()._task_get_search_domain(search_in, search)] + if search_in in ('sale_order', 'all'): + search_domain.append([('sale_order_id.name', 'ilike', search)]) + if search_in in ('sale_line', 'all'): + search_domain.append([('sale_line_id.name', 'ilike', search)]) + if search_in in ('invoice', 'all'): + search_domain.append([('sale_order_id.invoice_ids.name', 'ilike', search)]) + return OR(search_domain) diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 9b5564a69a2..a10415efe77 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -45,7 +45,7 @@ class Project(models.Model): class ProjectTask(models.Model): _inherit = "project.task" - sale_order_id = fields.Many2one('sale.order', 'Sales Order', help="Sales order to which the task is linked.") + sale_order_id = fields.Many2one('sale.order', 'Sales Order', compute='_compute_sale_order_id', store=True, help="Sales order to which the task is linked.") sale_line_id = fields.Many2one( 'sale.order.line', 'Sales Order Item', domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]", compute='_compute_sale_line', recursive=True, store=True, readonly=False, copy=False, @@ -54,6 +54,20 @@ class ProjectTask(models.Model): invoice_count = fields.Integer("Number of invoices", related='sale_order_id.invoice_count') task_to_invoice = fields.Boolean("To invoice", compute='_compute_task_to_invoice', search='_search_task_to_invoice', groups='sales_team.group_sale_salesman_all_leads') + @api.depends('sale_line_id', 'project_id', 'commercial_partner_id') + def _compute_sale_order_id(self): + for task in self: + sale_order_id = task.sale_order_id or self.env["sale.order"] + if task.sale_line_id: + sale_order_id = task.sale_line_id.sudo().order_id + elif task.project_id.sale_order_id: + sale_order_id = task.project_id.sale_order_id + if task.commercial_partner_id != sale_order_id.partner_id.commercial_partner_id: + sale_order_id = False + if sale_order_id and not task.partner_id: + task.partner_id = sale_order_id.partner_id + task.sale_order_id = sale_order_id + @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id') def _compute_sale_line(self): for task in self: diff --git a/addons/sale_project/views/sale_project_portal_templates.xml b/addons/sale_project/views/sale_project_portal_templates.xml new file mode 100644 index 00000000000..24a2cfa15a1 --- /dev/null +++ b/addons/sale_project/views/sale_project_portal_templates.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/addons/sale_timesheet/controllers/portal.py b/addons/sale_timesheet/controllers/portal.py index c0abd48f955..cee2111da6e 100644 --- a/addons/sale_timesheet/controllers/portal.py +++ b/addons/sale_timesheet/controllers/portal.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import http, _ +from odoo.exceptions import AccessError, MissingError from odoo.http import request from odoo.osv import expression @@ -63,6 +64,24 @@ class SaleTimesheetCustomerPortal(TimesheetCustomerPortal): invoice='timesheet_invoice_id') return groupby_mapping + def _task_get_page_view_values(self, task, access_token, **kwargs): + values = super()._task_get_page_view_values(task, access_token, **kwargs) + values['so_accessible'] = False + try: + if task.sale_order_id and self._document_check_access('sale.order', task.sale_order_id.id): + values['so_accessible'] = True + except (AccessError, MissingError): + pass + + values['invoices_accessible'] = [] + for invoice in task.sale_order_id.invoice_ids: + try: + if self._document_check_access('account.move', invoice.id): + values['invoices_accessible'].append(invoice.id) + except (AccessError, MissingError): + pass + return values + @http.route(['/my/timesheets', '/my/timesheets/page/'], type='http', auth="user", website=True) def portal_my_timesheets(self, page=1, sortby=None, filterby=None, search=None, search_in='all', groupby='sol', **kw): return super().portal_my_timesheets(page, sortby, filterby, search, search_in, groupby, **kw) diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 905a474727f..9940fed70bd 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -342,9 +342,7 @@ class ProjectTask(models.Model): return project.sale_line_employee_ids.sale_line_id.order_partner_id[:1] return res - # override sale_order_id and make it computed stored field instead of regular field. - sale_order_id = fields.Many2one(compute='_compute_sale_order_id', store=True, readonly=False, - domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]") + sale_order_id = fields.Many2one(domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]") analytic_account_id = fields.Many2one('account.analytic.account', related='sale_order_id.analytic_account_id') pricing_type = fields.Selection(related="project_id.pricing_type") is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty') @@ -380,20 +378,11 @@ class ProjectTask(models.Model): for task in self: task.analytic_account_active = task.analytic_account_active or task.analytic_account_id.active - @api.depends('sale_line_id', 'project_id', 'allow_billable', 'commercial_partner_id') + @api.depends('allow_billable') def _compute_sale_order_id(self): - for task in self: - if not task.allow_billable: - task.sale_order_id = False - else: - if task.sale_line_id: - task.sale_order_id = task.sale_line_id.sudo().order_id - elif task.project_id.sale_order_id: - task.sale_order_id = task.project_id.sale_order_id - if task.commercial_partner_id != task.sale_order_id.partner_id.commercial_partner_id: - task.sale_order_id = False - if task.sale_order_id and not task.partner_id: - task.partner_id = task.sale_order_id.partner_id + billable_tasks = self.filtered('allow_billable') + super(ProjectTask, billable_tasks)._compute_sale_order_id() + (self - billable_tasks).sale_order_id = False @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id', 'allow_billable') def _compute_sale_line(self): diff --git a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml index 28d6f333181..4c22ab4f4ac 100644 --- a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml +++ b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml @@ -98,4 +98,38 @@ + + + +