diff --git a/bemade_fsm/__init__.py b/bemade_fsm/__init__.py
index 0650744..9b42961 100644
--- a/bemade_fsm/__init__.py
+++ b/bemade_fsm/__init__.py
@@ -1 +1,2 @@
from . import models
+from . import wizard
diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py
index c976f1c..861a9e8 100644
--- a/bemade_fsm/__manifest__.py
+++ b/bemade_fsm/__manifest__.py
@@ -20,7 +20,7 @@
########################################################################################
{
'name': 'Improved Field Service Management',
- 'version': '15.0.1.0.0',
+ 'version': '16.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',
@@ -55,6 +55,7 @@
'views/sale_order_views.xml',
# 'reports/worksheet_custom_report_templates.xml',
# 'reports/worksheet_custom_reports.xml',
+ 'wizard/new_task_from_template.xml',
],
'assets': {
'web.assets_tests': [
@@ -64,6 +65,13 @@
],
'web.report_assets_common': [
'bemade_fsm/static/src/scss/bemade_fsm.scss'
+ ],
+ 'web.assets_backend': [
+ 'bemade_fsm/static/src/js/kanban_view.js',
+ 'bemade_fsm/static/src/js/list_view.js',
+ ],
+ 'web.assets_qweb': [
+ 'bemade_fsm/static/src/xml/project_view_buttons.xml',
]
},
'installable': True,
diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py
index b9b5a92..1a37f85 100644
--- a/bemade_fsm/models/task.py
+++ b/bemade_fsm/models/task.py
@@ -76,6 +76,12 @@ class Task(models.Model):
work_order_number = fields.Char(readonly=True)
+ propagate_assignment = fields.Boolean(
+ string='Propagate Assignment',
+ help='Propagate assignment of this task to all subtasks.',
+ default=False,
+ )
+
@api.model_create_multi
def create(self, vals):
res = super().create(vals)
@@ -92,6 +98,16 @@ class Task(models.Model):
+ f"-{seq}"
return res
+ def write(self, vals):
+ super().write(vals)
+ if not self: # End recursion on empty RecordSet
+ return
+ if 'user_ids' in vals:
+ to_propagate = self.filtered(lambda task: task.propagate_assignment)
+ # Here we use child_ids instead of _get_all_subtasks() so as to allow for setting propagate_assignment
+ # to false on a child task.
+ to_propagate.child_ids.write({'user_ids': vals['user_ids']})
+
@api.depends('sale_order_id')
def _compute_relevant_order_lines(self):
for rec in self:
@@ -234,6 +250,11 @@ class Task(models.Model):
task.write(values)
+ def _get_full_hierarchy(self):
+ if self.child_ids:
+ return self | self.child_ids._get_full_hierarchy()
+ return self
+
def synchronize_name_fsm(self):
""" Applies naming to the entire task tree for tasks that are part of this
recordset. Root tasks are named:
diff --git a/bemade_fsm/models/task_template.py b/bemade_fsm/models/task_template.py
index 7253fbf..fdc1a46 100644
--- a/bemade_fsm/models/task_template.py
+++ b/bemade_fsm/models/task_template.py
@@ -81,3 +81,38 @@ class TaskTemplate(models.Model):
for rec in self:
new_equipment_ids = [eq.id for eq in rec.equipment_ids if eq.partner_location_id == rec.customer]
rec.write({'equipment_ids': [Command.set(new_equipment_ids)]})
+
+ def _prepare_new_task_values_from_self(self, project, name=False, parent_id=False):
+ vals = {
+ 'project_id': project.id,
+ 'name': name or self.name,
+ 'description': self.description,
+ 'parent_id': parent_id,
+ 'user_ids': self.assignees.ids,
+ 'tag_ids': self.tags.ids,
+ 'planned_hours': self.planned_hours,
+ 'sequence': self.sequence,
+ 'equipment_ids': [Command.set(self.equipment_ids.ids)] if self.equipment_ids else False,
+ 'partner_id': project.partner_id and project.partner_id.id,
+ 'company_id': self.company_id.id,
+ }
+ return vals
+
+ def create_task_from_self(self, project, name=False, parent_id=False):
+ """ Create a project.task from this template and return it. Can be called on a RecordSet of multiple templates.
+
+ :param project: project.project record the task should be added to
+ :param name: name for the new task (defaults to template name)
+ :param parent_id: parent task for the new task (none by default)
+ :return: project.task record created from this template
+ """
+ tasks = self.env['project.task']
+ for rec in self:
+ vals = rec._prepare_new_task_values_from_self(project, name, parent_id)
+ task = rec.env['project.task'].create(vals)
+ rec.subtasks.create_task_from_self(project, name=False, parent_id=task.id)
+ tasks |= task
+ return tasks
+
+
+
diff --git a/bemade_fsm/security/ir.model.access.csv b/bemade_fsm/security/ir.model.access.csv
index df83ebc..2c30345 100644
--- a/bemade_fsm/security/ir.model.access.csv
+++ b/bemade_fsm/security/ir.model.access.csv
@@ -5,3 +5,4 @@ access_bemade_fsm_equipment,bemade_fsm_equipment,model_bemade_fsm_equipment,base
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
access_bemade_fsm_visit,access_bemade_fsm_visit,model_bemade_fsm_visit,base.group_user,1,1,1,1
+access_bemade_fsm_task_wizard,access_bemade_fsm_task_wizard,model_project_task_from_template_wizard,project.group_project_user,1,1,1,1
diff --git a/bemade_fsm/static/src/js/kanban_view.js b/bemade_fsm/static/src/js/kanban_view.js
new file mode 100644
index 0000000..7f99dcc
--- /dev/null
+++ b/bemade_fsm/static/src/js/kanban_view.js
@@ -0,0 +1,37 @@
+/** @odoo-module **/
+
+import KanbanController from 'web.KanbanController';
+import KanbanView from 'web.KanbanView';
+const viewRegistry = require('web.view_registry')
+
+const ProjectKanbanController = KanbanController.extend({
+ buttons_template: 'project.KanbanView.buttons',
+ custom_events: _.extend({}, KanbanController.prototype.custom_events, {
+ create_from_template: '_onCreateFromTemplate',
+ }),
+ _onCreateFromTemplate(ev) {
+ ev.stopPropagation();
+ const record = this.model.get(this.handle, {raw: true});
+ let context = record.context;
+ context.active_id = record.project_id;
+ this.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'project.task.from.template.wizard',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {...record.context, res_model: 'project.task'},
+ });
+ },
+ renderButtons ($node) {
+ this._super.apply(this, arguments);
+ this.$buttons.on('click', 'button.o-kanban-button-new-from-template', this._onCreateFromTemplate.bind(this))
+ },
+})
+
+const ProjectKanbanView = KanbanView.extend({
+ config: _.extend({}, KanbanView.prototype.config, {
+ Controller: ProjectKanbanController,
+ })
+});
+
+viewRegistry.add('project_kanban', ProjectKanbanView)
\ No newline at end of file
diff --git a/bemade_fsm/static/src/js/list_view.js b/bemade_fsm/static/src/js/list_view.js
new file mode 100644
index 0000000..dc60c3a
--- /dev/null
+++ b/bemade_fsm/static/src/js/list_view.js
@@ -0,0 +1,36 @@
+/** @odoo-module **/
+
+import ListController from 'web.ListController';
+import ListView from 'web.ListView';
+const viewRegistry = require('web.view_registry')
+
+const ProjectListController = ListController.extend({
+ buttons_template: 'project.ListView.buttons',
+ custom_events: _.extend({}, ListController.prototype.custom_events, {
+ create_from_template: '_onCreateFromTemplate',
+ }),
+ _onCreateFromTemplate(ev) {
+ ev.stopPropagation();
+ const record = this.model.get(this.handle, {raw: true});
+ this.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'project.task.from.template.wizard',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {...record.context, res_model: 'project.task'},
+ });
+ },
+ renderButtons ($node) {
+ self = this;
+ this._super.apply(this, arguments);
+ this.$buttons.on('click', 'button.o_list_button_add_from_template', this._onCreateFromTemplate.bind(this))
+ },
+})
+
+const ProjectListView = ListView.extend({
+ config: _.extend({}, ListView.prototype.config, {
+ Controller: ProjectListController,
+ })
+});
+
+viewRegistry.add('project_list', ProjectListView)
\ No newline at end of file
diff --git a/bemade_fsm/static/src/xml/project_view_buttons.xml b/bemade_fsm/static/src/xml/project_view_buttons.xml
new file mode 100644
index 0000000..e2c48c6
--- /dev/null
+++ b/bemade_fsm/static/src/xml/project_view_buttons.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bemade_fsm/static/tests/tours/equipment_tour.js b/bemade_fsm/static/tests/tours/equipment_tour.js
deleted file mode 100644
index ec69e1e..0000000
--- a/bemade_fsm/static/tests/tours/equipment_tour.js
+++ /dev/null
@@ -1,104 +0,0 @@
-/** @odoo-module **/
-
-import tour from 'web_tour.tour';
-
-const TEST_COMPANY = "Test Partner";
-const TEST_EQPT1 = "Test Equipment 1";
-const TEST_EQPT2 = "Test Equipment 2";
-tour.register('equipment_base_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',
- }, {
- 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_COMPANY}`,
- }, {
- content: 'Click the partner in the dropdown',
- trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`,
- }, {
- content: 'Save equipment',
- trigger: 'button.o_list_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: `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 () {
- },
- }
- ]);
-
-tour.register('equipment_sale_order_tour', {
- test: true,
- url: '/web',
-}, [tour.stepUtils.showAppsMenuItem(), {
- content: 'Navigate to the Sales menu',
- trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
-}, {
- content: 'Create an order',
- trigger: 'button.o_list_button_add',
-}, {
- content: 'Select the test partner',
- trigger: 'div[name="partner_id"] input',
- run: `text ${TEST_COMPANY}`,
-}, {
- content: 'Click the partner in the dropdown',
- trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`,
-}, {
- content: 'Save',
- trigger: 'button.o_form_button_save',
-}, {
- content: 'Navigate to the Field Service tab.',
- trigger: 'a.nav-link[role="tab"]:contains(Field Service)'
-}, {
- content: 'Check that the equipment is listed in the tab.',
- trigger: `li.ui-menu-item > a:contains(${TEST_EQPT1})`,
-}
-]);
\ No newline at end of file
diff --git a/bemade_fsm/static/tests/tours/sale_order_tour.js b/bemade_fsm/static/tests/tours/sale_order_tour.js
deleted file mode 100644
index 6ea8eee..0000000
--- a/bemade_fsm/static/tests/tours/sale_order_tour.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/** @odoo-module **/
-
-import tour from 'web_tour.tour';
-
-const SO_NAME = "TEST ORDER 2"
-const PRODUCT_NAME = "Test Product 3"
-tour.register('sale_order_tour', {
- test: true,
- url: '/web',
- },
- [tour.stepUtils.showAppsMenuItem(), {
- content: 'Navigate to the Service menu',
- trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
- }, {
- content: 'Search for the sales order',
- trigger: 'input.o_searchview_input',
- run: `text ${SO_NAME}`,
- }, {
- content: 'Validate Search',
- trigger: '.o_menu_item.o_selection_focus',
- run: 'click',
- }, {
- content: 'Open the test order',
- trigger: `.o_data_cell[name="client_order_ref"]:contains(${SO_NAME})`,
- }, {
- content: 'Click the view tasks button',
- trigger: 'button[name="action_view_task"]',
- }, /*{
- content: 'Click the first task',
- trigger: `div.o_kanban_record:has(span:contains(${PRODUCT_NAME}))`,
- },*/ {
- content: 'Click on the ready to invoice button',
- trigger: 'button[name="action_fsm_validate"]',
- extra_trigger: `li.breadcrumb-item.active:has(span:contains(${PRODUCT_NAME}))`
- }, {
- content: 'View the SO',
- trigger: 'button[name="action_view_so"]',
- // extra_trigger: 'button[title="Current state"]:contains(Done)',
- }
- ]);
diff --git a/bemade_fsm/static/tests/tours/task_equipment_tour.js b/bemade_fsm/static/tests/tours/task_equipment_tour.js
deleted file mode 100644
index f8a244d..0000000
--- a/bemade_fsm/static/tests/tours/task_equipment_tour.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/** @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_list_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
deleted file mode 100644
index 3573a45..0000000
--- a/bemade_fsm/static/tests/tours/task_template_tour.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/** @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
index 381536f..4b317ee 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_fsm_visit
+from . import test_task
diff --git a/bemade_fsm/tests/test_sale_order.py b/bemade_fsm/tests/test_sale_order.py
index 2801ff3..1e39371 100644
--- a/bemade_fsm/tests/test_sale_order.py
+++ b/bemade_fsm/tests/test_sale_order.py
@@ -251,7 +251,7 @@ class TestSalesOrder(BemadeFSMBaseTest):
self.assertFalse(visit_task.user_ids)
self.assertFalse(subtask1.user_ids)
self.assertFalse(subtask2.user_ids)
-
+
def test_long_line_name_overflows_to_task_description(self):
so = self._generate_sale_order()
product = self._generate_product()
diff --git a/bemade_fsm/tests/test_task.py b/bemade_fsm/tests/test_task.py
new file mode 100644
index 0000000..63185de
--- /dev/null
+++ b/bemade_fsm/tests/test_task.py
@@ -0,0 +1,40 @@
+from .test_bemade_fsm_common import BemadeFSMBaseTest
+from odoo.tests.common import tagged
+from odoo import Command
+
+
+@tagged('post_install', '-at_install')
+class TaskTest(BemadeFSMBaseTest):
+
+ def test_reassigning_assignment_propagating_task_changes_subtasks(self):
+ # This creation step is a bit lazy - we use defaults to make tasks with a hierachy and settings we want
+ so = self._generate_sale_order()
+ template = self._generate_task_template(names=['Parent', 'Child'], structure=[2])
+ product = self._generate_product(task_template=template)
+ sol = self._generate_sale_order_line(sale_order=so, product=product)
+ user = self._generate_project_manager_user('Bob', 'Bob')
+ so.action_confirm()
+ task = sol.task_id
+
+ task.write({
+ 'user_ids': [Command.set([user.id])]
+ })
+
+ self.assertTrue(all([t.user_ids == user for t in task | task._get_all_subtasks()]))
+
+ def test_reassigning_assignment_non_propagating_task_doesnt_change_subtasks(self):
+ so = self._generate_sale_order()
+ template = self._generate_task_template(names=['Parent', 'Child', 'Grandchild'], structure=[2, 1])
+ product = self._generate_product(task_template=template)
+ sol = self._generate_sale_order_line(sale_order=so, product=product)
+ user = self._generate_project_manager_user('Bob', 'Bob')
+ so.action_confirm()
+ task = sol.task_id
+ task.child_ids.write({'propagate_assignment': False}) # Stop propagation after the first level
+
+ task.write({
+ 'user_ids': [Command.set([user.id])]
+ })
+
+ self.assertTrue(all([t.user_ids == user for t in task | task.child_ids]))
+ self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids]))
diff --git a/bemade_fsm/tests/test_task_template.py b/bemade_fsm/tests/test_task_template.py
index d0cf173..178911a 100644
--- a/bemade_fsm/tests/test_task_template.py
+++ b/bemade_fsm/tests/test_task_template.py
@@ -78,3 +78,17 @@ class TestTaskTemplate(BemadeFSMBaseTest):
self.assertEqual(sol1.task_id.name, "Short Name 1")
self.assertEqual(sol2.task_id.name, "Short Name 2")
self.assertEqual(sol3.task_id.name, "Task")
+
+ def test_task_creation_directly_from_template(self):
+ project = self.env.ref("industry_fsm.fsm_project")
+ template = self._generate_task_template(names=['Task', 'Child', 'Grandchild'], structure=[2, 1])
+
+ task = template.create_task_from_self(project, "My new task")
+
+ self.assertEqual(len(task.child_ids), len(template.subtasks))
+ self.assertEqual(len(task.child_ids[0].child_ids), len(template.subtasks[0].subtasks))
+ self.assertEqual(len(task.child_ids[1].child_ids), len(template.subtasks[1].subtasks))
+ self.assertEqual(task.name, "My new task")
+ self.assertEqual(task.child_ids[0].name, template.subtasks[0].name)
+ self.assertTrue(all([t.project_id == project for t in task | task._get_all_subtasks()]))
+
diff --git a/bemade_fsm/views/task_views.xml b/bemade_fsm/views/task_views.xml
index ddc48b8..9d8d201 100644
--- a/bemade_fsm/views/task_views.xml
+++ b/bemade_fsm/views/task_views.xml
@@ -46,6 +46,9 @@
+
+
+
@@ -168,4 +171,4 @@
-
\ No newline at end of file
+
diff --git a/bemade_fsm/wizard/__init__.py b/bemade_fsm/wizard/__init__.py
new file mode 100644
index 0000000..5cc0245
--- /dev/null
+++ b/bemade_fsm/wizard/__init__.py
@@ -0,0 +1 @@
+from . import new_task_from_template
diff --git a/bemade_fsm/wizard/new_task_from_template.py b/bemade_fsm/wizard/new_task_from_template.py
new file mode 100644
index 0000000..6c8c5e1
--- /dev/null
+++ b/bemade_fsm/wizard/new_task_from_template.py
@@ -0,0 +1,51 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+
+
+class NewTaskFromTemplateWizard(models.TransientModel):
+ _name = "project.task.from.template.wizard"
+ _description = "Create Task from Template Wizard"
+
+ project_id = fields.Many2one(
+ comodel_name='project.project',
+ string='Project',
+ help='The project the new task should be created in.',
+ required=True,
+ )
+
+ task_template_id = fields.Many2one(
+ comodel_name='project.task.template',
+ string='Task Template',
+ help='The template to use when creating the new task.',
+ required=True,
+ )
+
+ new_task_title = fields.Char(
+ help='The title (name) for the newly created task. If left blank, the name of the template will be used.',
+ )
+
+ def default_get(self, fields_list):
+ res = super().default_get(fields_list)
+ active_id = self.env.context.get('active_id', False)
+ active_model = self.env.context.get('active_model', False)
+ if not active_model:
+ params = self.env.context.get('params', False)
+ active_model = params and params.get('model', False)
+ if active_model == 'project.task.template' and active_id and 'task_template_id' in fields_list:
+ res.update({'task_template_id': active_id})
+ if active_model == 'project.task' and 'project_id' in fields_list:
+ res.update({'project_id': self.env.ref('industry_fsm.fsm_project').id})
+ return res
+
+ def action_create_task_from_template(self):
+ self.ensure_one()
+ task = self.task_template_id.create_task_from_self(self.project_id, self.new_task_title)
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'project.task',
+ 'res_id': task.id,
+ 'view_mode': 'form',
+ 'target': 'current',
+ }
+
+
diff --git a/bemade_fsm/wizard/new_task_from_template.xml b/bemade_fsm/wizard/new_task_from_template.xml
new file mode 100644
index 0000000..1290b8f
--- /dev/null
+++ b/bemade_fsm/wizard/new_task_from_template.xml
@@ -0,0 +1,29 @@
+
+
+
+ project.task.from.template.wizard.view.form
+ project.task.from.template.wizard
+
+
+
+
+
+ New Task from Template
+ project.task.from.template.wizard
+
+ form
+ new
+
+
\ No newline at end of file
diff --git a/bemade_full_formview_from_modal/__init__.py b/bemade_full_formview_from_modal/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bemade_full_formview_from_modal/__manifest__.py b/bemade_full_formview_from_modal/__manifest__.py
new file mode 100644
index 0000000..66d3327
--- /dev/null
+++ b/bemade_full_formview_from_modal/__manifest__.py
@@ -0,0 +1,37 @@
+#
+# 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': 'Full Form from Dialog',
+ 'version': '15.0.1.0.0',
+ 'summary': 'Allows opening the full form view from the dialog (modal) view.',
+ 'description': 'Adds a button to open the full form view when viewing the form view for a record in a dialog.',
+ 'category': 'Technical',
+ 'author': 'Bemade Inc.',
+ 'website': 'http://www.bemade.org',
+ 'license': 'OPL-1',
+ 'depends': [],
+ 'data': [],
+ 'assets': {
+ 'web.assets_backend': [
+ 'bemade_full_formview_from_modal/static/src/js/form_view_dialog.js',
+ ],
+ },
+ 'installable': True,
+ 'auto_install': False
+}
diff --git a/bemade_full_formview_from_modal/static/src/js/form_view_dialog.js b/bemade_full_formview_from_modal/static/src/js/form_view_dialog.js
new file mode 100644
index 0000000..fbb2b83
--- /dev/null
+++ b/bemade_full_formview_from_modal/static/src/js/form_view_dialog.js
@@ -0,0 +1,40 @@
+/** @odoo-module **/
+
+import FormViewDialog from 'web.FormView';
+import Dialog from 'web.Dialog';
+import {patch} from '@web/core/utils/patch';
+import {_t} from 'web.core';
+
+const dom = require('web.dom')
+
+patch(Dialog.prototype, "bemade_full_formview_from_modal.Dialog", {
+ init: function (parent, options) {
+ this._super(...arguments);
+ },
+ willStart: function () {
+ const self = this;
+ return this._super(...arguments).then(function () {
+ if (self.$footer) {
+ const $button = dom.renderButton({
+ attrs: {
+ class: 'btn btn-secondary o_form_button_open',
+ },
+ text: _t('Open'),
+ });
+ $button.on('click', function() {self._onOpen.call(self)});
+ self.$footer.append($button);
+ }
+ }
+ );
+ },
+ _onOpen: function () {
+ this.do_action({
+ type: 'ir.actions.act_window',
+ res_model: this.options.res_model,
+ res_id: this.options.res_id,
+ views: [[false, 'form']],
+ target: 'current',
+ context: this.context,
+ })
+ }
+});