From 149a30f6f365bd1ccdcee565fde825e1ef8ea521 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Mon, 1 Apr 2024 08:45:29 -0400 Subject: [PATCH] bemade_fsm: manually merge in 15.0 differences and fix 2 failing tests --- bemade_fsm/models/__init__.py | 3 +- bemade_fsm/models/fsm_visit.py | 10 ++-- bemade_fsm/models/project.py | 11 ++++ bemade_fsm/models/sale_order.py | 46 ++++++++++++++++- bemade_fsm/models/sale_order_line.py | 51 +++++++++++++++---- bemade_fsm/models/task.py | 4 +- .../worksheet_custom_report_templates.xml | 5 +- bemade_fsm/tests/test_bemade_fsm_common.py | 46 ++++++----------- bemade_fsm/tests/test_equipment.py | 4 ++ bemade_fsm/tests/test_fsm_visit.py | 35 ++++++++----- bemade_fsm/tests/test_sale_order.py | 50 +++++++++++++++++- bemade_fsm/views/task_views.xml | 47 +++++++++++++++++ 12 files changed, 246 insertions(+), 66 deletions(-) create mode 100644 bemade_fsm/models/project.py diff --git a/bemade_fsm/models/__init__.py b/bemade_fsm/models/__init__.py index 1fbeeea..a80bc37 100644 --- a/bemade_fsm/models/__init__.py +++ b/bemade_fsm/models/__init__.py @@ -8,4 +8,5 @@ from . import equipment_tag from . import task from . import res_partner from . import fsm_visit -from . import res_company \ No newline at end of file +from . import res_company +from . import project diff --git a/bemade_fsm/models/fsm_visit.py b/bemade_fsm/models/fsm_visit.py index 988d556..c610fc1 100644 --- a/bemade_fsm/models/fsm_visit.py +++ b/bemade_fsm/models/fsm_visit.py @@ -5,9 +5,9 @@ class FSMVisit(models.Model): _name = "bemade_fsm.visit" _description = 'Represents a single visit by assigned service personnel.' - label = fields.Text(string="Label", required=True, related='so_section_id.name', readonly=False) + label = fields.Text(string="Label", required=True, related='so_section_id.name', readonly=False, copy=True) - approx_date = fields.Date(string='Approximate Date') + approx_date = fields.Date(string='Approximate Date', copy=False) so_section_id = fields.Many2one( comodel_name="sale.order.line", @@ -19,7 +19,7 @@ class FSMVisit(models.Model): sale_order_id = fields.Many2one( comodel_name="sale.order", string="Sales Order", - required=True + readonly=True, ) is_completed = fields.Boolean(string="Completed", related="so_section_id.is_fully_delivered") @@ -61,10 +61,10 @@ class FSMVisit(models.Model): @api.model_create_multi def create(self, vals_list): recs = super().create(vals_list) - for i, rec in enumerate(recs): + for i, rec in enumerate(recs.filtered(lambda visit: not visit.so_section_id)): rec.so_section_id = rec.env['sale.order.line'].create({ 'order_id': rec.sale_order_id.id, 'display_type': 'line_section', - 'name': vals_list[i]['label'], + 'name': vals_list[i].get('label', False), }) return recs diff --git a/bemade_fsm/models/project.py b/bemade_fsm/models/project.py new file mode 100644 index 0000000..c9cd7bd --- /dev/null +++ b/bemade_fsm/models/project.py @@ -0,0 +1,11 @@ +from odoo import models + + +class Project(models.Model): + _inherit = "project.project" + + def _fetch_sale_order_item_ids(self, domain_per_model=None, limit=None, offset=None): + # Override to flush the ORM cache to the database prior to running the query + # Temporary fix until Odoo fixes this method (PR #160067 submitted for this) + self.env.flush_all() + return super()._fetch_sale_order_item_ids(domain_per_model, limit, offset) diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py index 82a1a58..b6c0ac2 100644 --- a/bemade_fsm/models/sale_order.py +++ b/bemade_fsm/models/sale_order.py @@ -50,11 +50,17 @@ 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() linked_lines = self.order_line.filtered(lambda l: l.task_id == task_id - or l == task_id.visit_id.so_section_id) + or l == task_id.visit_id.so_section_id) visit_lines = linked_lines.filtered(lambda l: l.visit_id) for line in visit_lines: linked_lines |= line.get_section_line_ids() @@ -102,3 +108,41 @@ class SaleOrder(models.Model): 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.filtered("company_id.create_default_fsm_visit"): + 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 528b77c..449ccc5 100644 --- a/bemade_fsm/models/sale_order_line.py +++ b/bemade_fsm/models/sale_order_line.py @@ -14,7 +14,8 @@ class SaleOrderLine(models.Model): visit_ids = fields.One2many( comodel_name="bemade_fsm.visit", inverse_name="so_section_id", - string="Visits" + string="Visits", + copy=True, ) visit_id = fields.Many2one( @@ -22,7 +23,7 @@ class SaleOrderLine(models.Model): compute="_compute_visit_id", string="Visit", ondelete='cascade', - store=True + store=True, ) is_fully_delivered = fields.Boolean( @@ -56,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: @@ -74,6 +86,13 @@ class SaleOrderLine(models.Model): 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 @@ -140,7 +159,7 @@ class SaleOrderLine(models.Model): def _timesheet_service_generation(self): super()._timesheet_service_generation() visit_lines = self.filtered(lambda l: l.visit_id) - for line in visit_lines: + for index, line in enumerate(visit_lines): task_ids = line.get_section_line_ids().mapped('task_id') if not task_ids: continue @@ -148,16 +167,15 @@ class SaleOrderLine(models.Model): # 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) + 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.task_id.filtered("is_fsm").synchronize_name_fsm() + (self.mapped('task_id') | self.visit_ids.task_id).filtered("is_fsm").synchronize_name_fsm() - def _generate_task_for_visit_line(self, project): + def _generate_task_for_visit_line(self, project, visit_no: int): self.ensure_one() task = self.env['project.task'].create({ - 'name': 'Temp Name', - 'description': f"Parent task for {self.order_id.name}, visit {self.name}", + '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, @@ -207,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) @@ -244,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: @@ -265,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/models/task.py b/bemade_fsm/models/task.py index ea70a0f..0967fcb 100644 --- a/bemade_fsm/models/task.py +++ b/bemade_fsm/models/task.py @@ -101,9 +101,9 @@ class Task(models.Model): prev_seqs = self.sale_order_id.tasks_ids and \ self.sale_order_id.tasks_ids.mapped('work_order_number') if prev_seqs: - pattern = re.compile(r"\d+$") + pattern = re.compile(r"(\d+)$") matches = map(lambda n: pattern.search(n), prev_seqs) - seq += max(map(lambda n: int(n.group(1)) if n else 0), matches) + seq += max(map(lambda n: int(n.group(1)) if n else 0, matches)) rec.work_order_number = rec.sale_order_id.name.replace('SO', 'SVR', 1) \ + f"-{seq}" return res diff --git a/bemade_fsm/reports/worksheet_custom_report_templates.xml b/bemade_fsm/reports/worksheet_custom_report_templates.xml index 7733981..3defd15 100644 --- a/bemade_fsm/reports/worksheet_custom_report_templates.xml +++ b/bemade_fsm/reports/worksheet_custom_report_templates.xml @@ -3,9 +3,8 @@