@@ -266,4 +278,8 @@
+
+
+ Planned Hours:
+
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 @@
+
+
+
+
+
+
+ for sale order:
+ |
+
+ for sale order item:
+ |
+
+
+
+
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 @@
+
+
+
+
+ Invoices:
+
+
+ ,
+
+ ,
+
+
+ Invoiced:
+
+
+ To invoice:
+
+
+
+
+
+
+
+
+
+ Remaining Days on SO:
+ Remaining Hours on SO:
+
+
+
+