From eea5b54a4ff26d35cc2b60ef7ec7d8d9cd747d78 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 11 Jul 2023 14:28:19 -0400 Subject: [PATCH] bemade_fsm: added a test for contact setting on SO shipping address change. Fixes #53. --- bemade_fsm/models/__init__.py | 1 + bemade_fsm/models/fsm_visit.py | 37 ++++++++ bemade_fsm/models/sale_order.py | 36 +++++++- bemade_fsm/security/ir.model.access.csv | 3 +- bemade_fsm/tests/__init__.py | 1 + bemade_fsm/tests/test_fsm_contact_setting.py | 54 ++++++++---- bemade_fsm/tests/test_fsm_visit.py | 91 ++++++++++++++++++++ bemade_fsm/views/sale_order_views.xml | 18 +++- 8 files changed, 221 insertions(+), 20 deletions(-) create mode 100644 bemade_fsm/models/fsm_visit.py create mode 100644 bemade_fsm/tests/test_fsm_visit.py diff --git a/bemade_fsm/models/__init__.py b/bemade_fsm/models/__init__.py index 997de6e..3cb9595 100644 --- a/bemade_fsm/models/__init__.py +++ b/bemade_fsm/models/__init__.py @@ -4,3 +4,4 @@ from . import sale_order from . import equipment from . import task from . import res_partner +from . import fsm_visit diff --git a/bemade_fsm/models/fsm_visit.py b/bemade_fsm/models/fsm_visit.py new file mode 100644 index 0000000..4426ef5 --- /dev/null +++ b/bemade_fsm/models/fsm_visit.py @@ -0,0 +1,37 @@ +from odoo import models, fields, api, _ + + +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) + approx_date = fields.Date(string='Approximate Date') + so_section_id = fields.Many2one(comodel_name="sale.order.line", + string="Sale Order Section", + help="The section on the sale order that represents the labour and parts for " + "this visit") + sale_order_id = fields.Many2one(comodel_name="sale.order", + string="Sales Order", + required=True) + is_completed = fields.Boolean(string="Completed", + related="so_section_id.is_fully_delivered") + is_invoiced = fields.Boolean(string="Invoiced", + compute="_compute_is_invoiced") + + def _compute_is_invoiced(self): + self.is_invoiced = False + + @api.model_create_multi + def create(self, vals_list): + recs = super().create(vals_list) + for i, rec in enumerate(recs): + 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'], + }) + return recs diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py index ed547d6..cb2f88f 100644 --- a/bemade_fsm/models/sale_order.py +++ b/bemade_fsm/models/sale_order.py @@ -1,4 +1,5 @@ from odoo import fields, models, api, _, Command +from odoo.exceptions import ValidationError class SaleOrder(models.Model): @@ -24,6 +25,9 @@ class SaleOrder(models.Model): inverse='_inverse_default_contacts', string='Work Order Recipients', store=True) + visit_ids = fields.One2many(comodel_name='bemade_fsm.visit', + inverse_name="sale_order_id", + readonly=False) @api.onchange('partner_shipping_id') def _onchange_partner_shipping_id(self): @@ -53,6 +57,12 @@ class SaleOrder(models.Model): class SaleOrderLine(models.Model): _inherit = 'sale.order.line' + visit_id = fields.One2many(comodel_name="bemade_fsm.visit", + inverse_name="so_section_id", + string="Visit") + is_fully_delivered = fields.Boolean(string="Fully Delivered", + compute="_compute_is_fully_delivered") + 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 @@ -78,7 +88,7 @@ class SaleOrderLine(models.Model): task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, }) return task - def _generate_task_name(template = None): + def _generate_task_name(template=None): template_name = template and template.name return f"{self.order_id.name}: {self.order_id.partner_shipping_id.name} - {self.name} ({template_name})" @@ -116,3 +126,27 @@ class SaleOrderLine(models.Model): task.equipment_id = self.order_id.equipment_id task.name = _generate_task_name(tmpl) 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): + if not self.display_type: + self.is_fully_delivered = self.qty_to_deliver == 0 + elif self.display_type == 'line_note': + self.is_fully_delivered = True + else: + for line in self.order_id.order_line: + # Iterate through the lines on the SO in order until we find this one in the list + found = False + if line == self: + found = True + if not found: + continue + # If we've hit the next section without returning False, all lines in this section were delivered + if found and line.display_type == 'line_section': + self.is_fully_delivered = True + return + if line.qty_to_deliver > 0: + self.is_fully_delivered = False + return + self.is_fully_delivered = True diff --git a/bemade_fsm/security/ir.model.access.csv b/bemade_fsm/security/ir.model.access.csv index 82eb4f7..46a72bd 100644 --- a/bemade_fsm/security/ir.model.access.csv +++ b/bemade_fsm/security/ir.model.access.csv @@ -3,4 +3,5 @@ access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_temp access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1 access_bemade_fsm_equipment,bemade_fsm_equipment,model_bemade_fsm_equipment,base.group_user,1,1,1,1 access_bemade_fsm_equipment_tag,bemade_fsm_equipment_tag,model_bemade_fsm_equipment_tag,base.group_user,1,1,1,1 -access_bemade_fsm_equipment_type,access_bemade_fsm_equipment_type,model_bemade_fsm_equipment_type,base.group_user,1,0,0,0 \ No newline at end of file +access_bemade_fsm_equipment_type,access_bemade_fsm_equipment_type,model_bemade_fsm_equipment_type,base.group_user,1,0,0,0 +access_bemade_fsm_visit,access_bemade_fsm_visit,model_bemade_fsm_visit,base.group_user,1,1,1,1 diff --git a/bemade_fsm/tests/__init__.py b/bemade_fsm/tests/__init__.py index 5e3c145..e838478 100644 --- a/bemade_fsm/tests/__init__.py +++ b/bemade_fsm/tests/__init__.py @@ -4,3 +4,4 @@ from . import test_sale_order from . import test_equipment from . import test_fsm_contact_setting from . import test_so_task_contacts +from . import test_fsm_visit diff --git a/bemade_fsm/tests/test_fsm_contact_setting.py b/bemade_fsm/tests/test_fsm_contact_setting.py index 81ceb1a..ce8a60e 100644 --- a/bemade_fsm/tests/test_fsm_contact_setting.py +++ b/bemade_fsm/tests/test_fsm_contact_setting.py @@ -1,29 +1,33 @@ -from odoo.tests import TransactionCase, HttpCase, tagged +from odoo.tests import TransactionCase, HttpCase, tagged, Form from odoo import Command from .test_bemade_fsm_common import FSMManagerUserTransactionCase +def create_base_contacts(cls): + Partner = cls.env['res.partner'] + cls.parent_co = Partner.create({ + 'name': 'Parent Co', + 'company_type': 'company', + }) + cls.contact_1 = Partner.create({ + 'name': 'Contact 1', + 'company_type': 'person', + 'parent_id': cls.parent_co.id, + }) + cls.contact_2 = Partner.create({ + 'name': 'Contact 2', + 'company_type': 'person', + 'parent_id': cls.parent_co.id, + }) + + @tagged("-at_install", "post_install") class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - Partner = cls.env['res.partner'] - cls.parent_co = Partner.create({ - 'name': 'Parent Co', - 'company_type': 'company', - }) - cls.contact_1 = Partner.create({ - 'name': 'Contact 1', - 'company_type': 'person', - 'parent_id': cls.parent_co.id, - }) - cls.contact_2 = Partner.create({ - 'name': 'Contact 2', - 'company_type': 'person', - 'parent_id': cls.parent_co.id, - }) + create_base_contacts(cls) def test_site_contacts(self): # Make sure the SO pulls the defaults from the partner correctly @@ -56,10 +60,11 @@ class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase): so.work_order_contacts != so.partner_id.work_order_contacts and len(so.partner_id.work_order_contacts) == 2) -class SaleOrderMultiLocationContactsTest(SaleOrderFSMContactsCase): +class SaleOrderMultiLocationContactsTest(FSMManagerUserTransactionCase): @classmethod def setUpClass(cls): super().setUpClass() + create_base_contacts(cls) cls.shipping_location = cls.env['res.partner'].create({ 'name': 'Shipping location', 'company_type': 'company', @@ -90,3 +95,18 @@ class SaleOrderMultiLocationContactsTest(SaleOrderFSMContactsCase): self.assertEqual(so.partner_shipping_id, self.shipping_location) self.assertEqual(so.site_contacts, self.shipping_location.site_contacts) self.assertEqual(so.work_order_contacts, self.shipping_location.work_order_contacts) + + def test_onchange_shipping_address(self): + so = self.env['sale.order'].create({ + 'partner_id': self.parent_co.id, + 'partner_shipping_id': self.parent_co.id, + }) + self.assertFalse(so.work_order_contacts) + self.assertFalse(so.site_contacts) + + form = Form(so) + form.partner_shipping_id = self.shipping_location + form.save() + + self.assertEqual(so.work_order_contacts, self.shipping_location.work_order_contacts) + self.assertEqual(so.site_contacts, self.shipping_location.site_contacts) diff --git a/bemade_fsm/tests/test_fsm_visit.py b/bemade_fsm/tests/test_fsm_visit.py new file mode 100644 index 0000000..9928f4e --- /dev/null +++ b/bemade_fsm/tests/test_fsm_visit.py @@ -0,0 +1,91 @@ +from odoo.tests import TransactionCase, tagged, Form + + +@tagged('-at_install', 'post_install') +class FSMVisitTest(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_create_visit_sets_name_on_section(self): + so = self.generate_sale_order() + self.add_service_so_line(so) + + visit = self.generate_visit(so) + + self.assertTrue(visit.so_section_id) + self.assertEqual(visit.so_section_id.name, visit.label) + + def test_change_visit_section_name(self): + so = self.generate_sale_order() + visit = self.generate_visit(so, label="First Label") + line = visit.so_section_id + + line.name = "Second Label" + + self.assertEqual(visit.label, "Second Label") + + def test_change_visit_label_changes_section_name(self): + so = self.generate_sale_order() + visit = self.generate_visit(so, label="First Label") + line = visit.so_section_id + + visit.label = "Second Label" + + self.assertEqual(line.name, "Second Label") + + def test_visit_completes_when_task_completes(self): + so = self.generate_sale_order() + visit = self.generate_visit(so) + sol = self.add_service_so_line(so, task=True) + so.action_confirm() + task = so.order_line.filtered(lambda l: l.task_id).task_id + + task.action_fsm_validate() + + self.assertTrue(visit.is_completed) + + def generate_visit(self, sale_order, label="Test Label"): + return self.env['bemade_fsm.visit'].create([{ + 'sale_order_id': sale_order.id, + 'label': label, + }]) + + def generate_sale_order(self, partner_id=None): + so = self.env['sale.order'].create({ + 'partner_id': partner_id or self.generate_partner().id, + 'client_order_ref': 'Test', + }) + return so + + 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 + """ + if not task: + service_tracking = 'no' + project = False + else: + project = self.env.ref("industry_fsm.fsm_project") + service_tracking = 'task_global_project' + product = self.env['product.product'].create({ + 'name': 'Test Product', + 'type': 'service', + 'service_tracking': service_tracking, + 'project_id': project and project.id, + 'price': 100.0, + 'sale_ok': True, + }) + return self.env['sale.order.line'].create({ + 'product_id': product.id, + 'order_id': sale_order.id, + }) + + def generate_partner(self): + partner = self.env['res.partner'].create({ + 'name': 'Test Partner', + 'company_type': 'company', + }) + return partner diff --git a/bemade_fsm/views/sale_order_views.xml b/bemade_fsm/views/sale_order_views.xml index d0b01fb..0421d41 100644 --- a/bemade_fsm/views/sale_order_views.xml +++ b/bemade_fsm/views/sale_order_views.xml @@ -8,7 +8,11 @@ - + + + + @@ -21,5 +25,17 @@ + + bemade_fsm.visit.tree + bemade_fsm.visit + + + + + + + + + \ No newline at end of file