from odoo import fields, models, api, _, Command 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(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: %(so_name)s (%(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" )