diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py index 0210444..a5a487a 100644 --- a/bemade_fsm/__manifest__.py +++ b/bemade_fsm/__manifest__.py @@ -20,7 +20,7 @@ ######################################################################################## { 'name': 'Improved Field Service Management', - 'version': '15.0.1.0.1', + 'version': '15.0.1.0.2', 'summary': 'Adds functionality necessary for managing field service operations at Durpro.', 'description': 'Adds functionality necessary for managing field service operations at Durpro.', 'category': 'Services/Field Service', diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py index 15b23df..d1fcc4d 100644 --- a/bemade_fsm/models/sale_order.py +++ b/bemade_fsm/models/sale_order.py @@ -50,6 +50,12 @@ class SaleOrder(models.Model): readonly=False ) + is_fsm = fields.Boolean( + compute='_compute_is_fsm', + string='Is FSM', + store=True, + ) + @api.depends('order_line.task_id') def get_relevant_order_lines(self, task_id): self.ensure_one() @@ -101,3 +107,42 @@ class SaleOrder(models.Model): rec = super().copy(default) rec.visit_ids = [Command.set(rec.order_line.visit_ids.ids)] return rec + + def _create_default_visit(self): + """ Called when an order is confirmed with lines that will create an FSM task, in order to make sure there is + a visit line grouping all the service being done.""" + self.ensure_one() + visit = self.env['bemade_fsm.visit'].create({ + 'label': _('Service Visit'), + 'sale_order_id': self.id, + }) + # Make sure it goes to the top of the list + visit.so_section_id.sequence = 0 + + def _create_or_organize_visits_if_needed(self): + """ Adds a visit line to the top of the order if there are not already visit lines for an order with lines that + will create an FSM task.""" + for order in self: + if not order.visit_ids and order.is_fsm: + order._create_default_visit() + if order.is_fsm: + # Make sure that all the lines producing FSM tasks are under a visit + visit_line_ids = order.mapped('visit_ids').mapped('so_section_id').mapped('section_line_ids') + if any([ + True for line in + order.order_line.filtered(lambda line: not line.display_type) + if line not in visit_line_ids + ]): + # If not, promote the first visit to the top of the order items list + for line in order.order_line: + line.sequence += 1 + order.mapped('visit_ids').mapped('so_section_id')[0].sequence = 0 + + @api.depends('order_line.is_fsm') + def _compute_is_fsm(self): + for rec in self: + rec.is_fsm = any([line.is_fsm for line in rec.order_line]) + + def action_confirm(self): + self._create_or_organize_visits_if_needed() + return super().action_confirm() diff --git a/bemade_fsm/models/sale_order_line.py b/bemade_fsm/models/sale_order_line.py index bdab84f..91d0cf3 100644 --- a/bemade_fsm/models/sale_order_line.py +++ b/bemade_fsm/models/sale_order_line.py @@ -57,6 +57,17 @@ class SaleOrderLine(models.Model): compute="_compute_task_duration" ) + 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: @@ -214,6 +225,14 @@ class SaleOrderLine(models.Model): 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) @@ -251,8 +270,7 @@ class SaleOrderLine(models.Model): uom_hour = self.env.ref('uom.product_uom_hour') uom_unit = self.env.ref('uom.product_uom_unit') templated_lines = self.filtered(lambda l: l.product_id.task_template_id - and l.product_id.task_template_id. - planned_hours) + and l.product_id.task_template_id.planned_hours) visit_lines = self.filtered(lambda l: l.visit_id) regular_lines = self - templated_lines - visit_lines for line in regular_lines: @@ -272,3 +290,9 @@ class SaleOrderLine(models.Model): for line in visit_lines: line.task_duration = sum(line.get_section_line_ids(). mapped('task_duration')) + + @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') diff --git a/bemade_fsm/tests/test_sale_order.py b/bemade_fsm/tests/test_sale_order.py index 9fbc6c4..c51246c 100644 --- a/bemade_fsm/tests/test_sale_order.py +++ b/bemade_fsm/tests/test_sale_order.py @@ -300,3 +300,23 @@ class TestSalesOrder(BemadeFSMBaseTest): self.assertEqual(so2.order_line[0].visit_id, visit2) self.assertEqual(visit2.label, visit.label) self.assertFalse(visit2.approx_date) + + def test_confirming_sale_order_creates_visit_if_none_created(self): + so = self._generate_sale_order() + product = self._generate_product() + sol = self._generate_sale_order_line(so, product) + + so.action_confirm() + + visit_line = so.order_line.sorted('sequence')[0] + self.assertTrue(so.visit_ids) + self.assertEqual(visit_line.visit_id, so.visit_ids) + + def test_confirming_sale_order_with_visit_creates_no_new_lines(self): + so = self._generate_sale_order() + product = self._generate_product() + visit = self._generate_visit(so) + + so.action_confirm() + + self.assertEqual(len(so.visit_ids), 1)