commit b2a42e4ce8dbe5f2abaa977952cafebc2f3f8278 Author: Marc Durepos Date: Fri Jun 9 10:24:06 2023 -0400 bemade_fsm: Initial commit as submodule. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..a49a539 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,22 @@ +{ + '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': 'LGPL-3', + 'depends': ['project', 'stock', 'sale', 'sale_project', 'sale_stock'], + 'data': ['views/task_template_views.xml', + 'security/ir.model.access.csv', + 'views/product_views.xml', + ], + 'assets': { + 'web.assets_tests': [ + 'durpro_fsm/static/tests/tours/task_template_tour.js', + ], + }, + 'installable': True, + 'auto_install': False +} diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..b5c96a5 Binary files /dev/null and b/__pycache__/__init__.cpython-310.pyc differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..08966cb --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,3 @@ +from . import task_template +from . import product_template +from . import sale_order diff --git a/models/__pycache__/__init__.cpython-310.pyc b/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..894c59b Binary files /dev/null and b/models/__pycache__/__init__.cpython-310.pyc differ diff --git a/models/__pycache__/product_template.cpython-310.pyc b/models/__pycache__/product_template.cpython-310.pyc new file mode 100644 index 0000000..dcb03c9 Binary files /dev/null and b/models/__pycache__/product_template.cpython-310.pyc differ diff --git a/models/__pycache__/sale_order.cpython-310.pyc b/models/__pycache__/sale_order.cpython-310.pyc new file mode 100644 index 0000000..9f6b12a Binary files /dev/null and b/models/__pycache__/sale_order.cpython-310.pyc differ diff --git a/models/__pycache__/task_template.cpython-310.pyc b/models/__pycache__/task_template.cpython-310.pyc new file mode 100644 index 0000000..35d4aa6 Binary files /dev/null and b/models/__pycache__/task_template.cpython-310.pyc differ diff --git a/models/product_template.py b/models/product_template.py new file mode 100644 index 0000000..c7ecb0a --- /dev/null +++ b/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/models/sale_order.py b/models/sale_order.py new file mode 100644 index 0000000..6cbc9cb --- /dev/null +++ b/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. Subtasks are left without + a specific project_id so that only the top-level tasks shows up on the project task list/kanban views. + + :param project: project.project record to set on the task's project_id field. + Pass the project.project model 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. + """ + values = _timesheet_create_task_prepare_values_from_template(project, template, parent) + task = self.env['project.task'].sudo().create(values) + subtasks = [] + for subtask in tmpl.subtasks: + # Recurse, but pass the Project model so that no project_id gets set on the subtasks + subtask = _create_task_from_template(self.env['project.project'], tmpl, task) + subtasks.append(subtask) + task.write({'child_ids': [Command.set([t.id for t in subtasks])]}) + 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.update({'name': f"{vals['name']} ({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 + 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/models/task_template.py b/models/task_template.py new file mode 100644 index 0000000..b02f763 --- /dev/null +++ b/models/task_template.py @@ -0,0 +1,31 @@ +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") + 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) + + 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/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..52439aa --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_durpro_fsm_task_template,durpro_fsm_task_template,model_project_task_template,project.group_project_manager,1,1,1,1 +access_durpro_fsm_task_template,durpro_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1 \ No newline at end of file diff --git a/static/tests/tours/task_template_tour.js b/static/tests/tours/task_template_tour.js new file mode 100644 index 0000000..f1bf5d9 --- /dev/null +++ b/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="durpro_fsm.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/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..5bad71e --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +from . import test_task_template diff --git a/tests/__pycache__/__init__.cpython-310.pyc b/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..3928a60 Binary files /dev/null and b/tests/__pycache__/__init__.cpython-310.pyc differ diff --git a/tests/__pycache__/test_task_template.cpython-310.pyc b/tests/__pycache__/test_task_template.cpython-310.pyc new file mode 100644 index 0000000..ff546e6 Binary files /dev/null and b/tests/__pycache__/test_task_template.cpython-310.pyc differ diff --git a/tests/test_task_template.py b/tests/test_task_template.py new file mode 100644 index 0000000..abdf09c --- /dev/null +++ b/tests/test_task_template.py @@ -0,0 +1,97 @@ +from odoo.tests.common import TransactionCase, HttpCase, tagged +from odoo.exceptions import UserError + + +class TestTaskTemplateCommon(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_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, user_group_project_user, user_group_project_manager, user_group_sales_user, + user_group_sales_manager] + if user_product_customer: + group_ids.append(user_product_customer) + + # 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': [(6, 0, [user_group_employee.id, user_group_project_user.id, user_group_project_manager.id, + user_group_sales_user.id])] + }) + 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.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, + 'uom_po_id': hours_uom.id, + 'uom_id': hours_uom.id, + }) + cls.partner = cls.env['res.partner'].create({'name': 'Test Partner'}) + cls.sale_order = 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_order.id, + 'tax_id': False, + }) + + +@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(UserError): + self.task1.unlink() + + def test_order_confirmation_single_task(self): + """ Confirming the order should create a task in the global project. """ + self.sale_order.action_confirm() + so = self.sale_order + sol = self.sale_order.order_line[0] + task = sol.task_id + self.assertTrue(task) + + + +@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/views/product_views.xml b/views/product_views.xml new file mode 100644 index 0000000..52f31c6 --- /dev/null +++ b/views/product_views.xml @@ -0,0 +1,17 @@ + + + + + + durpro_fsm.product.template.form + product.template + + + + + + + + + \ No newline at end of file diff --git a/views/task_template_views.xml b/views/task_template_views.xml new file mode 100644 index 0000000..dbc7fed --- /dev/null +++ b/views/task_template_views.xml @@ -0,0 +1,101 @@ + + + + + + durpro_fsm.task_template.form + project.task.template + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +