diff --git a/addons/sale_project/__manifest__.py b/addons/sale_project/__manifest__.py index 1325521e871..73a3d8f5cd8 100644 --- a/addons/sale_project/__manifest__.py +++ b/addons/sale_project/__manifest__.py @@ -16,6 +16,7 @@ This module allows to generate a project/task from sales orders. 'security/sale_project_security.xml', 'views/product_views.xml', 'views/project_task_views.xml', + 'views/sale_order_line_views.xml', 'views/sale_order_views.xml', 'views/sale_project_portal_templates.xml', 'views/project_sharing_views.xml', diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index b5535e47167..1f68fef58e6 100755 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -26,6 +26,7 @@ class Project(models.Model): "It can be modified on each task and timesheet entry individually if necessary.") sale_order_id = fields.Many2one(string='Sales Order', related='sale_line_id.order_id', help="Sales order to which the project is linked.") has_any_so_to_invoice = fields.Boolean('Has SO to Invoice', compute='_compute_has_any_so_to_invoice') + sale_order_line_count = fields.Integer(compute='_compute_sale_order_count', groups='sales_team.group_sale_salesman') sale_order_count = fields.Integer(compute='_compute_sale_order_count', groups='sales_team.group_sale_salesman') has_any_so_with_nothing_to_invoice = fields.Boolean('Has a SO with an invoice status of No', compute='_compute_has_any_so_with_nothing_to_invoice') invoice_count = fields.Integer(compute='_compute_invoice_count', groups='account.group_account_readonly') @@ -94,7 +95,9 @@ class Project(models.Model): def _compute_sale_order_count(self): sale_order_items_per_project_id = self._fetch_sale_order_items_per_project_id({'project.task': [('state', 'not in', list(CLOSED_STATES))]}) for project in self: - project.sale_order_count = len(sale_order_items_per_project_id.get(project.id, self.env['sale.order.line']).order_id) + sale_order_lines = sale_order_items_per_project_id.get(project.id, self.env['sale.order.line']) + project.sale_order_line_count = len(sale_order_lines) + project.sale_order_count = len(sale_order_lines.order_id) def _compute_invoice_count(self): query = self.env['account.move.line']._search([('move_id.move_type', 'in', ['out_invoice', 'out_refund'])]) @@ -115,6 +118,35 @@ class Project(models.Model): for project in self: project.display_sales_stat_buttons = project.allow_billable and project.partner_id + def action_view_sols(self): + self.ensure_one() + all_sale_order_lines = self._fetch_sale_order_items({'project.task': [('state', 'not in', list(CLOSED_STATES))]}) + action_window = { + 'type': 'ir.actions.act_window', + 'res_model': 'sale.order.line', + 'name': _("%(name)s's Sales Order Items", name=self.name), + 'context': { + 'show_sale': True, + 'link_to_project': self.id, + 'form_view_ref': 'sale_project.sale_order_line_view_form_editable', # Necessary for some logic in the form view + 'default_partner_id': self.partner_id.id, + 'default_company_id': self.company_id.id, + 'default_order_id': self.sale_order_id.id, + }, + 'views': [(self.env.ref('sale_project.sale_order_line_view_form_editable').id, 'form')], + } + if len(all_sale_order_lines) <= 1: + action_window['res_id'] = all_sale_order_lines.id + else: + action_window.update({ + 'domain': [('id', 'in', all_sale_order_lines.ids)], + 'views': [ + (self.env.ref('sale_project.view_order_line_tree_with_create').id, 'tree'), + (self.env.ref('sale_project.sale_order_line_view_form_editable').id, 'form'), + ], + }) + return action_window + def action_view_sos(self): self.ensure_one() all_sale_orders = self._fetch_sale_order_items({'project.task': [('state', 'not in', list(CLOSED_STATES))]}).order_id @@ -616,6 +648,16 @@ class Project(models.Model): 'show': self_sudo.display_sales_stat_buttons and self_sudo.sale_order_count > 0, 'sequence': 27, }) + if self.user_has_groups('sales_team.group_sale_salesman_all_leads'): + buttons.append({ + 'icon': 'dollar', + 'text': _lt('Sales Order Items'), + 'number': self.sale_order_line_count, + 'action_type': 'object', + 'action': 'action_view_sols', + 'show': self.display_sales_stat_buttons, + 'sequence': 28, + }) if self.user_has_groups('account.group_account_readonly'): self_sudo = self.sudo() buttons.append({ diff --git a/addons/sale_project/models/sale_order_line.py b/addons/sale_project/models/sale_order_line.py index 8bb903d9b8c..8bb9ff39505 100644 --- a/addons/sale_project/models/sale_order_line.py +++ b/addons/sale_project/models/sale_order_line.py @@ -5,6 +5,7 @@ from collections import defaultdict from markupsafe import escape from odoo import api, Command, fields, models, _ +from odoo.exceptions import AccessError from odoo.tools import format_amount from odoo.tools.sql import column_exists, create_column @@ -22,6 +23,52 @@ class SaleOrderLine(models.Model): # used to know if generate a task and/or a project, depending on the product settings reached_milestones_ids = fields.One2many('project.milestone', 'sale_line_id', string='Reached Milestones', domain=[('is_reached', '=', True)]) + def default_get(self, fields): + res = super().default_get(fields) + if self.env.context.get('form_view_ref') == 'sale_project.sale_order_line_view_form_editable': + default_values = { + 'name': _("New Sales Order Item"), + } + + # If we can't add order lines to the default order, discard it + if 'order_id' in res: + try: + self.env['sale.order'].browse(res['order_id']).check_access_rule('write') + except AccessError: + del res['order_id'] + + if 'order_id' in fields and 'order_id' not in res: + assert (partner_id := self.env.context.get('default_partner_id')) + project_id = self.env.context.get('link_to_project') + sale_order = None + if project_id: + try: + project_so = self.env['project.project'].browse(project_id).sale_order_id + project_so.check_access_rule('write') + sale_order = project_so + except AccessError: + pass + + if not sale_order: + so_create_values = { + 'partner_id': partner_id, + 'project_ids': [Command.link(project_id)], + } + else: + so_create_values = { + 'partner_id': partner_id, + } + + if not sale_order: + company_id = self.env.context.get('defaut_company_id', False) + if company_id: + so_create_values['company_id'] = company_id + sale_order = self.env['sale.order'].create(so_create_values) + sale_order.action_confirm() + default_values['order_id'] = sale_order.id + return {**res, **default_values} + return res + @api.depends('product_id.type') def _compute_product_updatable(self): super()._compute_product_updatable() @@ -70,6 +117,13 @@ class SaleOrderLine(models.Model): if line.task_id and not has_task: msg_body = escape(_("Task Created (%s): %s")) % (line.product_id.name, line.task_id._get_html_link()) line.order_id.message_post(body=msg_body) + + # Set a service SOL on the project, if any is given + if (project_id := self.env.context.get('link_to_project')): + assert (service_line := next((line for line in lines if line.is_service), False)) + project = self.env['project.project'].browse(project_id) + if not project.sale_line_id: + project.sale_line_id = service_line return lines def write(self, values): diff --git a/addons/sale_project/views/sale_order_line_views.xml b/addons/sale_project/views/sale_order_line_views.xml new file mode 100644 index 00000000000..a43fe2efcc7 --- /dev/null +++ b/addons/sale_project/views/sale_order_line_views.xml @@ -0,0 +1,35 @@ + + + + sale.order.line.tree.with.create + sale.order.line + + primary + 999 + + + true + + + + + + sale.order.line.view.form.editable + sale.order.line + + primary + 999 + +
+ true +
+ + [('type', '=', 'service')] + { + 'default_detailed_type': 'service', + } + +
+
+ +
diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index a5cf88d4d88..9dc6cd32566 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -195,7 +195,9 @@ class Project(models.Model): def _compute_sale_order_count(self): billable_projects = self.filtered('allow_billable') super(Project, billable_projects)._compute_sale_order_count() - (self - billable_projects).sale_order_count = 0 + non_billable_projects = self - billable_projects + non_billable_projects.sale_order_line_count = 0 + non_billable_projects.sale_order_count = 0 @api.constrains('sale_line_id') def _check_sale_line_type(self):