From 0d5935a1e4bd4a0524fb4b26a9a2081f82de2244 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Mon, 4 Dec 2023 13:58:58 -0500 Subject: [PATCH 01/19] new module to open full form view from a dialog. --- bemade_full_formview_from_modal/__init__.py | 0 .../__manifest__.py | 37 ++++++++++++++++ .../static/src/js/form_view_dialog.js | 43 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 bemade_full_formview_from_modal/__init__.py create mode 100644 bemade_full_formview_from_modal/__manifest__.py create mode 100644 bemade_full_formview_from_modal/static/src/js/form_view_dialog.js 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..e94ad00 --- /dev/null +++ b/bemade_full_formview_from_modal/static/src/js/form_view_dialog.js @@ -0,0 +1,43 @@ +/** @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); + }, + custom_events: _.extend({}, Dialog.prototype.custom_events, { + open_full_form_view: '_onOpen', + }), + 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, + }) + } +}); From 8af9704b67ade939dcdd1cd955ca0a2e1f7bb987 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 5 Dec 2023 06:59:51 -0500 Subject: [PATCH 02/19] bemade_fsm: work for creating tasks directly from templates. --- bemade_fsm/models/task_template.py | 35 ++++++++++++++++ bemade_fsm/tests/test_task_template.py | 14 +++++++ bemade_fsm/wizard/__init__.py | 1 + bemade_fsm/wizard/new_task_from_template.py | 44 ++++++++++++++++++++ bemade_fsm/wizard/new_task_from_template.xml | 22 ++++++++++ 5 files changed, 116 insertions(+) create mode 100644 bemade_fsm/wizard/__init__.py create mode 100644 bemade_fsm/wizard/new_task_from_template.py create mode 100644 bemade_fsm/wizard/new_task_from_template.xml 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/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/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..575629c --- /dev/null +++ b/bemade_fsm/wizard/new_task_from_template.py @@ -0,0 +1,44 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + + +class NewTaskFromTemplateWizard(models.TransientModel): + _name = "project.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) + if 'project_id' in fields_list: + active_id = self.env.context.get('active_id', False) + res.update({'project_id': active_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..49a22ce --- /dev/null +++ b/bemade_fsm/wizard/new_task_from_template.xml @@ -0,0 +1,22 @@ + + + + project.task.from.template.wizard.view.form + project.task.from.template.wizard + +
+
+
+ + + + + + + +
+
+
+
\ No newline at end of file From 22936eee1c0fb7b403ca91611ad94f7d2d258ae8 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 5 Dec 2023 09:58:39 -0500 Subject: [PATCH 03/19] bemade_fsm: Create task from template (from FSM views) Adds a button to create a task from a task template to the list view and kanban view for the project.task when viewed from the FSM application. --- bemade_fsm/__init__.py | 1 + bemade_fsm/__manifest__.py | 8 ++++ bemade_fsm/security/ir.model.access.csv | 1 + bemade_fsm/static/src/js/kanban_view.js | 37 +++++++++++++++++++ bemade_fsm/static/src/js/list_view.js | 36 ++++++++++++++++++ .../static/src/xml/project_view_buttons.xml | 21 +++++++++++ bemade_fsm/views/task_views.xml | 13 +++++++ bemade_fsm/wizard/new_task_from_template.py | 14 +++++-- bemade_fsm/wizard/new_task_from_template.xml | 15 ++++++-- 9 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 bemade_fsm/static/src/js/kanban_view.js create mode 100644 bemade_fsm/static/src/js/list_view.js create mode 100644 bemade_fsm/static/src/xml/project_view_buttons.xml 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 c71d220..c378151 100644 --- a/bemade_fsm/__manifest__.py +++ b/bemade_fsm/__manifest__.py @@ -53,6 +53,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': [ @@ -62,6 +63,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/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..2185d01 --- /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, active_id: record.project_id}, + }); + }, + 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..3486965 --- /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, active_id: record.project_id}, + }); + }, + 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..df3484a --- /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/views/task_views.xml b/bemade_fsm/views/task_views.xml index 1fbfecf..25f71be 100644 --- a/bemade_fsm/views/task_views.xml +++ b/bemade_fsm/views/task_views.xml @@ -98,6 +98,9 @@ project.task + + project_list + @@ -112,6 +115,16 @@ + + project.task.view.kanban.fsm.inherit + project.task + + + + project_kanban + + + project.task.view.search.fsm.inherit project.task diff --git a/bemade_fsm/wizard/new_task_from_template.py b/bemade_fsm/wizard/new_task_from_template.py index 575629c..26d67ba 100644 --- a/bemade_fsm/wizard/new_task_from_template.py +++ b/bemade_fsm/wizard/new_task_from_template.py @@ -4,6 +4,7 @@ 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', @@ -25,9 +26,16 @@ class NewTaskFromTemplateWizard(models.TransientModel): def default_get(self, fields_list): res = super().default_get(fields_list) - if 'project_id' in fields_list: - active_id = self.env.context.get('active_id', False) - res.update({'project_id': active_id}) + 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.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): diff --git a/bemade_fsm/wizard/new_task_from_template.xml b/bemade_fsm/wizard/new_task_from_template.xml index 49a22ce..1290b8f 100644 --- a/bemade_fsm/wizard/new_task_from_template.xml +++ b/bemade_fsm/wizard/new_task_from_template.xml @@ -5,10 +5,6 @@ project.task.from.template.wizard
-
-
@@ -16,7 +12,18 @@ +
+
+ + New Task from Template + project.task.from.template.wizard + + form + new + \ No newline at end of file From 79d1cbe330870b6f7b30023bdfbfbad8062b0f80 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 5 Dec 2023 11:16:10 -0500 Subject: [PATCH 04/19] bemade_full_formview_from_modal: remove useless lines of JS. --- .../static/src/js/form_view_dialog.js | 3 --- 1 file changed, 3 deletions(-) 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 index e94ad00..fbb2b83 100644 --- 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 @@ -11,9 +11,6 @@ patch(Dialog.prototype, "bemade_full_formview_from_modal.Dialog", { init: function (parent, options) { this._super(...arguments); }, - custom_events: _.extend({}, Dialog.prototype.custom_events, { - open_full_form_view: '_onOpen', - }), willStart: function () { const self = this; return this._super(...arguments).then(function () { From 0806bc96f0292e54349cf9a1010b8da3e69549b8 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 5 Dec 2023 11:19:16 -0500 Subject: [PATCH 05/19] version bump for bemade_fsm --- bemade_fsm/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py index c378151..ecc58bc 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.3', + 'version': '15.0.1.0.4', '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', From 6ab1bda2b7bb0a3fd277bad84b9c9f50a03953a7 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 5 Dec 2023 11:43:24 -0500 Subject: [PATCH 06/19] bemade_fsm: bug fix in new_task_from_template.py default_get --- bemade_fsm/wizard/new_task_from_template.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bemade_fsm/wizard/new_task_from_template.py b/bemade_fsm/wizard/new_task_from_template.py index 26d67ba..6c8c5e1 100644 --- a/bemade_fsm/wizard/new_task_from_template.py +++ b/bemade_fsm/wizard/new_task_from_template.py @@ -30,8 +30,7 @@ class NewTaskFromTemplateWizard(models.TransientModel): active_model = self.env.context.get('active_model', False) if not active_model: params = self.env.context.get('params', False) - active_model = params.get('model', 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: From 9ff8934556c0a9a0c9004ec0bc4a6b70aca17604 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 5 Dec 2023 12:11:48 -0500 Subject: [PATCH 07/19] Fixed new task from template wizard to show right default project. --- bemade_fsm/static/src/js/kanban_view.js | 2 +- bemade_fsm/static/src/js/list_view.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bemade_fsm/static/src/js/kanban_view.js b/bemade_fsm/static/src/js/kanban_view.js index 2185d01..7f99dcc 100644 --- a/bemade_fsm/static/src/js/kanban_view.js +++ b/bemade_fsm/static/src/js/kanban_view.js @@ -19,7 +19,7 @@ const ProjectKanbanController = KanbanController.extend({ res_model: 'project.task.from.template.wizard', views: [[false, 'form']], target: 'new', - context: {...record.context, active_id: record.project_id}, + context: {...record.context, res_model: 'project.task'}, }); }, renderButtons ($node) { diff --git a/bemade_fsm/static/src/js/list_view.js b/bemade_fsm/static/src/js/list_view.js index 3486965..dc60c3a 100644 --- a/bemade_fsm/static/src/js/list_view.js +++ b/bemade_fsm/static/src/js/list_view.js @@ -17,7 +17,7 @@ const ProjectListController = ListController.extend({ res_model: 'project.task.from.template.wizard', views: [[false, 'form']], target: 'new', - context: {...record.context, active_id: record.project_id}, + context: {...record.context, res_model: 'project.task'}, }); }, renderButtons ($node) { From 41ef2c1484e28f1b72b62b5f9dfe26b8b5341a4d Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 5 Dec 2023 12:23:38 -0500 Subject: [PATCH 08/19] bemade_fsm: spacing for task from template button, delete js tests. --- .../static/src/xml/project_view_buttons.xml | 2 +- .../static/tests/tours/equipment_tour.js | 104 ------------------ .../static/tests/tours/sale_order_tour.js | 40 ------- .../static/tests/tours/task_equipment_tour.js | 75 ------------- .../static/tests/tours/task_template_tour.js | 47 -------- 5 files changed, 1 insertion(+), 267 deletions(-) delete mode 100644 bemade_fsm/static/tests/tours/equipment_tour.js delete mode 100644 bemade_fsm/static/tests/tours/sale_order_tour.js delete mode 100644 bemade_fsm/static/tests/tours/task_equipment_tour.js delete mode 100644 bemade_fsm/static/tests/tours/task_template_tour.js diff --git a/bemade_fsm/static/src/xml/project_view_buttons.xml b/bemade_fsm/static/src/xml/project_view_buttons.xml index df3484a..e2c48c6 100644 --- a/bemade_fsm/static/src/xml/project_view_buttons.xml +++ b/bemade_fsm/static/src/xml/project_view_buttons.xml @@ -12,7 +12,7 @@ - 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 From 74ec46382a2f435263f2d1caf5496e679b3ea451 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 5 Dec 2023 14:56:06 -0500 Subject: [PATCH 09/19] bemade_fsm: Propagation of task assignee to subtasks (optional). --- bemade_fsm/__manifest__.py | 2 +- bemade_fsm/models/task.py | 17 ++++++++++++ bemade_fsm/tests/__init__.py | 1 + bemade_fsm/tests/test_sale_order.py | 2 +- bemade_fsm/tests/test_task.py | 40 +++++++++++++++++++++++++++++ bemade_fsm/views/task_views.xml | 3 +++ 6 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 bemade_fsm/tests/test_task.py diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py index ecc58bc..9b7669d 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.4', + 'version': '15.0.1.0.5', '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', diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py index 296a7c6..c0b20e0 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=True, + ) + @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: @@ -199,6 +215,7 @@ class Task(models.Model): 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/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 c51246c..484392e 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/views/task_views.xml b/bemade_fsm/views/task_views.xml index 25f71be..8db34b3 100644 --- a/bemade_fsm/views/task_views.xml +++ b/bemade_fsm/views/task_views.xml @@ -46,6 +46,9 @@ + + + From 165ad8a226c55bf433105b9cc045f47791322bad Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Fri, 8 Dec 2023 14:01:41 -0500 Subject: [PATCH 10/19] bemade_fsm: propagate assignees to false by default. --- bemade_fsm/__manifest__.py | 2 +- bemade_fsm/models/task.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py index 9b7669d..fa6c83b 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.5', + 'version': '15.0.1.0.6', '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', diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py index c0b20e0..43a147d 100644 --- a/bemade_fsm/models/task.py +++ b/bemade_fsm/models/task.py @@ -79,7 +79,7 @@ class Task(models.Model): propagate_assignment = fields.Boolean( string='Propagate Assignment', help='Propagate assignment of this task to all subtasks.', - default=True, + default=False, ) @api.model_create_multi From c950fe3e93ca51f397ba5ab57f7f36f354ae16e4 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Mon, 11 Dec 2023 18:23:31 -0500 Subject: [PATCH 11/19] [ENH] bemade_sports_clinic: wrap text on the portal and show HTML. Allows portal users to see the full diagnosis and notes. Previously, diagnosis was truncated if it went beyond the bounds of the column. Also, notes are now shown in HTML instead of HTML escaped to text. --- bemade_sports_clinic/__manifest__.py | 2 +- bemade_sports_clinic/views/sports_clinic_portal_views.xml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bemade_sports_clinic/__manifest__.py b/bemade_sports_clinic/__manifest__.py index 8b604e0..f1689fd 100644 --- a/bemade_sports_clinic/__manifest__.py +++ b/bemade_sports_clinic/__manifest__.py @@ -18,7 +18,7 @@ # { 'name': 'Sports Clinic Management', - 'version': '16.0.1.5.0', + 'version': '16.0.1.5.1', 'summary': 'Manage the patients of a sports medicine clinic.', 'description': """ Adds the notion of sports teams, players (patients), coaches and treatment diff --git a/bemade_sports_clinic/views/sports_clinic_portal_views.xml b/bemade_sports_clinic/views/sports_clinic_portal_views.xml index a91b59e..63d0cb7 100644 --- a/bemade_sports_clinic/views/sports_clinic_portal_views.xml +++ b/bemade_sports_clinic/views/sports_clinic_portal_views.xml @@ -165,18 +165,18 @@ - Active Injury - Notes + Active Injury + Notes - + - + From 6ff1bf5706880dc2cb319dedd39d9e1eb3596fcb Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Mon, 11 Dec 2023 21:30:08 -0500 Subject: [PATCH 12/19] [ENH] bemade_sports_clinic: Kanban view created to enhance mobile. Kanban view added for players to improve the experience when browsing on mobile. Card text color synchronized with what the user expects from the list view. --- bemade_sports_clinic/__manifest__.py | 2 +- bemade_sports_clinic/models/patient.py | 2 +- .../views/sports_patient_views.xml | 43 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/bemade_sports_clinic/__manifest__.py b/bemade_sports_clinic/__manifest__.py index f1689fd..81b355f 100644 --- a/bemade_sports_clinic/__manifest__.py +++ b/bemade_sports_clinic/__manifest__.py @@ -18,7 +18,7 @@ # { 'name': 'Sports Clinic Management', - 'version': '16.0.1.5.1', + 'version': '16.0.1.5.2', 'summary': 'Manage the patients of a sports medicine clinic.', 'description': """ Adds the notion of sports teams, players (patients), coaches and treatment diff --git a/bemade_sports_clinic/models/patient.py b/bemade_sports_clinic/models/patient.py index a15db38..201bea2 100644 --- a/bemade_sports_clinic/models/patient.py +++ b/bemade_sports_clinic/models/patient.py @@ -61,7 +61,7 @@ class Patient(models.Model): "return to match play.") is_injured = fields.Boolean(compute="_compute_is_injured") stage = fields.Selection( - selection=[('no_play', 'Injured'), ('practice_ok', 'Cleared for Practice'), ('healthy', 'Cleared to Play')], + selection=[('no_play', 'Injured'), ('practice_ok', 'Practice OK'), ('healthy', 'Play OK')], compute='_compute_stage') last_consultation_date = fields.Date() active_injury_count = fields.Integer(compute='_compute_active_injury_count') diff --git a/bemade_sports_clinic/views/sports_patient_views.xml b/bemade_sports_clinic/views/sports_patient_views.xml index 6157357..3b1fccf 100644 --- a/bemade_sports_clinic/views/sports_patient_views.xml +++ b/bemade_sports_clinic/views/sports_patient_views.xml @@ -62,6 +62,49 @@
+ + sports.patient.view.kanban + sports.patient + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+
+ Status: +
+
+
+ DOB: + +
+
+
+
+
+
+
+
+ sports.patient.view.form sports.patient From 148fc500ed915eb91324e0dee97f28a6fd775830 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Mon, 18 Dec 2023 15:20:33 -0500 Subject: [PATCH 13/19] move account_credit_hold into bemade-addons --- account_credit_hold/__init__.py | 1 + account_credit_hold/__manifest__.py | 30 ++++++++++ account_credit_hold/models/__init__.py | 5 ++ .../models/account_followup.py | 8 +++ .../models/account_followup_report.py | 14 +++++ account_credit_hold/models/res_partner.py | 59 +++++++++++++++++++ account_credit_hold/models/sale_order.py | 17 ++++++ account_credit_hold/models/stock_picking.py | 9 +++ account_credit_hold/readme.md | 35 +++++++++++ .../static/src/js/followup_form_controller.js | 48 +++++++++++++++ .../static/src/js/followup_form_model.js | 14 +++++ .../src/xml/account_followup_template.xml | 10 ++++ .../views/account_followup_views.xml | 16 +++++ .../views/res_partner_views.xml | 33 +++++++++++ .../views/sale_order_views.xml | 18 ++++++ .../views/stock_picking_views.xml | 20 +++++++ 16 files changed, 337 insertions(+) create mode 100644 account_credit_hold/__init__.py create mode 100644 account_credit_hold/__manifest__.py create mode 100644 account_credit_hold/models/__init__.py create mode 100644 account_credit_hold/models/account_followup.py create mode 100644 account_credit_hold/models/account_followup_report.py create mode 100644 account_credit_hold/models/res_partner.py create mode 100644 account_credit_hold/models/sale_order.py create mode 100644 account_credit_hold/models/stock_picking.py create mode 100644 account_credit_hold/readme.md create mode 100644 account_credit_hold/static/src/js/followup_form_controller.js create mode 100644 account_credit_hold/static/src/js/followup_form_model.js create mode 100644 account_credit_hold/static/src/xml/account_followup_template.xml create mode 100644 account_credit_hold/views/account_followup_views.xml create mode 100644 account_credit_hold/views/res_partner_views.xml create mode 100644 account_credit_hold/views/sale_order_views.xml create mode 100644 account_credit_hold/views/stock_picking_views.xml diff --git a/account_credit_hold/__init__.py b/account_credit_hold/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/account_credit_hold/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_credit_hold/__manifest__.py b/account_credit_hold/__manifest__.py new file mode 100644 index 0000000..564b9bd --- /dev/null +++ b/account_credit_hold/__manifest__.py @@ -0,0 +1,30 @@ +{ + 'name': 'Account Credit Hold', + 'version': '15.0.2.0.0.1', + 'summary': 'Allows setting clients on credit hold, blocking the ability confirm a new sales order.', + 'description': 'Allows setting clients on hold, blocking the ability confirm a new sales order.', + 'category': 'Accounting/Accounting', + 'author': 'Bemade Inc.', + 'maintainer': 'Marc Durepos ', + 'website': 'http://www.bemade.org', + 'license': 'LGPL-3', + 'depends': ['sale', 'account_followup', 'stock'], + 'data': [ + 'views/account_followup_views.xml', + 'views/sale_order_views.xml', + 'views/res_partner_views.xml', + 'views/stock_picking_views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'account_credit_hold/static/src/js/followup_form_model.js', + 'account_credit_hold/static/src/js/followup_form_controller.js', + ], + 'web.assets_qweb': [ + 'account_credit_hold/static/src/xml/account_followup_template.xml', + ], + }, + 'demo': [], + 'installable': True, + 'auto_install': False +} diff --git a/account_credit_hold/models/__init__.py b/account_credit_hold/models/__init__.py new file mode 100644 index 0000000..0c2c11c --- /dev/null +++ b/account_credit_hold/models/__init__.py @@ -0,0 +1,5 @@ +from . import account_followup +from . import res_partner +from . import sale_order +from . import stock_picking +from . import account_followup_report diff --git a/account_credit_hold/models/account_followup.py b/account_credit_hold/models/account_followup.py new file mode 100644 index 0000000..5af3363 --- /dev/null +++ b/account_credit_hold/models/account_followup.py @@ -0,0 +1,8 @@ +from odoo import fields, models, api + + +class FollowupLine(models.Model): + _inherit = 'account_followup.followup.line' + + account_hold = fields.Boolean(string="Place on Credit Hold", + help="Place clients on account hold, restricting confirmation of new orders.") diff --git a/account_credit_hold/models/account_followup_report.py b/account_credit_hold/models/account_followup_report.py new file mode 100644 index 0000000..9c9b852 --- /dev/null +++ b/account_credit_hold/models/account_followup_report.py @@ -0,0 +1,14 @@ +from odoo import models, fields, api, _ + +class FollowUpReport(models.AbstractModel): + _inherit = 'account.followup.report' + + def _get_line_info(self, followup_line): + res = super()._get_line_info(followup_line) + res.update({'credit_hold': followup_line.account_hold}) + return res + @api.model + def credit_hold(self, options): + partner_id = options['partner_id'] + partner = self.env['res.partner'].browse(partner_id) + partner.action_credit_hold() diff --git a/account_credit_hold/models/res_partner.py b/account_credit_hold/models/res_partner.py new file mode 100644 index 0000000..f7fbded --- /dev/null +++ b/account_credit_hold/models/res_partner.py @@ -0,0 +1,59 @@ +from odoo import fields, models, api, _ +from datetime import date + + +class Partner(models.Model): + _inherit = 'res.partner' + + postpone_hold_until = fields.Date(string="Postpone Hold", + help="Grace period specific to this partner despite unpaid invoices.", ) + + hold_bg = fields.Boolean(string="Hold (technical)", + compute="_compute_hold_bg", + store=True, + default=False) + on_hold = fields.Boolean(string="Account on Hold", + help="Client account is on hold for unpaid overdue invoices.", + compute="_compute_on_hold") + + @api.depends('postpone_hold_until', 'hold_bg') + def _compute_on_hold(self): + # manually re-compute hold_bg since followup_status doesn't get updated in Python but gets recalculated + # by an SQL query every time + self._compute_hold_bg() + for rec in self: + if rec.hold_bg and not (rec.postpone_hold_until and rec.postpone_hold_until > date.today()): + rec.on_hold = True + else: + rec.on_hold = False + + @api.autovacuum + def _cleanup_expired_hold_postponements(self): + expired_holds = self.search([('postpone_hold_until', '<=', date.today())]) + expired_holds.write({'postpone_hold_until': False}) + + def action_credit_hold(self): + message = _('Placed on credit hold') + for rec in self: + rec.hold_bg = True + rec.message_post() + + def _execute_followup_partner(self): + res = super()._execute_followup_partner() + if self.followup_status == 'in_need_of_action': + if self.followup_line_id.account_hold: + self.action_credit_hold() + return res + + # BV: FOR MIGRATION + #@api.depends('followup_status', 'followup_level') + def _compute_hold_bg(self): + first_followup_level = self.env['account_followup.followup.line'].search( + [('company_id', '=', self.env.company.id)], order="delay asc", limit=1) + for rec in self: + prev_hold_bg = rec.hold_bg + level = rec.followup_line_id + if rec.followup_status == 'no_action_needed' and level == first_followup_level: + rec.hold_bg = False + else: + rec.hold_bg = prev_hold_bg diff --git a/account_credit_hold/models/sale_order.py b/account_credit_hold/models/sale_order.py new file mode 100644 index 0000000..390e97e --- /dev/null +++ b/account_credit_hold/models/sale_order.py @@ -0,0 +1,17 @@ +from odoo import fields, models, api, _ +from odoo.exceptions import UserError + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + client_on_hold = fields.Boolean(string='Client on Hold', + help="Whether or not a client has been put on hold due to unpaid invoices.", + related="partner_id.on_hold") + + @api.depends('client_on_hold') + def action_confirm(self): + if any(self.mapped('client_on_hold')): + raise UserError(_("This client is on credit hold. No new orders can be confirmed until past-due invoices " + "are paid or the accounting team postpones the hold.")) + super().action_confirm() diff --git a/account_credit_hold/models/stock_picking.py b/account_credit_hold/models/stock_picking.py new file mode 100644 index 0000000..0d46e32 --- /dev/null +++ b/account_credit_hold/models/stock_picking.py @@ -0,0 +1,9 @@ +from odoo import fields, models, api + + +class ModelName(models.Model): + _inherit = "stock.picking" + + client_on_hold = fields.Boolean(string='Client on Hold', + help="Whether or not a client has been put on hold due to unpaid invoices.", + related="partner_id.on_hold") diff --git a/account_credit_hold/readme.md b/account_credit_hold/readme.md new file mode 100644 index 0000000..b11668c --- /dev/null +++ b/account_credit_hold/readme.md @@ -0,0 +1,35 @@ +# Overview + +This module adds the notion of placing clients on credit hold to the followup levels from the Odoo Enterprise +account_followup module. It adds an option to followup levels to mark clients matching the followup criteria as on +credit hold. This hold restricts the confirmation of new sales orders for these clients. + +Accountant and admin users can set a date until which the account hold will be +postponed on a specific partner's form view. This effectively gives clients an extra +grace period, allowing orders to be confirmed until the period ends. + +# Change Log +## 15.0.2.0.0 (2023-05-04) + +Complete remake of the module, making the "Credit Hold" an action that is either manually or +automatically triggered from the Accounting > Followup Reports section or by setting the automatic application field +on followup levels. + +## 15.0.1.1.0 (2023-05-03) + +Adds a ribbon to stock pickings for clients on hold, and therefore a dependency on stock. + +## 15.0.1.0.2 (2023-05-03) + +Fix to sale order view and sale order confirmation for clients not on hold. + +## 15.0.1.0.1 (2023-05-03) + +Fix clients on hold when status is "outstanding_invoices". + +## 15.0.1.0.0 (2023-05-02) Initial Release + +Initial release of the module, including a setting on follow-up levels to toggle placing on credit hold. Blocks +the confirmation of sales orders for clients on credit hold. Red "Credit Hold" banner appears on sales orders and +partner form view when a client is on credit hold. Credit hold can be postponed by setting the "Postpone Hold" field +on the partner form view. \ No newline at end of file diff --git a/account_credit_hold/static/src/js/followup_form_controller.js b/account_credit_hold/static/src/js/followup_form_controller.js new file mode 100644 index 0000000..39b26d0 --- /dev/null +++ b/account_credit_hold/static/src/js/followup_form_controller.js @@ -0,0 +1,48 @@ +/** @odoo-module **/ + +var FollowupFormController = require('account_followup.FollowupFormController'); +import { patch } from '@web/core/utils/patch'; + +var PatchedController = patch(FollowupFormController.prototype, "followup_form_controller", { + events: _.extend({}, FollowupFormController.prototype.events, { + 'click .o_account_followup_credit_hold_button': '_onCreditHold', + }), + updateButtons() { + this._super(...arguments); + let setButtonClass = (button, primary) => { + /* Set class 'btn-primary' if parameter `primary` is true + * 'btn-secondary' otherwise + */ + let addedClass = primary ? 'btn-primary' : 'btn-secondary' + let removedClass = !primary ? 'btn-secondary' : 'btn-primary' + this.$buttons.find(`button.${button}`) + .removeClass(removedClass).addClass(addedClass); + } + if (!this.$buttons) { + return; + } + let followupLevel = this.model.localData[this.handle].data.followup_level; + setButtonClass('o_account_followup_credit_hold_button', followupLevel.credit_hold); + }, + _onCreditHold: function() { + var self = this; + this.model.doCreditHold(this.handle); + this.options = { + partner_id: this._getPartner() + }; + this._rpc({ + model: 'account.followup.report', + method: 'credit_hold', + args: [this.options], + }).then(function (result) { + self._removeHighlightCreditHold(); + self._displayDone(); + }); + }, + _removeHighlightCreditHold: function() { + this.$buttons.find('button.o_account_followup_credit_hold_button') + .removeClass('btn-primary').addClass('btn-secondary'); + }, +}); + +export { PatchedController }; \ No newline at end of file diff --git a/account_credit_hold/static/src/js/followup_form_model.js b/account_credit_hold/static/src/js/followup_form_model.js new file mode 100644 index 0000000..bd84e76 --- /dev/null +++ b/account_credit_hold/static/src/js/followup_form_model.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +var FollowupFormModel = require('account_followup.FollowupFormModel'); +import { patch } from '@web/core/utils/patch'; + +var PatchedModel = patch(FollowupFormModel.prototype, 'followup_form_model', { + doCreditHold: function(handle) { + var level = this.localData[handle].data.followup_level; + if(level && level.credit_hold) { + level.credit_hold = false; + } + }, +}); +export { PatchedModel }; \ No newline at end of file diff --git a/account_credit_hold/static/src/xml/account_followup_template.xml b/account_credit_hold/static/src/xml/account_followup_template.xml new file mode 100644 index 0000000..50f74ee --- /dev/null +++ b/account_credit_hold/static/src/xml/account_followup_template.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/account_credit_hold/views/account_followup_views.xml b/account_credit_hold/views/account_followup_views.xml new file mode 100644 index 0000000..61caad9 --- /dev/null +++ b/account_credit_hold/views/account_followup_views.xml @@ -0,0 +1,16 @@ + + + + + + account_credit_hold.account_followup_line.form + account_followup.followup.line + + + + + + + + + \ No newline at end of file diff --git a/account_credit_hold/views/res_partner_views.xml b/account_credit_hold/views/res_partner_views.xml new file mode 100644 index 0000000..4e86f5e --- /dev/null +++ b/account_credit_hold/views/res_partner_views.xml @@ -0,0 +1,33 @@ + + + + + + account_credit_hold.res_partner.form + res.partner + + + + + + + + + + + account_credit_hold.view_partner_property_form + res.partner + + + + + + + + + + + \ No newline at end of file diff --git a/account_credit_hold/views/sale_order_views.xml b/account_credit_hold/views/sale_order_views.xml new file mode 100644 index 0000000..bf8af69 --- /dev/null +++ b/account_credit_hold/views/sale_order_views.xml @@ -0,0 +1,18 @@ + + + + + + account_credit_hold.sale_order.form + sale.order + + + + + + + + + + \ No newline at end of file diff --git a/account_credit_hold/views/stock_picking_views.xml b/account_credit_hold/views/stock_picking_views.xml new file mode 100644 index 0000000..2b96e89 --- /dev/null +++ b/account_credit_hold/views/stock_picking_views.xml @@ -0,0 +1,20 @@ + + + + + + account_credit_hold.stock_picking.form + stock.picking + + + + + + + + + + + + \ No newline at end of file From 1583e4c324e3320cbfdd4c7a1e0268d163d4d752 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Mon, 18 Dec 2023 16:00:49 -0500 Subject: [PATCH 14/19] account_credit_hold migrated to 16.0 --- account_credit_hold/__manifest__.py | 11 +---- .../models/account_followup_report.py | 10 ++-- account_credit_hold/models/res_partner.py | 16 ++++--- .../static/src/js/followup_form_controller.js | 48 ------------------- .../static/src/js/followup_form_model.js | 14 ------ .../src/xml/account_followup_template.xml | 10 ---- .../views/account_followup_views.xml | 38 ++++++++++++++- 7 files changed, 52 insertions(+), 95 deletions(-) delete mode 100644 account_credit_hold/static/src/js/followup_form_controller.js delete mode 100644 account_credit_hold/static/src/js/followup_form_model.js delete mode 100644 account_credit_hold/static/src/xml/account_followup_template.xml diff --git a/account_credit_hold/__manifest__.py b/account_credit_hold/__manifest__.py index 564b9bd..0212a4d 100644 --- a/account_credit_hold/__manifest__.py +++ b/account_credit_hold/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Account Credit Hold', - 'version': '15.0.2.0.0.1', + 'version': '16.0.1.0.0', 'summary': 'Allows setting clients on credit hold, blocking the ability confirm a new sales order.', 'description': 'Allows setting clients on hold, blocking the ability confirm a new sales order.', 'category': 'Accounting/Accounting', @@ -15,15 +15,6 @@ 'views/res_partner_views.xml', 'views/stock_picking_views.xml', ], - 'assets': { - 'web.assets_backend': [ - 'account_credit_hold/static/src/js/followup_form_model.js', - 'account_credit_hold/static/src/js/followup_form_controller.js', - ], - 'web.assets_qweb': [ - 'account_credit_hold/static/src/xml/account_followup_template.xml', - ], - }, 'demo': [], 'installable': True, 'auto_install': False diff --git a/account_credit_hold/models/account_followup_report.py b/account_credit_hold/models/account_followup_report.py index 9c9b852..a28efdc 100644 --- a/account_credit_hold/models/account_followup_report.py +++ b/account_credit_hold/models/account_followup_report.py @@ -1,14 +1,12 @@ from odoo import models, fields, api, _ + class FollowUpReport(models.AbstractModel): _inherit = 'account.followup.report' def _get_line_info(self, followup_line): res = super()._get_line_info(followup_line) - res.update({'credit_hold': followup_line.account_hold}) + res.update({ + 'credit_hold': followup_line.account_hold + }) return res - @api.model - def credit_hold(self, options): - partner_id = options['partner_id'] - partner = self.env['res.partner'].browse(partner_id) - partner.action_credit_hold() diff --git a/account_credit_hold/models/res_partner.py b/account_credit_hold/models/res_partner.py index f7fbded..3cc765d 100644 --- a/account_credit_hold/models/res_partner.py +++ b/account_credit_hold/models/res_partner.py @@ -25,6 +25,8 @@ class Partner(models.Model): if rec.hold_bg and not (rec.postpone_hold_until and rec.postpone_hold_until > date.today()): rec.on_hold = True else: + if rec.on_hold: + rec.message_post(_("Credit hold lifted.")) rec.on_hold = False @api.autovacuum @@ -33,10 +35,14 @@ class Partner(models.Model): expired_holds.write({'postpone_hold_until': False}) def action_credit_hold(self): - message = _('Placed on credit hold') for rec in self: rec.hold_bg = True - rec.message_post() + rec.message_post(body=_('Placed on credit hold.')) + + def action_lift_credit_hold(self): + for rec in self: + rec.hold_bg = False + rec.message_post(body=_('Credit hold lifted.')) def _execute_followup_partner(self): res = super()._execute_followup_partner() @@ -45,11 +51,9 @@ class Partner(models.Model): self.action_credit_hold() return res - # BV: FOR MIGRATION - #@api.depends('followup_status', 'followup_level') + @api.depends('followup_status', 'followup_line_id') def _compute_hold_bg(self): - first_followup_level = self.env['account_followup.followup.line'].search( - [('company_id', '=', self.env.company.id)], order="delay asc", limit=1) + first_followup_level = self._get_first_followup_level() for rec in self: prev_hold_bg = rec.hold_bg level = rec.followup_line_id diff --git a/account_credit_hold/static/src/js/followup_form_controller.js b/account_credit_hold/static/src/js/followup_form_controller.js deleted file mode 100644 index 39b26d0..0000000 --- a/account_credit_hold/static/src/js/followup_form_controller.js +++ /dev/null @@ -1,48 +0,0 @@ -/** @odoo-module **/ - -var FollowupFormController = require('account_followup.FollowupFormController'); -import { patch } from '@web/core/utils/patch'; - -var PatchedController = patch(FollowupFormController.prototype, "followup_form_controller", { - events: _.extend({}, FollowupFormController.prototype.events, { - 'click .o_account_followup_credit_hold_button': '_onCreditHold', - }), - updateButtons() { - this._super(...arguments); - let setButtonClass = (button, primary) => { - /* Set class 'btn-primary' if parameter `primary` is true - * 'btn-secondary' otherwise - */ - let addedClass = primary ? 'btn-primary' : 'btn-secondary' - let removedClass = !primary ? 'btn-secondary' : 'btn-primary' - this.$buttons.find(`button.${button}`) - .removeClass(removedClass).addClass(addedClass); - } - if (!this.$buttons) { - return; - } - let followupLevel = this.model.localData[this.handle].data.followup_level; - setButtonClass('o_account_followup_credit_hold_button', followupLevel.credit_hold); - }, - _onCreditHold: function() { - var self = this; - this.model.doCreditHold(this.handle); - this.options = { - partner_id: this._getPartner() - }; - this._rpc({ - model: 'account.followup.report', - method: 'credit_hold', - args: [this.options], - }).then(function (result) { - self._removeHighlightCreditHold(); - self._displayDone(); - }); - }, - _removeHighlightCreditHold: function() { - this.$buttons.find('button.o_account_followup_credit_hold_button') - .removeClass('btn-primary').addClass('btn-secondary'); - }, -}); - -export { PatchedController }; \ No newline at end of file diff --git a/account_credit_hold/static/src/js/followup_form_model.js b/account_credit_hold/static/src/js/followup_form_model.js deleted file mode 100644 index bd84e76..0000000 --- a/account_credit_hold/static/src/js/followup_form_model.js +++ /dev/null @@ -1,14 +0,0 @@ -/** @odoo-module **/ - -var FollowupFormModel = require('account_followup.FollowupFormModel'); -import { patch } from '@web/core/utils/patch'; - -var PatchedModel = patch(FollowupFormModel.prototype, 'followup_form_model', { - doCreditHold: function(handle) { - var level = this.localData[handle].data.followup_level; - if(level && level.credit_hold) { - level.credit_hold = false; - } - }, -}); -export { PatchedModel }; \ No newline at end of file diff --git a/account_credit_hold/static/src/xml/account_followup_template.xml b/account_credit_hold/static/src/xml/account_followup_template.xml deleted file mode 100644 index 50f74ee..0000000 --- a/account_credit_hold/static/src/xml/account_followup_template.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/account_credit_hold/views/account_followup_views.xml b/account_credit_hold/views/account_followup_views.xml index 61caad9..10a7165 100644 --- a/account_credit_hold/views/account_followup_views.xml +++ b/account_credit_hold/views/account_followup_views.xml @@ -8,9 +8,45 @@ - + + + + customer.statements.form.view.inherit + res.partner + + + + + + + + + + + + + \ No newline at end of file diff --git a/bemade_packing_wizard/__manifest__.py b/bemade_packing_wizard/__manifest__.py index 41b136d..124c309 100644 --- a/bemade_packing_wizard/__manifest__.py +++ b/bemade_packing_wizard/__manifest__.py @@ -1,12 +1,12 @@ { 'name': 'Packing wizard', - 'version': '15.0.1.0.0', + 'version': '16.0.1.0.0', 'category': 'Extra Tools', 'summary': 'Allow automated packing type creation', 'description': """ This module allows users simply enter all the package dimensions on every packing and it will create the packing type automatically if it not exists, use it the create the package with the weight and related package type. - + You need to activate the auto create package feature on the delivery carrier to use this feature. """, 'depends': [ From e0bde4dcbc49eac8372088e4ba8321db8a673fc5 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Wed, 3 Jan 2024 17:41:51 -0500 Subject: [PATCH 18/19] bemade_stock_quant_valuation to 16.0 --- bemade_stock_quant_valuation/__manifest__.py | 2 +- temporary_change_image_size/__manifest__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bemade_stock_quant_valuation/__manifest__.py b/bemade_stock_quant_valuation/__manifest__.py index 19ffd69..05a8fca 100644 --- a/bemade_stock_quant_valuation/__manifest__.py +++ b/bemade_stock_quant_valuation/__manifest__.py @@ -18,7 +18,7 @@ # { 'name': 'Stock Quant Valuation', - 'version': '15.0.1.0.0', + 'version': '16.0.1.0.0', 'summary': 'Adds valuation to stock quant for better inventory adjustment management', 'description': '', 'category': 'Inventory', diff --git a/temporary_change_image_size/__manifest__.py b/temporary_change_image_size/__manifest__.py index 54a9ac5..611f8cf 100644 --- a/temporary_change_image_size/__manifest__.py +++ b/temporary_change_image_size/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Temporary Change for Image Full Size', - 'version': '15.0.0.0.0', + 'version': '16.0.0.0.0', 'category': 'Extra Tools', 'summary': 'Temporary Change for Image Full Size', 'description': """ From 7f775779e3635406646732a522278fe61be5f2b0 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Wed, 3 Jan 2024 17:46:07 -0500 Subject: [PATCH 19/19] [MIG] bemade_partner_email_domain to 16.0 --- bemade_partner_email_domain/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bemade_partner_email_domain/__manifest__.py b/bemade_partner_email_domain/__manifest__.py index 5047c18..9f8fbe9 100644 --- a/bemade_partner_email_domain/__manifest__.py +++ b/bemade_partner_email_domain/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Automated Partner Association by Email Domain', - 'version': '15.0.0.0.0', + 'version': '16.0.0.0.0', 'category': 'Extra Tools', 'summary': 'Automatically associates partners with companies using matching email domains', 'description': """