294 lines
11 KiB
Python
294 lines
11 KiB
Python
from odoo import fields, models, api, _, Command
|
|
|
|
|
|
class SaleOrderLine(models.Model):
|
|
_inherit = "sale.order.line"
|
|
valid_equipment_ids = fields.One2many(
|
|
comodel_name="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="fsm.equipment",
|
|
relation="bemade_fsm_equipment_sale_order_line_rel",
|
|
column1="sale_order_line_id",
|
|
column2="equipment_id",
|
|
)
|
|
|
|
is_field_service = fields.Boolean(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
|
|
vals["partner_id"] = self.order_id.partner_id.id
|
|
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=%(so_id)d>%(so_name)s</a> (%(product_name)s)"
|
|
) % {
|
|
"so_id": self.order_id.id,
|
|
"so_name": self.order_id.name,
|
|
"product_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 line: line.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, sum(task_ids.mapped("allocated_hours"))
|
|
)
|
|
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, allocated_hours: 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,
|
|
"allocated_hours": allocated_hours,
|
|
"date_deadline": self.visit_id.approx_date,
|
|
"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 line: line.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 line: line.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 line: line.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"
|
|
)
|