From 37aea5c181feec9e824236a31c44db5997e3ebb3 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Thu, 20 Jul 2023 11:09:10 -0400 Subject: [PATCH] bemade_fsm: Visits on sales orders now group created tasks under a common parent. Fixes #72. --- bemade_fsm/models/fsm_visit.py | 10 ++++++++ bemade_fsm/models/sale_order.py | 38 ++++++++++++++++++++++++++---- bemade_fsm/models/task.py | 7 ++++-- bemade_fsm/tests/test_fsm_visit.py | 33 ++++++++++++++++---------- 4 files changed, 68 insertions(+), 20 deletions(-) diff --git a/bemade_fsm/models/fsm_visit.py b/bemade_fsm/models/fsm_visit.py index 0cc023f..74b594b 100644 --- a/bemade_fsm/models/fsm_visit.py +++ b/bemade_fsm/models/fsm_visit.py @@ -24,6 +24,16 @@ class FSMVisit(models.Model): summarized_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment", string="Equipment to Service", compute="_compute_summarized_equipment_ids") + task_id = fields.Many2one(comodel_name="project.task", + compute="_compute_task_id", + string="Service Visit",) + task_ids = fields.One2many(comodel_name="project.task", + inverse_name="visit_id") + + @api.depends('task_ids') + def _compute_task_id(self): + for rec in self: + rec.task_id = rec.task_ids and rec.task_ids[0] @api.depends('so_section_id', 'sale_order_id.summary_equipment_ids') def _compute_summarized_equipment_ids(self): diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py index 3c12223..0257314 100644 --- a/bemade_fsm/models/sale_order.py +++ b/bemade_fsm/models/sale_order.py @@ -89,6 +89,7 @@ class SaleOrderLine(models.Model): column1="sale_order_line_id", column2="equipment_id") + @api.model_create_multi def create(self, vals): recs = super().create(vals) @@ -162,6 +163,33 @@ class SaleOrderLine(models.Model): task.name = _generate_task_name(tmpl) return task + def _timesheet_service_generation(self): + super()._timesheet_service_generation() + visit_lines = self.filtered(lambda l: l.visit_id) + for line in visit_lines: + task_ids = line.get_section_lines().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) + task_ids.write({'parent_id': line.visit_id.task_id.id}) + + def _generate_task_for_visit_line(self, project): + self.ensure_one() + task = self.env['project.task'].create({ + 'name': self.order_id.name + ": " + self.name, + 'description': f"Parent task for {self.order_id.name}, visit {self.name}", + 'project_id': project.id, + 'equipment_ids': self.get_section_lines().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, + }) + 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): @@ -175,22 +203,22 @@ class SaleOrderLine(models.Model): self.is_fully_delivered_and_invoiced = self._iterate_items_compute_bool(lambda l: l.qty_to_invoice == 0) def get_section_lines(self): - """ Returns a list containing the sale order lines that fall under this section. """ + """ Returns a RecordSet containing the sale order lines that fall under this section. """ self.ensure_one() assert self.display_type == 'line_section', 'Method called incorrectly on non-section order line.' found = False lines = [] - for line in self.order_id: + for line in self.order_id.order_line: if line == self: found = True continue if not found: continue if line.display_type == 'line_section': # Stop when we hit the next section - return lines + break else: - lines.add(line) - return lines + lines.append(line) + return self.env['sale.order.line'].union(*lines) def _iterate_items_compute_bool(self, single_line_func): if not self.display_type: diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py index f4da20f..ef32d55 100644 --- a/bemade_fsm/models/task.py +++ b/bemade_fsm/models/task.py @@ -32,6 +32,8 @@ class Task(models.Model): allow_billable = fields.Boolean(string="Can be billed", related=False, compute="_compute_allow_billable",) + + visit_id = fields.Many2one(comodel_name='bemade_fsm.visit') @api.depends('sale_line_id.order_id.site_contacts', 'sale_line_id.order_id.work_order_contacts') def _compute_contacts(self): """ The work order contacts and site contacts for a given task are taken from the sale order if the task @@ -55,10 +57,11 @@ class Task(models.Model): 'site_contacts': [Command.set(rec.site_contacts.ids)], }) - @api.depends('parent_id', 'project_id') + @api.depends('parent_id.visit_id', 'project_id.is_fsm', 'project_id.allow_billable') def _compute_allow_billable(self): for rec in self: - if rec.parent_id and rec.project_id and rec.project_id.is_fsm: + # If an FSM task has a parent that is linked to an SO line, then the parent is the billable one + if rec.parent_id and not rec.parent_id.visit_id and rec.project_id and rec.project_id.is_fsm: rec.allow_billable = False else: rec.allow_billable = rec.project_id.allow_billable diff --git a/bemade_fsm/tests/test_fsm_visit.py b/bemade_fsm/tests/test_fsm_visit.py index cf01c1e..e4f64ce 100644 --- a/bemade_fsm/tests/test_fsm_visit.py +++ b/bemade_fsm/tests/test_fsm_visit.py @@ -7,7 +7,7 @@ class FSMVisitTest(BemadeFSMBaseTest): def test_create_visit_sets_name_on_section(self): so = self._generate_sale_order() - self._add_service_so_line(so) + self._generate_sale_order_line(sale_order=so) visit = self._generate_visit(so) @@ -35,7 +35,7 @@ class FSMVisitTest(BemadeFSMBaseTest): def test_visit_completes_when_task_completes(self): so = self._generate_sale_order() visit = self._generate_visit(so) - self._add_service_so_line(so, task=True) + self._generate_sale_order_line(so) so.action_confirm() task = so.order_line.filtered(lambda l: l.task_id).task_id @@ -46,7 +46,7 @@ class FSMVisitTest(BemadeFSMBaseTest): def test_visit_shows_invoiced_when_invoiced(self): so = self._generate_sale_order() visit = self._generate_visit(so) - self._add_service_so_line(so, task=True) + self._generate_sale_order_line(so) so.action_confirm() task = so.order_line.filtered(lambda l: l.task_id).task_id task.action_fsm_validate() @@ -55,6 +55,23 @@ class FSMVisitTest(BemadeFSMBaseTest): self.assertTrue(visit.is_invoiced) + def test_visit_groups_section_tasks_when_confirmed(self): + partner = self._generate_partner() + so = self._generate_sale_order() + visit = self._generate_visit(sale_order=so) + sol1 = self._generate_sale_order_line(sale_order=so) + sol2 = self._generate_sale_order_line(sale_order=so) + visit.so_section_id.sequence = 1 + sol1.sequence = 2 + sol2.sequence = 3 + + so.action_confirm() + + visit_task = visit.task_id + self.assertTrue(visit_task) + visit_subtasks = visit_task.child_ids + self.assertTrue(visit_subtasks and sol1.task_id in visit_subtasks and sol2.task_id in visit_subtasks) + def _invoice_sale_order(self, so): wiz = self.env['sale.advance.payment.inv'].with_context({'active_ids': [so.id]}).create({}) wiz.create_invoices() @@ -67,13 +84,3 @@ class FSMVisitTest(BemadeFSMBaseTest): 'sale_order_id': sale_order.id, 'label': label, }]) - - def _add_service_so_line(self, sale_order, task: bool = False): - """ Generates a sales order line for a service product. - - :param sale_order: The sales order to which the new line is to be added - :param task: If true, the created line will be for a product with service_tracking=task_global_project - """ - service_tracking = 'task_global_project' if task else 'no' - product = self._generate_product(service_tracking=service_tracking) - return self._generate_sale_order_line(sale_order, product)