diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..3928b9d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[ENH]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. Ex. As a [type of user], I could click [...] and [...] would happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/bemade_fsm/__init__.py b/bemade_fsm/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/bemade_fsm/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py new file mode 100644 index 0000000..0a0e459 --- /dev/null +++ b/bemade_fsm/__manifest__.py @@ -0,0 +1,55 @@ +###################################################################################### +# +# Bemade Inc. +# +# Copyright (C) 2023-June Bemade Inc. (). +# Author: Marc Durepos (Contact : mdurepos@durpro.com) +# +# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1) +# It is forbidden to publish, distribute, sublicense, or sell copies of the Software +# or modified copies of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +######################################################################################## +{ + 'name': 'Durpro Field Service Management', + 'version': '15.0.1.0.0', + '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', + 'author': 'Bemade Inc.', + 'website': 'http://www.bemade.org', + 'license': 'OPL-1', + 'depends': ['project', + 'stock', + 'sale', + 'sale_project', + 'sale_stock', + 'industry_fsm', + 'industry_fsm_sale', + 'bemade_partner_root_ancestor', + ], + 'data': ['views/task_template_views.xml', + 'views/equipment.xml', + 'security/ir.model.access.csv', + 'views/product_views.xml', + 'views/res_partner.xml', + 'views/menus.xml', + 'views/task_views.xml', + ], + 'assets': { + 'web.assets_tests': [ + 'bemade_fsm/static/tests/tours/task_template_tour.js', + 'bemade_fsm/static/tests/tours/task_equipment_tour.js', + ], + }, + 'installable': True, + 'auto_install': False +} diff --git a/bemade_fsm/models/__init__.py b/bemade_fsm/models/__init__.py new file mode 100644 index 0000000..997de6e --- /dev/null +++ b/bemade_fsm/models/__init__.py @@ -0,0 +1,6 @@ +from . import task_template +from . import product_template +from . import sale_order +from . import equipment +from . import task +from . import res_partner diff --git a/bemade_fsm/models/equipment.py b/bemade_fsm/models/equipment.py new file mode 100644 index 0000000..b82c143 --- /dev/null +++ b/bemade_fsm/models/equipment.py @@ -0,0 +1,90 @@ +from odoo import api, fields, models + + +class EquipmentTag(models.Model): + _name = "bemade_fsm.equipment.tag" + _description = 'Field service equipment category' + + name = fields.Char('Name', required=True, translate=True) + color = fields.Integer('Color Index', default=10) + + _sql_constraints = [ + ('name_uniq', 'unique (name)', "Tag name already exists !"), + ] + + +class EquipmentType(models.Model): + _name = 'bemade_fsm.equipment.type' + _description = 'Field service equipment type' + _order = 'id' + + name = fields.Char(string='Intervention Name', required=True, translate=True) + +class Equipment(models.Model): + _name = 'bemade_fsm.equipment' + _rec_name = 'complete_name' + _description = 'Field service equipment' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + pid_tag = fields.Char(string="P&ID Tag", tracking=True) + + name = fields.Char(string="Name", tracking=True, required=True) + + complete_name = fields.Char(string="Equipment Name", compute="_compute_complete_name", store=True) + + tag_ids = fields.Many2many('bemade_fsm.equipment.tag', + string='Application', + tracking=True, + help="Classify and analyze your equipment categories like: Boiler, Laboratory, " + "Waste water, Pure water") + partner_id = fields.Many2one('res.partner', + string="Owner", + compute="_compute_partner") + description = fields.Text(string="Description", + tracking=True) + + partner_location_id = fields.Many2one('res.partner', + string="Physical Address", + tracking=True,) + + location_notes = fields.Text(string="Physical Location Notes", + tracking=True) + task_ids = fields.One2many(comodel_name='project.task', + inverse_name='equipment_id', + string='Interventions') + + @api.depends('partner_location_id') + def _compute_partner(self): + for rec in self: + rec.partner_id = rec.partner_location_id and rec.partner_location_id.root_ancestor + + @api.depends('pid_tag', 'name') + def _compute_complete_name(self): + for rec in self: + rec.complete_name = "[%s] %s" % (rec.pid_tag or ' ', rec.name) + + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + args = args or [] + if name: + equipments = self.search([ + '|', '|', '|', + ('pid_tag', operator, name), + ('name', operator, name), + ('partner_id.name', operator, name), + ('partner_location_id.name', operator, name)], + limit=limit) + else: + equipments = self.search(args, limit=limit) + return equipments.name_get() + + def action_view_equipment(self): + return { + 'name': 'Equipment', + 'view_type': 'form', + 'view_mode': 'form', + 'res_id': self.id, + 'context': self.env.context, + 'res_model': 'bemade_fsm.equipment', + 'type': 'ir.actions.act_window', + } \ No newline at end of file diff --git a/bemade_fsm/models/product_template.py b/bemade_fsm/models/product_template.py new file mode 100644 index 0000000..c7ecb0a --- /dev/null +++ b/bemade_fsm/models/product_template.py @@ -0,0 +1,7 @@ +from odoo import fields, models, api + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + task_template_id = fields.Many2one("project.task.template", string="Task Template", ondelete='restrict') diff --git a/bemade_fsm/models/res_partner.py b/bemade_fsm/models/res_partner.py new file mode 100644 index 0000000..c9f1877 --- /dev/null +++ b/bemade_fsm/models/res_partner.py @@ -0,0 +1,45 @@ +from odoo import api, fields, models, Command + +class Partner(models.Model): + _inherit = 'res.partner' + + equipment_count = fields.Integer(compute='_compute_equipment_count', + string='Equipment Count') + + owned_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment", + inverse_name="partner_id", + string="Owned Equipments") + + + equipment_ids = fields.One2many(comodel_name='bemade_fsm.equipment', + inverse_name='partner_location_id', + string='Site Equipment') + + is_site_contact = fields.Boolean(string='Is a site contact', + compute="_compute_is_site_contact") + + site_ids = fields.Many2many(string='Work Sites', + comodel_name='res.partner', + relation='res_partner_site_contact_rel', + column1='site_contact_id', + column2='site_id', + tracking=True) + + site_contacts = fields.Many2many(string='Site Contacts', + comodel_name='res.partner', + relation='res_partner_site_contact_rel', + column1='site_id', + column2='site_contact_id', + domain=[('is_company', '=', False)], + tracking=True) + + @api.depends('site_ids') + def _compute_is_site_contact(self): + for rec in self: + rec.is_site_contact = rec.site_ids is not False + + @api.depends('equipment_ids') + def _compute_equipment_count(self): + for rec in self: + all_equipmemt_ids = self.env['bemade_fsm.equipment'].search([('partner_id', '=', rec.id)]) + rec.equipment_count = len(all_equipmemt_ids) diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py new file mode 100644 index 0000000..0605e28 --- /dev/null +++ b/bemade_fsm/models/sale_order.py @@ -0,0 +1,61 @@ +from odoo import fields, models, api, _, Command + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + 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 + :return task: record of the created task + + Override to add the logic needed to implement task templates.""" + + def _create_task_from_template(project, template, parent): + """ Recursively generates the task and any subtasks from a project.task.template. + + :param project: project.project record to set on the task's project_id field. + :param template: project.task.template to use to create the task. + :param parent: project.task to set as the parent to this task. + """ + values = _timesheet_create_task_prepare_values_from_template(project, template, parent) + task = self.env['project.task'].sudo().create(values) + subtasks = [] + for t in template.subtasks: + subtask = _create_task_from_template(project, t, task) + subtasks.append(subtask) + task.write({'child_ids': [Command.set([t.id for t in subtasks])]}) + # We don't want to see the sub-tasks on the SO + task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, }) + return task + + def _timesheet_create_task_prepare_values_from_template(project, template, parent): + """ Copies the values from a project.task.template over to the set of values used to create a project.task. + + :param project: project.project record to set on the task's project_id field. + Pass the project.project model or an empty recordset to leave task project_id blank. + DO NOT pass False or None as this will cause an error in _timesheet_create_task_prepare_values(project). + :param template: project.task.template to use to create the task. + :param parent: project.task to set as the parent to this task. + """ + vals = self._timesheet_create_task_prepare_values(project) + vals['name'] = f"{vals['name']} ({template.name})" if not parent else template.name + vals['description'] = template.description or vals['description'] + vals['parent_id'] = parent and parent.id + vals['user_ids'] = template.assignees.ids + vals['tag_ids'] = template.tags.ids + vals['planned_hours'] = template.planned_hours + return vals + + tmpl = self.product_id.task_template_id + if not tmpl: + task = super()._timesheet_create_task(project) + else: + task = _create_task_from_template(project, tmpl, None) + self.write({'task_id': task.id}) + # post message on task + task_msg = _( + "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) + return task diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py new file mode 100644 index 0000000..7ee4a71 --- /dev/null +++ b/bemade_fsm/models/task.py @@ -0,0 +1,7 @@ +from odoo import fields, models, api + + +class Task(models.Model): + _inherit = "project.task" + + equipment_id = fields.Many2one("bemade_fsm.equipment", string="Target Equipment") diff --git a/bemade_fsm/models/task_template.py b/bemade_fsm/models/task_template.py new file mode 100644 index 0000000..f95b3c7 --- /dev/null +++ b/bemade_fsm/models/task_template.py @@ -0,0 +1,32 @@ +from odoo import models, fields, api, _ + + +class TaskTemplate(models.Model): + _name = 'project.task.template' + + @api.model + def _current_company(self): + return self.env.company + + name = fields.Char(string="Task Title", required=True) + description = fields.Html(string="Description") + assignees = fields.Many2many("res.users", string="Default Assignees", help="Employees assigned to tasks created from this template.") + customer = fields.Many2one("res.partner", string="Default Customer", help="Default customer for tasks created from this template.") + project = fields.Many2one("project.project", string="Default Project", help="Default project for tasks created from this template.") + tags = fields.Many2many("project.tags", string="Default Tags", help="Default tags for tasks created from this template.") + parent = fields.Many2one("project.task.template", string="Parent Task Template", ondelete='cascade') + subtasks = fields.One2many("project.task.template", inverse_name="parent", string="Subtask Templates") + sequence = fields.Integer(string="Sequence") + company_id = fields.Many2one("res.company", string="Company", index=1, default=_current_company) + planned_hours = fields.Float("Initially Planned Hours") + + def action_open_task(self): + return { + 'view_mode': 'form', + 'res_model': 'project.task.template', + 'res_id': self.id, + 'type': 'ir.actions.act_window', + 'context': self._context + } + + diff --git a/bemade_fsm/security/ir.model.access.csv b/bemade_fsm/security/ir.model.access.csv new file mode 100644 index 0000000..82eb4f7 --- /dev/null +++ b/bemade_fsm/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_template,project.group_project_manager,1,1,1,1 +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 diff --git a/bemade_fsm/static/tests/tours/task_equipment_tour.js b/bemade_fsm/static/tests/tours/task_equipment_tour.js new file mode 100644 index 0000000..4599fb5 --- /dev/null +++ b/bemade_fsm/static/tests/tours/task_equipment_tour.js @@ -0,0 +1,75 @@ +/** @odoo-module **/ + +import tour from 'web_tour.tour'; +const TEST_COMPANY = "Test Partner Company"; +const TEST_EQPT1 = "Test Equipment 1"; +const TEST_EQPT2 = "Test Equipment 2"; +tour.register('task_equipment_tour', { + test: true, + url: '/web', + }, /* Create a new "Test Equipment" and link it to "Test Partner Company" */ + [tour.stepUtils.showAppsMenuItem(), { + content: 'Navigate to the Service menu', + trigger: '.o_app[data-menu-xmlid="industry_fsm.fsm_menu_root"]', + }, { + content: 'Navigate to the Clients submenu', + trigger: 'button.dropdown-toggle[data-menu-xmlid="bemade_fsm.menu_service_client"]', + }, { + content: 'Navigate to the Equipment menu', + trigger: '.dropdown-item[data-menu-xmlid="bemade_fsm.menu_service_client_equipment"]', + }, { + content: 'Click the create button', + trigger: '.o_list_button_add', + extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Equipment))', + }, { + content: 'Add a tag', + trigger: 'input[name="pid_tag"]', + run: 'text TestPIDTag', + }, { + content: 'Set the name', + trigger: 'input[name="name"]', + run: `text ${TEST_EQPT2}`, + }, { + content: 'Set the partner', + trigger: 'div[name="partner_location_id"] div div input', + run: 'text Test Partner Company', + }, { + content: 'Click the partner in the dropdown', + trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`, + }, { + content: 'Save equipment', + trigger: 'button.o_form_button_save', + }, { + /* Navigate to the client and make sure that there are two equipments saved (one from the Python test case) */ + content: 'Navigate to the Clients submenu', + trigger: 'button.dropdown-toggle[data-menu-xmlid="bemade_fsm.menu_service_client"]', + }, { + content: 'Click on the clients submenu', + trigger: '.dropdown-item[data-menu-xmlid="bemade_fsm.menu_service_client_clients"]', + }, { + content: 'Search for the Test Partner Company', + trigger: 'input.o_searchview_input', + extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Customers))', + run: `text ${TEST_COMPANY}` + }, { + content: 'Validate Search', + trigger: '.o_menu_item.o_selection_focus', + run: 'click', + }, { + content: 'Open the test client.', + trigger: `div.o_kanban_record:has(span:contains(${TEST_COMPANY}))`, + }, { + content: 'Click the Field Service tab.', + trigger: 'a.nav-link:contains(Field Service)', + extra_trigger: `h1 span.o_field_partner_autocomplete[name="name"]:contains(${TEST_COMPANY})`, + }, { + content: 'Make sure we have a first test equipment', + /*trigger: `div[name="equipment_ids"]:has(td:contains(${TEST_EQPT1}))`,*/ + trigger: `td:contains(${TEST_EQPT1})`, + run: function() {}, + }, { + content: 'Make sure we have a second test equipment', + trigger: `div[name="equipment_ids"]:has(td:contains(${TEST_EQPT2}))`, + run: function() {}, + } + ]) \ No newline at end of file diff --git a/bemade_fsm/static/tests/tours/task_template_tour.js b/bemade_fsm/static/tests/tours/task_template_tour.js new file mode 100644 index 0000000..3573a45 --- /dev/null +++ b/bemade_fsm/static/tests/tours/task_template_tour.js @@ -0,0 +1,47 @@ +/** @odoo-module */ + +import tour from 'web_tour.tour'; + +tour.register('task_template_tour', { + test: true, + url: '/web', + }, /* Make sure the test task is visible via the Project > Task Templates path */ + [tour.stepUtils.showAppsMenuItem(), { + trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]', + }, { + content: 'Open the task templates list view.', + trigger: '.o_nav_entry[data-menu-xmlid="bemade_fsm.project_task_template_menu"]', + }, { + content: 'Open the existing task template.', + trigger: '.o_data_cell:contains("Template 1"):parent()', + }, { + content: 'Confirm that the header contains the task title.', + trigger: 'span[name="name"]:contains("Template 1")', + }, /* Make sure we can see the task template from the product */ + tour.stepUtils.toggleHomeMenu(), + { + content: 'Open the sales menu.', + trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', + }, { + content: 'Open the product dropdown menu.', + trigger: 'button[data-menu-xmlid="sale.product_menu_catalog"]', + }, { + content: 'Click the products menu item.', + trigger: 'a.dropdown-item[data-menu-xmlid="sale.menu_product_template_action"]', + }, { + content: 'Search for the test product.', + trigger: 'input.o_searchview_input', + extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Products))', + run: 'text Test Product 1', + }, { + trigger: '.o_menu_item.o_selection_focus', + content: 'Validate search', + run: 'click', + }, { + content: 'Open the test product.', + trigger: 'div.o_kanban_record:has(span:contains(Test Product 1))', + }, { + content: 'Ensure the product_template_id field is displayed with the "Template 1" mention.', + trigger: 'a.o_quick_editable[name="task_template_id"]:first-child:contains("Template 1")', + }, + ]) \ No newline at end of file diff --git a/bemade_fsm/tests/__init__.py b/bemade_fsm/tests/__init__.py new file mode 100644 index 0000000..a66b934 --- /dev/null +++ b/bemade_fsm/tests/__init__.py @@ -0,0 +1,4 @@ +from . import test_bemade_fsm_common +from . import test_task_template +from . import test_task_template_sale_order +from . import test_equipment diff --git a/bemade_fsm/tests/test_bemade_fsm_common.py b/bemade_fsm/tests/test_bemade_fsm_common.py new file mode 100644 index 0000000..47942ac --- /dev/null +++ b/bemade_fsm/tests/test_bemade_fsm_common.py @@ -0,0 +1,37 @@ +from odoo.tests.common import TransactionCase +from odoo import Command + +class FSMManagerUserTransactionCase(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + 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') + user_group_fsm_user = cls.env.ref('industry_fsm.group_fsm_user') + user_group_fsm_manager = cls.env.ref('industry_fsm.group_fsm_manager') + user_group_sales_manager = cls.env.ref('sales_team.group_sale_manager') + user_group_sales_user = cls.env.ref('sales_team.group_sale_salesman') + user_product_customer = cls.env.ref('customer_product_code.group_product_customer_code_user') + + group_ids = [user_group_employee.id, + user_group_project_user.id, + user_group_project_manager.id, + user_group_fsm_user.id, + user_group_fsm_manager.id, + user_group_sales_user.id, + 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)], + }) diff --git a/bemade_fsm/tests/test_equipment.py b/bemade_fsm/tests/test_equipment.py new file mode 100644 index 0000000..2ca0485 --- /dev/null +++ b/bemade_fsm/tests/test_equipment.py @@ -0,0 +1,37 @@ +from odoo.tests.common import HttpCase, tagged +from .test_bemade_fsm_common import FSMManagerUserTransactionCase + +class TestEquipmentCommon(FSMManagerUserTransactionCase): + + @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 TestEquipmentTour(HttpCase, TestEquipmentCommon): + + def test_equipment_tour(self): + self.start_tour('/web', 'task_equipment_tour', + login=self.user.login, ) diff --git a/bemade_fsm/tests/test_fsm_contact_setting.py b/bemade_fsm/tests/test_fsm_contact_setting.py new file mode 100644 index 0000000..a8874c2 --- /dev/null +++ b/bemade_fsm/tests/test_fsm_contact_setting.py @@ -0,0 +1,30 @@ +from odoo.tests import TransactionCase, HttpCase, tagged +from odoo import Command +from test_bemade_fsm_common import FSMManagerUserTransactionCase + +class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase): + + def test_default_site_contacts(self): + # Make sure the SO pulls the defaults from the partner correctly + pass + + def test_default_billing_contacts(self): + # Make sure the SO pulls the defaults from the partner correctly + pass + + def test_default_workorder_contacts(self): + # Make sure the SO pulls the defaults from the partner correctly + pass + + def test_change_site_contacts(self): + # Make sure the SO contacts can be updated without feeding back to the partner + pass + + def test_change_workorder_contacts(self): + # Make sure the SO contacts can be updated without feeding back to the partner + pass + + def test_change_billing_contacts(self): + # Make sure the SO contacts can be updated without feedin back to the partner + pass + diff --git a/bemade_fsm/tests/test_task_template.py b/bemade_fsm/tests/test_task_template.py new file mode 100644 index 0000000..bd46e0a --- /dev/null +++ b/bemade_fsm/tests/test_task_template.py @@ -0,0 +1,108 @@ +from .test_bemade_fsm_common import FSMManagerUserTransactionCase +from odoo.tests.common import HttpCase, tagged +from odoo.exceptions import MissingError +from odoo import Command +from psycopg2.errors import ForeignKeyViolation + + +class TestTaskTemplateCommon(FSMManagerUserTransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.PLANNED_HOURS = 6 + hours_uom = cls.env['uom.uom'].search([('name', '=', 'Hour')]) or False + # Test product to use with the various tests + cls.task1 = cls.env['project.task.template'].create({ + 'name': 'Template 1', + }) + + cls.project = cls.env['project.project'].create({ + 'name': 'Test Project', + }) + 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, + }) + cls.project_template = cls.env['project.project'].create({ + 'name': 'Test Project Template', + }) + 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, + }) + + # 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])]}) + + # 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, + '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, + }) + + +@tagged('-at_install', 'post_install') +class TestTaskTemplate(TestTaskTemplateCommon): + + def test_delete_task_template(self): + """User should never be able to delete a task template used on a product""" + with self.assertRaises(ForeignKeyViolation): + self.task1.unlink() + + 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() + # Reading deleted child's name field should be impossible + with self.assertRaises(MissingError): + test = self.grandchild_task.name + + +@tagged('-at_install', 'post_install') +class TestTaskTemplateTour(HttpCase, TestTaskTemplateCommon): + + def test_task_template_tour(self): + self.start_tour('/web', 'task_template_tour', + login='misterpm', ) diff --git a/bemade_fsm/tests/test_task_template_sale_order.py b/bemade_fsm/tests/test_task_template_sale_order.py new file mode 100644 index 0000000..176c222 --- /dev/null +++ b/bemade_fsm/tests/test_task_template_sale_order.py @@ -0,0 +1,86 @@ +from .test_task_template import TestTaskTemplateCommon +from odoo.tests.common import tagged + + +class TestTaskTemplateSalesOrder(TestTaskTemplateCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env['res.partner'].create({ + 'name': 'Test Partner', + }) + cls.sale_order1 = cls.env['sale.order'].create({ + 'partner_id': cls.partner.id, + 'client_order_ref': 'TEST ORDER', + }) + 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, + 'product_uom_qty': 1, + 'product_uom': cls.product_task_global_project.uom_id.id, + 'price_unit': 120.0, + 'order_id': cls.sale_order1.id, + 'tax_id': False, + }) + cls.sol_serv_order_task_in_project = cls.env['sale.order.line'].create({ + 'name': cls.product_task_in_project.name, + 'product_id': cls.product_task_in_project.id, + 'product_uom_qty': 1, + 'product_uom': cls.product_task_in_project.uom_id.id, + 'price_unit': 150.0, + 'order_id': cls.sale_order1.id, + 'tax_id': False, + }) + cls.sale_order2 = cls.env['sale.order'].create({ + 'partner_id': cls.partner.id, + 'client_order_ref': 'TEST ORDER', + }) + cls.sol_tree_order = cls.env['sale.order.line'].create({ + 'name': cls.product_task_tree_global_project.name, + 'product_id': cls.product_task_tree_global_project.id, + 'product_uom_qty': 1, + 'product_uom': cls.product_task_tree_global_project.uom_id.id, + 'price_unit': 120.0, + 'order_id': cls.sale_order2.id, + 'tax_id': False, + }) + cls.sol_serv_order_task_in_project = cls.env['sale.order.line'].create({ + 'name': cls.product_task_tree_in_project.name, + 'product_id': cls.product_task_tree_in_project.id, + 'product_uom_qty': 1, + 'product_uom': cls.product_task_tree_in_project.uom_id.id, + 'price_unit': 150.0, + 'order_id': cls.sale_order2.id, + '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. """ + so = self.sale_order1 + so.action_confirm() + sol1 = so.order_line[0] + sol2 = so.order_line[1] + task1 = sol1.task_id + task2 = sol2.task_id + self.assertTrue(task1) + self.assertTrue(task2) + self.assertTrue(self.task1.name in task1.name) + self.assertTrue(self.task1.name in task2.name) + self.assertTrue(self.task1.planned_hours == task1.planned_hours) + + def test_order_confirmation_tree_template(self): + def assert_structure(sol): + self.assertTrue(sol.task_id.child_ids and len(sol.task_id.child_ids) == 2) + self.assertTrue(self.parent_task.name in sol.task_id.name) + self.assertTrue(self.child_task_1.name in sol.task_id.child_ids[0].name) + self.assertTrue(self.child_task_2.name in sol.task_id.child_ids[1].name) + self.assertTrue(sol.task_id.child_ids[1].child_ids and len(sol.task_id.child_ids.child_ids) == 1) + self.assertTrue(self.grandchild_task.name in sol.task_id.child_ids.child_ids[0].name) + so = self.sale_order2 + so.action_confirm() + sol1 = so.order_line[0] + sol2 = so.order_line[1] + assert_structure(sol1) + assert_structure(sol2) + diff --git a/bemade_fsm/views/equipment.xml b/bemade_fsm/views/equipment.xml new file mode 100644 index 0000000..4047df6 --- /dev/null +++ b/bemade_fsm/views/equipment.xml @@ -0,0 +1,95 @@ + + + + + bemade_fsm.equipment.form + bemade_fsm.equipment + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+ + + bemade_fsm.equipment.tree + bemade_fsm.equipment + + + + + + + + + + + + + + + Equipment + bemade_fsm.equipment + tree,form + + + + Equipment Tag + bemade_fsm.equipment.tag + tree,form + + +
diff --git a/bemade_fsm/views/menus.xml b/bemade_fsm/views/menus.xml new file mode 100644 index 0000000..d5dce9e --- /dev/null +++ b/bemade_fsm/views/menus.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/bemade_fsm/views/product_views.xml b/bemade_fsm/views/product_views.xml new file mode 100644 index 0000000..7183ac5 --- /dev/null +++ b/bemade_fsm/views/product_views.xml @@ -0,0 +1,27 @@ + + + + + + bemade_fsm.product.template.form + product.template + + + + + + + + + bemade_fsm.product_search_form_view_inherit_bemade_fsm + product.product + + + + 0 + + + + + \ No newline at end of file diff --git a/bemade_fsm/views/res_partner.xml b/bemade_fsm/views/res_partner.xml new file mode 100644 index 0000000..d451cb9 --- /dev/null +++ b/bemade_fsm/views/res_partner.xml @@ -0,0 +1,70 @@ + + + + Equipments + bemade_fsm.equipment + tree,form + {'default_partner_id': active_id} + [("partner_id", "=", active_id)] + + + + bemade_fsm.partner.equipment.location.form + res.partner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bemade_fsm/views/task_template_views.xml b/bemade_fsm/views/task_template_views.xml new file mode 100644 index 0000000..bbdaed1 --- /dev/null +++ b/bemade_fsm/views/task_template_views.xml @@ -0,0 +1,102 @@ + + + + + + bemade_fsm.task_template.form + project.task.template + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +