bemade-addons/bemade_fsm/models/sale_order_line.py

255 lines
10 KiB
Python

from odoo import fields, models, api, _, Command
from odoo.exceptions import ValidationError
from odoo.tools import float_round
import re
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
valid_equipment_ids = fields.One2many(
comodel_name="bemade_fsm.equipment",
related="order_id.valid_equipment_ids"
)
visit_ids = fields.One2many(
comodel_name="bemade_fsm.visit",
inverse_name="so_section_id",
string="Visits",
copy=True,
)
visit_id = fields.Many2one(
comodel_name="bemade_fsm.visit",
compute="_compute_visit_id",
string="Visit",
ondelete='cascade',
store=True,
)
is_fully_delivered = fields.Boolean(
string="Fully Delivered",
compute="_compute_is_fully_delivered",
help="Indicates whether a line or all the lines in a section have been entirely delivered."
)
is_fully_delivered_and_invoiced = fields.Boolean(
string="Fully Invoiced",
compute="_compute_is_fully_invoiced",
help="Indicates whether a line or all the lines in a section have been entirely delivered and invoiced."
)
equipment_ids = fields.Many2many(
string="Equipment to Service",
comodel_name="bemade_fsm.equipment",
relation="bemade_fsm_equipment_sale_order_line_rel",
column1="sale_order_line_id",
column2="equipment_id"
)
is_field_service = fields.Boolean(
string="Is Field Service",
compute="_compute_is_field_service",
store=True
)
is_fsm = fields.Boolean(
string='Is FSM',
compute='_compute_is_fsm',
store=True,
)
section_line_ids = fields.One2many(
comodel_name='sale.order.line',
compute='_compute_section_line_ids',
)
@api.depends('visit_ids')
def _compute_visit_id(self):
for rec in self:
rec.visit_id = rec.visit_ids and rec.visit_ids[0]
@api.depends('product_id')
def _compute_is_field_service(self):
for rec in self:
rec.is_field_service = rec.product_id.is_field_service
@api.model_create_multi
def create(self, vals):
recs = super().create(vals)
for rec in recs:
if rec.order_id.default_equipment_ids and not rec.equipment_ids:
rec.equipment_ids = rec.order_id.default_equipment_ids
return recs
def copy_data(self, default=None):
if default is None:
default = {}
if 'visit_ids' not in default:
default['visit_ids'] = [(0, 0, visit.copy_data()[0]) for visit in self.visit_ids]
return super().copy_data(default)
def _timesheet_create_task(self, project):
""" Generate task for the given so line, and link it.
:param project: record of project.project in which the task should be created
:return task: record of the created task
Override to add the logic needed to implement task templates and equipment linkages."""
def _create_task_from_template(project, template, parent):
""" Recursively generates the task and any subtasks from a project.task.template.
:param project: project.project record to set on the task's project_id field.
:param template: project.task.template to use to create the task.
:param parent: project.task to set as the parent to this task.
"""
values = _timesheet_create_task_prepare_values_from_template(project, template, parent)
task = self.env['project.task'].sudo().create(values)
subtasks = []
for t in template.subtasks:
subtask = _create_task_from_template(project, t, task)
subtasks.append(subtask)
task.write({'child_ids': [Command.set([t.id for t in subtasks])]})
# We don't want to see the sub-tasks on the SO
task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, })
return task
def _timesheet_create_task_prepare_values_from_template(project, template,
parent):
""" Copies the values from a project.task.template over to the set of values used to create a project.task.
:param project: project.project record to set on the task's project_id field.
Pass the project.project model or an empty recordset to leave task project_id blank.
DO NOT pass False or None as this will cause an error in _timesheet_create_task_prepare_values(project).
:param template: project.task.template to use to create the task.
:param parent: project.task to set as the parent to this task.
"""
vals = self._timesheet_create_task_prepare_values(project)
vals['name'] = template.name
vals['description'] = template.description or '' if parent else vals['description']
vals['parent_id'] = parent and parent.id
vals['user_ids'] = template.assignees.ids
vals['tag_ids'] = template.tags.ids
vals['allocated_hours'] = template.planned_hours
vals['sequence'] = template.sequence
if template.equipment_ids:
vals['equipment_ids'] = template.equipment_ids.ids
return vals
tmpl = self.product_id.task_template_id
if not tmpl:
task = super()._timesheet_create_task(project)
else:
task = _create_task_from_template(project, tmpl, None)
self.write({'task_id': task.id})
# post message on task
task_msg = _(
"This task has been created from: <a href=# data-oe-model=sale.order data-oe-id=%d>%s</a> (%s)") % (
self.order_id.id, self.order_id.name, self.product_id.name)
task.message_post(body=task_msg)
if not task.equipment_ids and self.equipment_ids:
task.equipment_ids = self.equipment_ids.ids
return task
def _timesheet_service_generation(self):
super()._timesheet_service_generation()
visit_lines = self.filtered(lambda l: l.visit_id)
for index, line in enumerate(visit_lines):
task_ids = line.get_section_line_ids().mapped('task_id')
if not task_ids:
continue
if len(set([task.project_id for task in task_ids])) > 1:
# Can't group up the tasks if they're part of different projects
return
project_id = task_ids[0].project_id
line.visit_id.task_id = line._generate_task_for_visit_line(project_id, index + 1)
task_ids.write({'parent_id': line.visit_id.task_id.id})
(self.mapped('task_id') | self.visit_ids.task_id).filtered("is_fsm").synchronize_name_fsm()
def _generate_task_for_visit_line(self, project, visit_no: int):
self.ensure_one()
allocated_hours = sum(self.get_section_line_ids().task_id.mapped('allocated_hours'))
task = self.env['project.task'].create({
'name': f"{self.order_id.name} - " + _("Visit") + f" {visit_no} - {self.name}",
'project_id': project.id,
'equipment_ids': self.get_section_line_ids().mapped('equipment_ids').ids,
'sale_order_id': self.order_id.id,
'partner_id': self.order_id.partner_shipping_id.id,
'visit_id': self.visit_id.id,
'date_deadline': self.visit_id.approx_date,
'allocated_hours': allocated_hours,
'user_ids': False, # Force to empty or it uses the current user
})
return task
@api.depends(
'order_id.order_line',
'display_type',
'qty_to_deliver',
'order_id.order_line.qty_to_deliver',
'order_id.order_line.display_type'
)
def _compute_is_fully_delivered(self):
for rec in self:
rec.is_fully_delivered = rec._iterate_items_compute_bool(
lambda l: l.qty_to_deliver == 0)
@api.depends('is_fully_delivered')
def _compute_is_fully_invoiced(self):
for rec in self:
if not rec.is_fully_delivered:
rec.is_fully_delivered_and_invoiced = False
return
rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool(
lambda l: l.qty_to_invoice == 0)
def get_section_line_ids(self):
""" Return a recordset of sale.order.line records that are in this sale order section. """
self.ensure_one()
assert self.display_type == 'line_section', "Cannot get section lines for a non-section."
found = False
lines = []
for line in self.order_id.order_line.sorted(lambda l: l.sequence):
if line == self:
found = True
continue
if not found:
continue
if line.display_type == 'line_section': # Stop when we hit the next section
break
else:
lines.append(line)
return self.env['sale.order.line'].union(*lines)
@api.depends('display_type', 'order_id.order_line')
def _compute_section_line_ids(self):
for rec in self:
if rec.display_type == 'line_section':
rec.section_line_ids = [Command.set(rec.get_section_line_ids().ids)]
else:
rec.section_line_ids = False
def _iterate_items_compute_bool(self, single_line_func):
if not self.display_type:
return single_line_func(self)
elif self.display_type == 'line_note':
return True
else:
for line in self.order_id.order_line:
found = False
if line == self:
found = True
if not found:
continue
if found and line.display_type == 'line_section':
return True
val = single_line_func(self)
if not val:
return val
return True
@api.depends('product_id.detailed_type', 'product_id.service_tracking')
def _compute_is_fsm(self):
for rec in self:
rec.is_fsm = (rec.product_id.detailed_type == 'service'
and rec.product_id.service_tracking == 'task_global_project')