diff --git a/bemade_fsm/models/equipment.py b/bemade_fsm/models/equipment.py
index 2cbb096..d26b966 100644
--- a/bemade_fsm/models/equipment.py
+++ b/bemade_fsm/models/equipment.py
@@ -41,7 +41,7 @@ class Equipment(models.Model):
partner_id = fields.Many2one('res.partner',
string="Owner",
compute="_compute_partner",
- search="_search_partner",)
+ search="_search_partner", )
description = fields.Text(string="Description",
tracking=True)
@@ -53,9 +53,11 @@ class Equipment(models.Model):
location_notes = fields.Text(string="Physical Location Notes",
tracking=True)
- task_ids = fields.One2many(comodel_name='project.task',
- inverse_name='equipment_id',
- string='Interventions')
+ task_ids = fields.Many2many(comodel_name='project.task',
+ relation="bemade_fsm_task_equipment_rel",
+ column1="equipment_id",
+ column2="task_id",
+ string='Interventions')
@api.depends('partner_location_id')
def _compute_partner(self):
diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py
index df88a4d..7b8b988 100644
--- a/bemade_fsm/models/sale_order.py
+++ b/bemade_fsm/models/sale_order.py
@@ -114,7 +114,7 @@ class SaleOrderLine(models.Model):
vals['user_ids'] = template.assignees.ids
vals['tag_ids'] = template.tags.ids
vals['planned_hours'] = template.planned_hours
- vals['equipment_id'] = template.equipment_id.id
+ vals['equipment_ids'] = [Command.set([template.equipment_id.id])]
return vals
tmpl = self.product_id.task_template_id
@@ -128,8 +128,8 @@ class SaleOrderLine(models.Model):
"This task has been created from: %s (%s)") % (
self.order_id.id, self.order_id.name, self.product_id.name)
task.message_post(body=task_msg)
- if not task.equipment_id:
- task.equipment_id = self.order_id.equipment_id
+ if not task.equipment_ids:
+ task.write({'equipment_ids': [Command.set(self.order_id.equipment_id)]})
task.name = _generate_task_name(tmpl)
return task
diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py
index ce5c99d..f4da20f 100644
--- a/bemade_fsm/models/task.py
+++ b/bemade_fsm/models/task.py
@@ -5,7 +5,12 @@ from odoo.exceptions import ValidationError
class Task(models.Model):
_inherit = "project.task"
- equipment_id = fields.Many2one("bemade_fsm.equipment", string="Equipment to Service", tracking=True)
+ equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
+ relation="bemade_fsm_task_equipment_rel",
+ column1="task_id",
+ column2="equipment_id",
+ string="Equipment to Service",
+ tracking=True,)
work_order_contacts = fields.Many2many(comodel_name="res.partner",
relation="task_work_order_contact_rel",
diff --git a/bemade_fsm/tests/test_bemade_fsm_common.py b/bemade_fsm/tests/test_bemade_fsm_common.py
index 313dcf9..94de29c 100644
--- a/bemade_fsm/tests/test_bemade_fsm_common.py
+++ b/bemade_fsm/tests/test_bemade_fsm_common.py
@@ -3,11 +3,17 @@ from odoo import Command
@tagged("-at_install", "post_install")
-class FSMManagerUserTransactionCase(TransactionCase):
+class BemadeFSMBaseTest(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
+ cls.user = cls._generate_project_manager_user('Project Manager', 'misterpm')
+ cls.user_limited = cls._generate_project_user('Project User', 'mruser')
+ cls.env.ref("industry_fsm.fsm_project").write({'allow_subtasks': True})
+
+ @classmethod
+ def _generate_project_manager_user(cls, name, login):
user_group_employee = cls.env.ref('base.group_user')
user_group_project_user = cls.env.ref('project.group_project_user')
user_group_project_manager = cls.env.ref('project.group_project_manager')
@@ -26,24 +32,117 @@ class FSMManagerUserTransactionCase(TransactionCase):
user_group_sales_manager.id, ]
if user_product_customer:
group_ids.append(user_product_customer.id)
-
- # Test user with project access rights for the various tests
- Users = cls.env['res.users'].with_context({'no_reset_password': True})
- cls.user = Users.create({
- 'name': 'Project Manager',
- 'login': 'misterpm',
- 'password': 'misterpm',
- 'email': 'mrpm@testco.com',
- 'signature': 'Mr. PM',
- 'groups_id': [Command.set(group_ids)],
- })
- group_ids.remove(user_group_fsm_manager.id)
- group_ids.remove(user_group_project_manager.id)
- cls.user_limited = Users.create({
- 'name': 'Project User',
- 'login': 'mruser',
- 'password': 'mruser',
- 'email': 'mruser@testco.com',
- 'signature': 'Mr. User',
+ return cls.env['res.users'].with_context({'no_reset_password': True}).create({
+ 'name': name,
+ 'login': login,
+ 'password': login,
+ 'email': f"{login}@test.co",
'groups_id': [Command.set(group_ids)]
})
+
+ @classmethod
+ def _generate_project_user(cls, name, login):
+ user = cls._generate_project_manager_user(name, login)
+ user.write({'groups_id': [Command.unlink(cls.env.ref('project.group_project_manager'))]})
+ user.write({'groups_id': [Command.unlink(cls.env.ref('industry_fsm.group_fsm_manager'))]})
+
+ @classmethod
+ def _generate_partner(cls, name: str = 'Test Company', company_type: str = 'company', parent=None):
+ """ Generates a partner with basic address filled in.
+
+ :param name: The partner's name.
+ :param company_type: The type of partner, either 'company' or 'person' are accepted."""
+ return cls.env['res.partner'].create({
+ 'name': name,
+ 'company_type': company_type,
+ 'street': '123 Street St.',
+ 'city': 'Montreal',
+ 'state_id': cls.env.ref('base.state_ca_qc').id,
+ 'country_id': cls.env.ref('base.ca').id,
+ 'parent_id': parent and parent.id,
+ })
+
+ @classmethod
+ def _generate_sale_order(cls, partner, client_order_ref='Test Order', equipment=None):
+ return cls.env['sale.order'].create({
+ 'partner_id': partner.id,
+ 'client_order_ref': client_order_ref,
+ 'equipment_id': equipment.id,
+ })
+
+ @classmethod
+ def _generate_sale_order_line(cls, sale_order, product, project, qty=1.0, uom=None, price=100.0, tax_id=False):
+ return cls.env['sale.order.line'].create({
+ 'order_id': sale_order.id,
+ 'product_id': product.id,
+ 'product_uom_qty': qty,
+ 'product_uom': uom or cls.env.ref("uom.product_uom_hour"),
+ 'price_unit': price,
+ 'tax_id': tax_id,
+ })
+
+ @classmethod
+ def _generate_equipment(cls, name='test equipment', partner=None):
+ return cls.env['bemade_fsm.equipment'].create({
+ 'name': name,
+ 'partner_location_id': partner and partner.id,
+ })
+
+ @classmethod
+ def _generate_product(cls, name='Test Product', product_type='service', service_tracking='task_global_project',
+ project=None, task_template=None, service_policy='delivered_manual', uom=None):
+ if 'project' in service_tracking and not project:
+ project = cls.env.ref("industry_fsm.fsm_project")
+ uom_id = uom and uom.id or cls.env.ref("uom.product_uom_hour").id
+ return cls.env['product.product'].create({
+ 'name': name,
+ 'type': product_type,
+ 'service_tracking': service_tracking,
+ 'project_id': service_tracking in ('task_global_project', 'project_only') and project.id,
+ 'project_template_id': service_tracking == 'task_in_project' and project.id,
+ 'task_template_id': task_template and task_template.id,
+ 'service_policy': service_policy,
+ 'uom_id': uom_id,
+ 'uom_po_id': uom_id,
+ })
+
+ @classmethod
+ def _generate_fsm_project(cls, name='Test Project'):
+ return cls.env['project.project'].create({
+ 'name': name,
+ 'allow_material': True,
+ 'allow_timesheets': True,
+ 'allow_subtasks': True,
+ 'allow_quotations': True,
+ 'allow_worksheets': True,
+ 'is_fsm': True,
+ })
+
+ @classmethod
+ def _generate_task_template(cls, parent=None, structure=[], names=['Test Task Template']):
+ """ Generates a task template with the specified structure and naming.
+
+ :param structure: A list of integers describing the number of tasks for each level of descendants. An empty
+ list represents only one top-level task template.
+ :param names: The name prefixes to be given to the task templates at each level. Each prefix will be followed
+ by a sequential integer for its level. Child 1, Child 2, Grandchild 1, etc."""
+ if len(structure) != len(names) - 1:
+ raise ValueError("The length of the structure argument must contain exactly one element less than the "
+ "names argument.")
+ name = names.pop(0)
+ template = cls.env['project.task.template'].create({
+ 'name': name,
+ 'parent': parent and parent.id,
+ })
+ parent = template
+ while structure:
+ subtasks = []
+ for i in range(0, structure[0]):
+ subtasks.append(cls.env['project.task.template'].create({
+ 'parent': parent.id,
+ 'name': names[0] + f" {i}",
+ }))
+ structure.pop(0)
+ names.pop(0)
+ parent = subtasks[0]
+ return template
diff --git a/bemade_fsm/tests/test_equipment.py b/bemade_fsm/tests/test_equipment.py
index 8a38c49..65fba3c 100644
--- a/bemade_fsm/tests/test_equipment.py
+++ b/bemade_fsm/tests/test_equipment.py
@@ -1,53 +1,34 @@
from odoo.tests.common import HttpCase, tagged
-from .test_bemade_fsm_common import FSMManagerUserTransactionCase
+from .test_bemade_fsm_common import BemadeFSMBaseTest
from odoo import Command
from odoo.exceptions import MissingError
@tagged("-at_install", "post_install")
-class TestEquipmentCommon(FSMManagerUserTransactionCase):
+class TestEquipment(BemadeFSMBaseTest):
+ def test_crud(self):
+ self.partner_company = self._generate_partner()
+ self.partner_contact = self._generate_partner('Site Contact', 'person', self.partner_company)
+ self.equipment = self._generate_equipment('Test Equipment 1', self.partner_company)
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
-
- # Set up the test partner
- cls.partner_company = cls.env['res.partner'].create({
- 'name': 'Test Partner Company',
- 'company_type': 'company',
- 'street': '123 Street St.',
- 'city': 'Montreal',
- 'state_id': cls.env['res.country.state'].search([('name', 'ilike', 'Quebec%')]).id,
- 'country_id': cls.env['res.country'].search([('name', '=', 'Canada')]).id
- })
-
- cls.partner_contact = cls.env['res.partner'].create({
- 'name': 'Site Contact',
- 'company_type': 'person',
- 'parent_id': cls.partner_company.id,
- })
-
- cls.equipment = cls.env['bemade_fsm.equipment'].create({
- 'name': 'Test Equipment 1',
- 'partner_location_id': cls.partner_company.id,
- })
-
-
-@tagged('-at_install', 'post_install')
-class TestEquipmentBase(TestEquipmentCommon):
-
- def test_crd(self):
# Just make sure the basic ORM stuff is OK
self.assertTrue(self.equipment in self.partner_company.equipment_ids)
self.assertTrue(len(self.partner_company.equipment_ids) == 1)
- self.partner_company.write({'equipment_ids': [Command.set([])]})
+
# Delete should cascade
+ self.partner_company.write({'equipment_ids': [Command.set([])]})
with self.assertRaises(MissingError):
self.equipment.name
@tagged('-at_install', 'post_install', 'slow')
-class TestEquipmentTours(HttpCase, TestEquipmentCommon):
+class TestEquipmentTours(HttpCase, BemadeFSMBaseTest):
+
+ @classmethod
+ def setUpClass(cls):
+ cls._generate_partner()
+ cls._generate_partner('Site Contact', 'person', cls.partner_company)
+ cls._generate_equipment('Test Equipment 1', cls.partner_company)
def test_equipment_base_tour(self):
self.start_tour('/web', 'equipment_base_tour',
diff --git a/bemade_fsm/tests/test_fsm_contact_setting.py b/bemade_fsm/tests/test_fsm_contact_setting.py
index ce8a60e..0c097ab 100644
--- a/bemade_fsm/tests/test_fsm_contact_setting.py
+++ b/bemade_fsm/tests/test_fsm_contact_setting.py
@@ -1,6 +1,6 @@
from odoo.tests import TransactionCase, HttpCase, tagged, Form
from odoo import Command
-from .test_bemade_fsm_common import FSMManagerUserTransactionCase
+from .test_bemade_fsm_common import BemadeFSMBaseTest
def create_base_contacts(cls):
@@ -22,7 +22,7 @@ def create_base_contacts(cls):
@tagged("-at_install", "post_install")
-class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase):
+class SaleOrderFSMContactsCase(BemadeFSMBaseTest):
@classmethod
def setUpClass(cls):
@@ -60,7 +60,7 @@ class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase):
so.work_order_contacts != so.partner_id.work_order_contacts and len(so.partner_id.work_order_contacts) == 2)
-class SaleOrderMultiLocationContactsTest(FSMManagerUserTransactionCase):
+class SaleOrderMultiLocationContactsTest(BemadeFSMBaseTest):
@classmethod
def setUpClass(cls):
super().setUpClass()
diff --git a/bemade_fsm/tests/test_sale_order.py b/bemade_fsm/tests/test_sale_order.py
index 7cd7398..ae558f7 100644
--- a/bemade_fsm/tests/test_sale_order.py
+++ b/bemade_fsm/tests/test_sale_order.py
@@ -10,20 +10,12 @@ class TestSalesOrder(TestTaskTemplateCommon):
cls.partner = cls.env['res.partner'].create({
'name': 'Test Partner',
})
- cls.equipment =cls.env['bemade_fsm.equipment'].create({
- 'name': 'test equipment',
- 'partner_location_id': cls.partner.id,
- })
- cls.so_equipment = cls.env['bemade_fsm.equipment'].create({
- 'name': 'test equipment 2',
- 'partner_location_id': cls.partner.id,
- })
- cls.sale_order1 = cls.env['sale.order'].create({
- 'partner_id': cls.partner.id,
- 'client_order_ref': 'TEST ORDER 1',
- 'state': 'draft',
- 'equipment_id': cls.so_equipment.id,
- })
+ cls.equipment = TestSalesOrder._generate_equipment()
+ cls.so_equipment = TestSalesOrder._generate_equipment('test equipment 2')
+
+ cls.sale_order1 = TestSalesOrder._generate_equipment(partner=cls.partner, client_order_ref='TEST ORDER 1',
+ equipment=cls.so_equipment)
+ cls.sol_serv_order = TestSalesOrder._generate_sale_order_line(product, project, )
cls.sol_serv_order = cls.env['sale.order.line'].create({
'name': cls.product_task_global_project.name,
'product_id': cls.product_task_global_project.id,
@@ -66,6 +58,7 @@ class TestSalesOrder(TestTaskTemplateCommon):
'tax_id': False,
})
+
@tagged('-at_install', 'post_install')
def test_order_confirmation_simple_template(self):
""" Confirming the order should create a task in the global project. """
@@ -144,4 +137,4 @@ class TestSaleOrderTour(HttpCase, TestSalesOrder):
so.action_confirm()
sol = so.order_line.filtered(lambda l: 'Test Product 3' in l.name)
self.start_tour('/web', 'sale_order_tour', login='misterpm')
- self.assertTrue(sol.qty_delivered != 0)
\ No newline at end of file
+ self.assertTrue(sol.qty_delivered != 0)
diff --git a/bemade_fsm/tests/test_task_template.py b/bemade_fsm/tests/test_task_template.py
index 97fd66a..3d20375 100644
--- a/bemade_fsm/tests/test_task_template.py
+++ b/bemade_fsm/tests/test_task_template.py
@@ -1,4 +1,4 @@
-from .test_bemade_fsm_common import FSMManagerUserTransactionCase
+from .test_bemade_fsm_common import BemadeFSMBaseTest
from odoo.tests.common import HttpCase, tagged
from odoo.exceptions import MissingError
from odoo import Command
@@ -6,7 +6,7 @@ from psycopg2.errors import ForeignKeyViolation
@tagged("-at_install", "post_install")
-class TestTaskTemplateCommon(FSMManagerUserTransactionCase):
+class TestTaskTemplateCommon(BemadeFSMBaseTest):
@classmethod
def setUpClass(cls):
@@ -19,79 +19,26 @@ class TestTaskTemplateCommon(FSMManagerUserTransactionCase):
})
cls.project = cls.env.ref('industry_fsm.fsm_project')
- cls.project.write({'allow_subtasks': True,})
- cls.product_task_global_project = cls.env['product.product'].create({
- 'name': 'Test Product 1',
- 'type': 'service',
- 'service_tracking': 'task_global_project',
- 'project_id': cls.project.id,
- 'task_template_id': cls.task1.id,
- 'uom_id': hours_uom.id,
- 'uom_po_id': hours_uom.id,
- 'service_policy': 'delivered_manual',
- })
- cls.project_template = cls.env['project.project'].create({
- 'name': 'Test Project Template',
- 'allow_material': True,
- 'allow_timesheets': True,
- 'allow_subtasks': True,
- 'allow_quotations': True,
- 'allow_worksheets': True,
- 'is_fsm': True,
- })
- cls.product_task_in_project = cls.env['product.product'].create({
- 'name': 'Test Product 2',
- 'type': 'service',
- 'service_tracking': 'task_in_project',
- 'task_template_id': cls.task1.id,
- 'project_template_id': cls.project_template.id,
- 'uom_po_id': hours_uom.id,
- 'uom_id': hours_uom.id,
- 'service_policy': 'delivered_manual',
- })
+ cls.product_task_global_project = cls._generate_product(name='Test Product 1', task_template=cls.task1)
+ cls.project_template = cls._generate_fsm_project('Test Project Template')
+ cls.product_task_in_project = cls._generate_product(name='Test Product 2', project=cls.project_template,
+ service_tracking='task_in_project', task_template=cls.task1)
# Set up a task template tree with 2 children and 1 grandchild
- cls.parent_task = cls.env['project.task.template'].create({
- 'name': 'Parent Template',
- 'planned_hours': cls.PLANNED_HOURS,
- })
- cls.child_task_1 = cls.env['project.task.template'].create({
- 'name': 'Child Template 1',
- 'parent': cls.parent_task.id,
- })
- cls.child_task_2 = cls.env['project.task.template'].create({
- 'name': 'Child Template 2',
- 'parent': cls.parent_task.id,
- })
- cls.parent_task.write({'subtasks': [Command.set([cls.child_task_1.id, cls.child_task_2.id])]})
- cls.grandchild_task = cls.env['project.task.template'].create({
- 'name': 'Grandchild Template',
- 'parent': cls.child_task_2.id
- })
- cls.child_task_2.write({'subtasks': [Command.set([cls.grandchild_task.id])]})
+ cls.parent_task = cls._generate_task_template(structure=[2, 1],
+ names=['Parent Template', 'Child Template',
+ 'Grandchild Template'])
+ cls.child_task_1 = cls.parent_task.subtasks[0]
+ cls.child_task_2 = cls.parent_task.subtasks[1]
+ cls.grandchild_task = cls.child_task_1.subtasks[0]
# Create products using the task tree we just created
- cls.product_task_tree_global_project = cls.env['product.product'].create({
- 'name': 'Test Product 3',
- 'type': 'service',
- 'service_tracking': 'task_global_project',
- 'project_id': cls.project.id,
- 'task_template_id': cls.parent_task.id,
- 'service_policy': 'delivered_manual',
- 'uom_id': hours_uom.id,
- 'uom_po_id': hours_uom.id,
- })
- cls.product_task_tree_in_project = cls.env['product.product'].create({
- 'name': 'Test Product 2',
- 'type': 'service',
- 'service_tracking': 'task_in_project',
- 'task_template_id': cls.parent_task.id,
- 'project_template_id': cls.project_template.id,
- 'uom_po_id': hours_uom.id,
- 'uom_id': hours_uom.id,
- 'service_policy': 'delivered_manual',
- })
+ cls.product_task_tree_global_project = cls._generate_product(name='Test Product 3',
+ task_template=cls.parent_task)
+ cls.product_task_tree_in_project = cls._generate_product(name="Test Product 2", project=cls.project_template,
+ service_tracking='task_in_project',
+ task_template=cls.parent_task)
@tagged('-at_install', 'post_install')
@@ -105,7 +52,7 @@ class TestTaskTemplate(TestTaskTemplateCommon):
def test_delete_subtask_template(self):
""" Deletion of a child task should be OK even if the parent is on a product. Children of the deleted
subtask should be deleted."""
- self.child_task_2.unlink()
+ self.child_task_1.unlink()
# Reading deleted child's name field should be impossible
with self.assertRaises(MissingError):
test = self.grandchild_task.name
diff --git a/bemade_fsm/views/equipment.xml b/bemade_fsm/views/equipment.xml
index 4047df6..869181d 100644
--- a/bemade_fsm/views/equipment.xml
+++ b/bemade_fsm/views/equipment.xml
@@ -50,7 +50,7 @@
bemade_fsm.equipment.tree
bemade_fsm.equipment
-
+
diff --git a/bemade_fsm/views/task_views.xml b/bemade_fsm/views/task_views.xml
index 75af60a..e63fedb 100644
--- a/bemade_fsm/views/task_views.xml
+++ b/bemade_fsm/views/task_views.xml
@@ -8,7 +8,9 @@
-
+