forked from Mapan/odoo17e
205 lines
10 KiB
Python
205 lines
10 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.osv import expression
|
|
from odoo.tools.misc import unquote
|
|
|
|
|
|
class HelpdeskTicket(models.Model):
|
|
_inherit = 'helpdesk.ticket'
|
|
|
|
def _domain_sale_line_id(self):
|
|
domain = expression.AND([
|
|
self.env['sale.order.line']._sellable_lines_domain(),
|
|
[
|
|
('company_id', '=', unquote('company_id')),
|
|
('is_service', '=', True),
|
|
('order_partner_id', 'child_of', unquote('commercial_partner_id')),
|
|
('is_expense', '=', False),
|
|
('state', '=', 'sale')],
|
|
]
|
|
)
|
|
return domain
|
|
|
|
use_helpdesk_sale_timesheet = fields.Boolean('Reinvoicing Timesheet activated on Team', related='team_id.use_helpdesk_sale_timesheet', readonly=True)
|
|
sale_order_id = fields.Many2one('sale.order', compute="_compute_helpdesk_sale_order", compute_sudo=True, store=True, readonly=False)
|
|
invoice_count = fields.Integer(related='sale_order_id.invoice_count')
|
|
display_invoice_button = fields.Boolean(compute='_compute_display_invoice_button', compute_sudo=True)
|
|
sale_line_id = fields.Many2one(
|
|
'sale.order.line', string="Sales Order Item", tracking=True,
|
|
compute="_compute_sale_line_id", store=True, readonly=False,
|
|
domain=lambda self: str(self._domain_sale_line_id()),
|
|
help="Sales Order Item to which the time spent on this ticket will be added in order to be invoiced to your customer.\n"
|
|
"By default the last prepaid sales order item that has time remaining will be selected.\n"
|
|
"Remove the sales order item in order to make this ticket non-billable.\n"
|
|
"You can also change or remove the sales order item of each timesheet entry individually.")
|
|
project_sale_order_id = fields.Many2one('sale.order', string="Project's sale order", related='project_id.sale_order_id')
|
|
remaining_hours_available = fields.Boolean(related="sale_line_id.remaining_hours_available")
|
|
remaining_hours_so = fields.Float('Remaining Hours on SO', compute='_compute_remaining_hours_so', search='_search_remaining_hours_so')
|
|
|
|
@api.constrains('sale_line_id')
|
|
def _check_sale_line_type(self):
|
|
for ticket in self:
|
|
if ticket.sale_line_id and not ticket.sale_line_id.is_service:
|
|
raise ValidationError(_(
|
|
'You cannot link order item %s - %s to this ticket because it is not a service product.',
|
|
ticket.sale_line_id.order_id.name,
|
|
ticket.sale_line_id.product_id.display_name,
|
|
))
|
|
|
|
@api.depends('sale_line_id', 'timesheet_ids', 'timesheet_ids.unit_amount')
|
|
def _compute_remaining_hours_so(self):
|
|
# TODO This is not yet perfectly working as timesheet.so_line stick to its old value although changed
|
|
# in the task From View.
|
|
timesheets = self.timesheet_ids.filtered(lambda t: t.helpdesk_ticket_id.sale_line_id in (t.so_line, t._origin.so_line) and t.so_line.remaining_hours_available)
|
|
|
|
mapped_remaining_hours = {ticket._origin.id: ticket.sale_line_id and ticket.sale_line_id.remaining_hours or 0.0 for ticket in self}
|
|
uom_hour = self.env.ref('uom.product_uom_hour')
|
|
for timesheet in timesheets:
|
|
delta = 0
|
|
if timesheet._origin.so_line == timesheet.helpdesk_ticket_id.sale_line_id:
|
|
delta += timesheet._origin.unit_amount
|
|
if timesheet.so_line == timesheet.helpdesk_ticket_id.sale_line_id:
|
|
delta -= timesheet.unit_amount
|
|
if delta:
|
|
mapped_remaining_hours[timesheet.helpdesk_ticket_id._origin.id] += timesheet.product_uom_id._compute_quantity(delta, uom_hour)
|
|
|
|
for ticket in self:
|
|
ticket.remaining_hours_so = mapped_remaining_hours[ticket._origin.id]
|
|
|
|
@api.model
|
|
def _search_remaining_hours_so(self, operator, value):
|
|
return [('sale_line_id.remaining_hours', operator, value)]
|
|
|
|
@api.depends('partner_id', 'use_helpdesk_sale_timesheet', 'project_id.pricing_type', 'project_id.sale_line_id')
|
|
def _compute_sale_line_id(self):
|
|
billable_tickets = self.filtered('use_helpdesk_sale_timesheet')
|
|
(self - billable_tickets).update({
|
|
'sale_line_id': False
|
|
})
|
|
for ticket in billable_tickets:
|
|
if ticket.project_id and ticket.project_id.pricing_type != 'task_rate':
|
|
ticket.sale_line_id = ticket.project_id.sale_line_id
|
|
# Check sale_line_id and customer are coherent
|
|
if ticket.sale_line_id.sudo().order_partner_id.commercial_partner_id != ticket.commercial_partner_id:
|
|
ticket.sale_line_id = False
|
|
if not ticket.sale_line_id:
|
|
ticket.sale_line_id = ticket._get_last_sol_of_customer()
|
|
|
|
def _compute_display_invoice_button(self):
|
|
for ticket in self:
|
|
ticket.display_invoice_button = ticket.use_helpdesk_sale_timesheet and\
|
|
(ticket.sale_order_id or ticket.sale_line_id) and ticket.invoice_count > 0
|
|
|
|
def _get_last_sol_of_customer(self):
|
|
# Get the last SOL made for the customer in the current task where we need to compute
|
|
self.ensure_one()
|
|
if not self.commercial_partner_id or not self.project_id.allow_billable or not self.use_helpdesk_sale_timesheet:
|
|
return False
|
|
domain = [('company_id', '=', self.company_id.id), ('is_service', '=', True), ('order_partner_id', 'child_of', self.commercial_partner_id.id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('remaining_hours', '>', 0)]
|
|
if self.project_id.pricing_type != 'task_rate' and self.project_sale_order_id and self.commercial_partner_id == self.project_id.partner_id.commercial_partner_id:
|
|
domain.append(('order_id', '=?', self.project_sale_order_id.id))
|
|
return self.env['sale.order.line'].search(domain, limit=1)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
tickets = super().create(vals_list)
|
|
sol_ids = {
|
|
vals['sale_line_id']
|
|
for vals in vals_list
|
|
if vals.get('sale_line_id')
|
|
}
|
|
if sol_ids:
|
|
tickets._ensure_sale_order_linked(list(sol_ids))
|
|
return tickets
|
|
|
|
def _ensure_sale_order_linked(self, sol_ids):
|
|
quotations = self.env['sale.order.line'].sudo()._read_group(
|
|
domain=[('state', '=', 'draft'), ('id', 'in', sol_ids)],
|
|
aggregates=['order_id:recordset'],
|
|
)[0][0]
|
|
if quotations:
|
|
quotations.action_confirm()
|
|
|
|
def write(self, values):
|
|
recompute_so_lines = None
|
|
other_timesheets = None
|
|
if 'timesheet_ids' in values and isinstance(values.get('timesheet_ids'), (tuple, list)):
|
|
# Then, we check if the list contains tuples/lists like "(code=1, timesheet_id, vals)" and we extract timesheet_id if it is an update and 'so_line' in vals
|
|
timesheet_ids = [command[1] for command in values.get('timesheet_ids') if isinstance(command, (list, tuple)) and command[0] == 1 and 'so_line' in command[2]]
|
|
recompute_so_lines = self.timesheet_ids.filtered(lambda t: t.id in timesheet_ids).mapped('so_line')
|
|
if not self.env.user.has_group('hr_timesheet.group_hr_timesheet_approver') and values.get('sale_line_id', None):
|
|
# We need to search the timesheets of other employee to update the so_line
|
|
other_timesheets = self.env['account.analytic.line'].sudo().search([('id', 'not in', timesheet_ids), ('helpdesk_ticket_id', '=', self.id)])
|
|
|
|
res = super(HelpdeskTicket, self).write(values)
|
|
if sol_id := values.get('sale_line_id'):
|
|
self._ensure_sale_order_linked([sol_id])
|
|
if other_timesheets:
|
|
# Then we update the so_line if needed
|
|
compute_timesheets = defaultdict(list, [(timesheet, timesheet.so_line) for timesheet in other_timesheets]) # key = timesheet and value = so_line of the timesheet before the _compute_so_line
|
|
other_timesheets._compute_so_line()
|
|
for timesheet, sol in compute_timesheets.items():
|
|
if timesheet.so_line != sol:
|
|
recompute_so_lines |= sol
|
|
if recompute_so_lines:
|
|
recompute_so_lines._compute_qty_delivered()
|
|
return res
|
|
|
|
@api.depends('sale_line_id', 'project_id.sale_order_id')
|
|
def _compute_helpdesk_sale_order(self):
|
|
for ticket in self:
|
|
if ticket.sale_line_id:
|
|
ticket.sale_order_id = ticket.sale_line_id.order_id
|
|
elif ticket.project_id.sale_order_id:
|
|
ticket.sale_order_id = ticket.project_id.sale_order_id
|
|
else:
|
|
ticket.sale_order_id = False
|
|
if ticket.sale_order_id and not ticket.partner_id:
|
|
ticket.partner_id = ticket.sale_order_id.partner_id
|
|
|
|
@api.model
|
|
def _sla_reset_trigger(self):
|
|
field_list = super()._sla_reset_trigger()
|
|
field_list.append('sale_line_id')
|
|
return field_list
|
|
|
|
def _sla_find_extra_domain(self):
|
|
self.ensure_one()
|
|
domain = super()._sla_find_extra_domain()
|
|
return expression.AND([domain, [
|
|
'|', '|', ('partner_ids', 'parent_of', self.partner_id.ids),
|
|
'|', ('partner_ids', 'child_of', self.partner_id.ids),
|
|
('sale_line_ids', 'in', self.sale_line_id.ids),
|
|
'&', ('partner_ids', '=', False),
|
|
('sale_line_ids', '=', False)
|
|
]])
|
|
|
|
def action_view_so(self):
|
|
self.ensure_one()
|
|
action_window = {
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "sale.order",
|
|
"name": _("Sales Order"),
|
|
"views": [[False, "form"]],
|
|
"context": {"create": False, "show_sale": True},
|
|
"res_id": self.sale_line_id.order_id.id or self.sale_order_id.id
|
|
}
|
|
return action_window
|
|
|
|
def action_view_invoices(self):
|
|
self.ensure_one()
|
|
invoices = self.sale_order_id.invoice_ids
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "account.move",
|
|
"name": _("Invoices"),
|
|
"views": [[False, 'form']] if len(invoices) == 1 else [[False, "tree"], [False, "form"]],
|
|
"context": {"create": False},
|
|
"domain": [('id', 'in', invoices.ids)],
|
|
"res_id": invoices.id if len(invoices) == 1 else False,
|
|
}
|