Purpose ======= This PR is in continuity of #62360 and #62900 done in 14.0. The goal of this PR essentially updates and improves the changes done in 14.0. ## Details - Remove the context key added in 14.0 to keep the 'Create Sale Order' button in task and helpdesk.ticket. Because at the end we remove this button in these models. - Remove non_allow_billable field in tasks and timesheets. - Remove the partner_email from task form view. - Remove timesheet_product_id from task form view - Merge sale_timesheet_edit module with sale_timesheet module. - Remove timesheet table in portal sales order and invoice (deprecated templates in #62900). - Remove override of project kanban controller (deprecated file from #62360). - Store remaining_hours field in sale.order.line model (this field has been introduced in the following PR #62900). - Remove _onchange_task_id_employee_id, the process in the method is done in _compute_so_line. - Merge bill_type and pricing_type in project.project. - Move partner_id field in project form view. - Remove value selected in SO and SOL in project if the customer does not fit with the SO selected. - Changes SOL in mapping when the pricing type of the current project is "employee rate" if the customer changes. - Hide the timesheet_product_id field if the project is not a fsm project. - Move sql constraint for timesheet_product_id in industry_fsm_sale, because this field is no longer mandatory in project which is not fsm project. - Changes the search of timesheets in portal > timesheets. - we allows the user to search with name of SO and invoice to find the timesheets about the wanted SO or invoice rathan than the id of SOL and invoice. - Move View Timesheets button in portal > invoice - Remove deprecated codes. - Miscellaneous fixes. Task-2424382 closes odoo/odoo#64474 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com> Co-authored-by: Laurent Stukkens (LTU) <ltu@odoo.com>
159 lines
7.5 KiB
Python
159 lines
7.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from ast import literal_eval
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
class Project(models.Model):
|
|
_inherit = 'project.project'
|
|
|
|
sale_line_id = fields.Many2one(
|
|
'sale.order.line', 'Sales Order Item', copy=False,
|
|
compute="_compute_sale_line_id", store=True, readonly=False,
|
|
domain="[('is_service', '=', True), ('is_expense', '=', False), ('order_id', '=', sale_order_id), ('state', 'in', ['sale', 'done']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
|
help="Sales order item to which the project is linked. Link the timesheet entry to the sales order item defined on the project. "
|
|
"Only applies on tasks without sale order item defined, and if the employee is not in the 'Employee/Sales Order Item Mapping' of the project.")
|
|
sale_order_id = fields.Many2one('sale.order', 'Sales Order',
|
|
compute="_compute_sale_order_id", store=True, readonly=False,
|
|
domain="[('order_line.product_id.type', '=', 'service'), ('partner_id', '=', partner_id)]",
|
|
copy=False, help="Sales order to which the project is linked.")
|
|
project_overview = fields.Boolean('Show Project Overview', compute='_compute_project_overview')
|
|
|
|
_sql_constraints = [
|
|
('sale_order_required_if_sale_line', "CHECK((sale_line_id IS NOT NULL AND sale_order_id IS NOT NULL) OR (sale_line_id IS NULL))", 'The project should be linked to a sale order to select a sale order item.'),
|
|
]
|
|
|
|
@api.model
|
|
def _map_tasks_default_valeus(self, task, project):
|
|
defaults = super()._map_tasks_default_valeus(task, project)
|
|
defaults['sale_line_id'] = False
|
|
return defaults
|
|
|
|
@api.depends('sale_order_id')
|
|
def _compute_sale_line_id(self):
|
|
self.filtered(lambda p: p.sale_line_id and (not p.sale_order_id or p.sale_order_id != p.sale_line_id.order_id)).update({'sale_line_id': False})
|
|
|
|
@api.depends('partner_id')
|
|
def _compute_sale_order_id(self):
|
|
self.filtered(lambda p: p.sale_order_id and (not p.partner_id or p.sale_order_id.partner_id != p.partner_id)).update({'sale_order_id': False})
|
|
|
|
@api.depends('analytic_account_id')
|
|
def _compute_project_overview(self):
|
|
overview = self.env['project.project']
|
|
if self.user_has_groups('analytic.group_analytic_accounting'):
|
|
overview = self.filtered(lambda p: p.analytic_account_id)
|
|
overview.project_overview = True
|
|
(self - overview).project_overview = False
|
|
|
|
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_line_id = fields.Many2one(
|
|
'sale.order.line', 'Sales Order Item', domain="[('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_id', '=?', project_sale_order_id)]",
|
|
compute='_compute_sale_line', store=True, readonly=False, copy=False,
|
|
help="Sales order item to which the project is linked. Link the timesheet entry to the sales order item defined on the project. "
|
|
"Only applies on tasks without sale order item defined, and if the employee is not in the 'Employee/Sales Order Item Mapping' of the project.")
|
|
project_sale_order_id = fields.Many2one('sale.order', string="Project's sale order", related='project_id.sale_order_id')
|
|
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('project_id.sale_line_id.order_partner_id')
|
|
def _compute_partner_id(self):
|
|
for task in self:
|
|
if not task.partner_id:
|
|
task.partner_id = task.project_id.sale_line_id.order_partner_id
|
|
super()._compute_partner_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:
|
|
if not task.sale_line_id:
|
|
task.sale_line_id = task.parent_id.sale_line_id or task.project_id.sale_line_id
|
|
# check sale_line_id and customer are coherent
|
|
if task.sale_line_id.order_partner_id.commercial_partner_id != task.partner_id.commercial_partner_id:
|
|
task.sale_line_id = False
|
|
|
|
@api.constrains('sale_line_id')
|
|
def _check_sale_line_type(self):
|
|
for task in self.sudo():
|
|
if task.sale_line_id:
|
|
if not task.sale_line_id.is_service or task.sale_line_id.is_expense:
|
|
raise ValidationError(_(
|
|
'You cannot link the order item %(order_id)s - %(product_id)s to this task because it is a re-invoiced expense.',
|
|
order_id=task.sale_line_id.order_id.id,
|
|
product_id=task.sale_line_id.product_id.name,
|
|
))
|
|
|
|
@api.ondelete(at_uninstall=False)
|
|
def _unlink_except_linked_so(self):
|
|
if any(task.sale_line_id for task in self):
|
|
raise ValidationError(_('You have to unlink the task from the sale order item in order to delete it.'))
|
|
|
|
# ---------------------------------------------------
|
|
# Actions
|
|
# ---------------------------------------------------
|
|
|
|
def _get_action_view_so_ids(self):
|
|
return self.sale_order_id.ids
|
|
|
|
def action_view_so(self):
|
|
self.ensure_one()
|
|
so_ids = self._get_action_view_so_ids()
|
|
action_window = {
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "sale.order",
|
|
"name": "Sales Order",
|
|
"views": [[False, "tree"], [False, "form"]],
|
|
"context": {"create": False, "show_sale": True},
|
|
"domain": [["id", "in", so_ids]],
|
|
}
|
|
if len(so_ids) == 1:
|
|
action_window["views"] = [[False, "form"]]
|
|
action_window["res_id"] = so_ids[0]
|
|
|
|
return action_window
|
|
|
|
def rating_get_partner_id(self):
|
|
partner = self.partner_id or self.sale_line_id.order_id.partner_id
|
|
if partner:
|
|
return partner
|
|
return super().rating_get_partner_id()
|
|
|
|
@api.depends('sale_order_id.invoice_status', 'sale_order_id.order_line')
|
|
def _compute_task_to_invoice(self):
|
|
for task in self:
|
|
if task.sale_order_id:
|
|
task.task_to_invoice = bool(task.sale_order_id.invoice_status not in ('no', 'invoiced'))
|
|
else:
|
|
task.task_to_invoice = False
|
|
|
|
@api.model
|
|
def _search_task_to_invoice(self, operator, value):
|
|
query = """
|
|
SELECT so.id
|
|
FROM sale_order so
|
|
WHERE so.invoice_status != 'invoiced'
|
|
AND so.invoice_status != 'no'
|
|
"""
|
|
operator_new = 'inselect'
|
|
if(bool(operator == '=') ^ bool(value)):
|
|
operator_new = 'not inselect'
|
|
return [('sale_order_id', operator_new, (query, ()))]
|
|
|
|
|
|
class ProjectTaskRecurrence(models.Model):
|
|
_inherit = 'project.task.recurrence'
|
|
|
|
def _new_task_values(self, task):
|
|
values = super(ProjectTaskRecurrence, self)._new_task_values(task)
|
|
task = self.sudo().task_ids[0]
|
|
values['sale_line_id'] = self._get_sale_line_id(task)
|
|
return values
|
|
|
|
def _get_sale_line_id(self, task):
|
|
return task.sale_line_id.id
|