Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions spp_base_common/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"assets": {
"web.assets_backend": [
"spp_base_common/static/src/scss/navbar.scss",
"spp_base_common/static/src/js/custom_list_create.js",
"spp_base_common/static/src/xml/custom_list_create_template.xml",
],
"web._assets_primary_variables": [
"spp_base_common/static/src/scss/colors.scss",
Expand Down
25 changes: 25 additions & 0 deletions spp_base_common/static/src/js/custom_list_create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/** @odoo-module **/

import {ListController} from "@web/views/list/list_controller";
import {patch} from "@web/core/utils/patch";

/**
* Base handler for the generic custom create button.
* Modules override this method via patch to handle their specific model.
*
* Usage in other modules:
* patch(ListController.prototype, {
* async onCustomListCreate() {
* if (this.model.root.resModel === "my.model") {
* // open wizard
* return;
* }
* return super.onCustomListCreate(...arguments);
* },
* });
*/
patch(ListController.prototype, {
async onCustomListCreate() {
// Base no-op — overridden by consuming modules
},
});
Comment on lines +21 to +25

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The base onCustomListCreate is a no-op, which requires every consuming module to patch it with very similar logic (check model, call action). This pattern can be improved to be more data-driven, which would reduce boilerplate code in the modules that use it.

Consider moving the action-calling logic into this base method. The specific action to be called can then be defined as a property of the customListCreateButton configuration object. This would make the pattern more declarative and easier to use.

patch(ListController.prototype, {
    setup() {
        super.setup(...arguments);
        // Ensure actionService is available for onCustomListCreate and other potential patches.
        if (!this.actionService) {
            this.actionService = useService("action");
        }
    },
    async onCustomListCreate() {
        if (this.customListCreateButton?.action) {
            await this.actionService.doAction(this.customListCreateButton.action, {
                onClose: async () => {
                    await this.model.root.load();
                },
            });
        } else if (this.customListCreateButton) {
            // Fallback for modules that still override this method and don't provide an action.
            // This maintains backward compatibility with the original proposed pattern.
            return super.onCustomListCreate(...arguments);
        }
    },
});

46 changes: 46 additions & 0 deletions spp_base_common/static/src/xml/custom_list_create_template.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<!-- Generic extensible create button for ListView.
Any module can set this.customListCreateButton = {label, title, className}
and override onCustomListCreate() in their ListController patch
to replace the default "New" button with a custom one. -->
<t t-inherit="web.ListView" t-inherit-mode="extension">
<xpath expr="//Layout/t[@t-set-slot='control-panel-create-button']" position="replace">
<t t-set-slot="control-panel-create-button">
<t t-if="customListCreateButton">
<button
t-if="!editedRecord and activeActions.create and props.showButtons"
type="button"
t-att-class="'btn btn-primary ' + (customListCreateButton.className or '')"
data-hotkey="c"
t-att-title="customListCreateButton.title or ''"
t-on-click="onCustomListCreate"
t-esc="customListCreateButton.label"
/>
</t>
<t t-else="">
<button
t-if="!editedRecord and activeActions.create and props.showButtons"
type="button"
class="btn btn-primary o_list_button_add"
data-hotkey="c"
t-on-click="onClickCreate"
data-bounce-button=""
>New</button>
<t t-if="props.showButtons and !env.inDialog" t-call="web.ListView.EditableButtons" />
</t>
</t>
</xpath>
</t>

<!-- Generic extensible form create button hide.
Any module can set this.hideFormCreateButton = true in their
FormController patch to hide the default "New" button. -->
<t t-inherit="web.FormView" t-inherit-mode="extension">
<xpath expr="//button[hasclass('o_form_button_create')]" position="attributes">
<attribute name="t-if">!hideFormCreateButton</attribute>
</xpath>
</t>

</templates>
45 changes: 34 additions & 11 deletions spp_change_request_v2/static/src/js/create_change_request.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* @odoo-module */

import {FormController} from "@web/views/form/form_controller";
import {ListController} from "@web/views/list/list_controller";
import {onWillStart} from "@odoo/owl";
import {patch} from "@web/core/utils/patch";
Expand All @@ -11,25 +12,47 @@ patch(ListController.prototype, {
super.setup();
this.actionService = useService("action");
onWillStart(async () => {
if (this.model.root.resModel !== "spp.change.request") {
return;
}
const is_admin = await user.hasGroup("spp_security.group_spp_admin");
const is_cr_user = await user.hasGroup(
"spp_change_request_v2.group_cr_user"
);
this.canCreateChangeRequest = is_admin || is_cr_user;
if (is_admin || is_cr_user) {
this.customListCreateButton = {
label: "New Request",
title: "Create a New Change Request",
className: "o_list_button_add_cr",
};
}
});
},

async loadChangeRequestWizard() {
if (this.model.root.resModel !== "spp.change.request") {
/**
* Opens the Create Change Request wizard when the custom button is clicked.
*/
async onCustomListCreate() {
if (this.model.root.resModel === "spp.change.request") {
await this.actionService.doAction(
"spp_change_request_v2.action_cr_create_wizard",
{
onClose: async () => {
await this.model.root.load();
},
}
);
return;
}
await this.actionService.doAction(
"spp_change_request_v2.action_cr_create_wizard",
{
onClose: async () => {
await this.model.root.load();
},
}
);
return super.onCustomListCreate(...arguments);
},
});

patch(FormController.prototype, {
setup() {
super.setup();
if (this.props.resModel === "spp.change.request") {
this.hideFormCreateButton = true;
}
},
});
Original file line number Diff line number Diff line change
@@ -1,46 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-inherit="web.ListView" t-inherit-mode="extension">
<xpath
expr="//Layout/t[@t-set-slot='control-panel-create-button']"
position="replace"
>
<t t-set-slot="control-panel-create-button">
<t t-if="model.root.resModel == 'spp.change.request'">
<button
t-if="!editedRecord and activeActions.create and props.showButtons and canCreateChangeRequest"
type="button"
class="btn btn-primary o_list_button_add_cr"
accesskey="c"
title="Create a New Change Request"
t-on-click="loadChangeRequestWizard"
>New Request</button>
</t>
<t t-else="">
<button
t-if="!editedRecord and activeActions.create and props.showButtons"
type="button"
class="btn btn-primary o_list_button_add"
data-hotkey="c"
t-on-click="onClickCreate"
data-bounce-button=""
>New</button>
<t
t-if="props.showButtons and !env.inDialog"
t-call="web.ListView.EditableButtons"
/>
</t>
</t>
</xpath>
</t>

<t t-inherit="web.FormView" t-inherit-mode="extension">
<xpath expr="//button[hasclass('o_form_button_create')]" position="attributes">
<attribute
name="t-if"
>model.root.resModel != 'spp.change.request'</attribute>
</xpath>
</t>
<!-- No ListView/FormView slot replacement here.
The generic customListCreateButton pattern lives in spp_base_common.
See spp_base_common/static/src/xml/custom_list_create_template.xml -->

</templates>
78 changes: 62 additions & 16 deletions spp_programs/static/src/js/create_program.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,86 @@
/** @odoo-module **/

import {FormController} from "@web/views/form/form_controller";
import {ListController} from "@web/views/list/list_controller";
import {onWillStart} from "@odoo/owl";
import {patch} from "@web/core/utils/patch";
import {registry} from "@web/core/registry";
import {user} from "@web/core/user";
import {useService} from "@web/core/utils/hooks";

/**
* Client action to close modal and then open program form.
* This ensures proper sequencing with async/await.
*/
async function openProgramCloseModal(env, action) {
const actionService = env.services.action;
const programId = action.params?.program_id;

await actionService.doAction(
{type: "ir.actions.act_window_close"},
{clearBreadcrumbs: true}
);

if (programId) {
await actionService.doAction({
type: "ir.actions.act_window",
name: "Program",
res_model: "spp.program",
res_id: programId,
views: [[false, "form"]],
target: "current",
});
}
}

registry.category("actions").add("open_program_close_modal", openProgramCloseModal);

patch(ListController.prototype, {
setup() {
super.setup();
this.actionService = useService("action");
onWillStart(async () => {
// Check if user has permission to create programs
// Groups: spp_security.group_spp_admin OR spp_programs.group_programs_manager
if (this.model.root.resModel !== "spp.program") {
return;
}
const is_admin = await user.hasGroup("spp_security.group_spp_admin");
const is_program_manager = await user.hasGroup("spp_programs.group_programs_manager");
this.canCreateProgram = is_admin || is_program_manager;
const is_program_manager = await user.hasGroup(
"spp_programs.group_programs_manager"
);
if (is_admin || is_program_manager) {
this.customListCreateButton = {
label: "Create Program",
title: "Create a New Program",
className: "o_list_button_add_program",
};
}
});
},

/**
* Opens the Create Program wizard when the "Create Program" button is clicked.
* This is called from the custom button in the Programs list view.
* Opens the Create Program wizard when the custom button is clicked.
*/
async load_wizard() {
// Only proceed if we're on the spp.program model
if (this.model.root.resModel !== "spp.program") {
async onCustomListCreate() {
if (this.model.root.resModel === "spp.program") {
await this.actionService.doAction(
"spp_programs.action_create_program_wizard",
{
onClose: async () => {
await this.model.root.load();
},
}
);
return;
}
return super.onCustomListCreate(...arguments);
},
});
Comment on lines 38 to +77

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Following the suggestion to make the onCustomListCreate handler in spp_base_common data-driven, this patch can be greatly simplified. You can remove the onCustomListCreate override and instead provide the wizard's action name within the customListCreateButton configuration object. This change reduces boilerplate and centralizes the action-handling logic in the base module, making it easier to maintain.

patch(ListController.prototype, {
    setup() {
        super.setup(...arguments);
        this.actionService = useService("action");
        onWillStart(async () => {
            if (this.model.root.resModel !== "spp.program") {
                return;
            }
            const is_admin = await user.hasGroup("spp_security.group_spp_admin");
            const is_program_manager = await user.hasGroup(
                "spp_programs.group_programs_manager"
            );
            if (is_admin || is_program_manager) {
                this.customListCreateButton = {
                    label: "Create Program",
                    title: "Create a New Program",
                    className: "o_list_button_add_program",
                    action: "spp_programs.action_create_program_wizard",
                };
            }
        });
    },
});


// Open the create program wizard action
await this.actionService.doAction("spp_programs.action_create_program_wizard", {
onClose: async () => {
// Refresh the list after the wizard closes
await this.model.root.load();
},
});
patch(FormController.prototype, {
setup() {
super.setup();
if (this.props.resModel === "spp.program") {
this.hideFormCreateButton = true;
}
},
});
41 changes: 3 additions & 38 deletions spp_programs/static/src/xml/create_program_template.xml
Original file line number Diff line number Diff line change
@@ -1,43 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<!-- Extend the ListView control panel to add custom "Create Program" button
for the spp.program model while keeping default behavior for other models. -->
<t t-inherit="web.ListView" t-inherit-mode="extension">
<xpath expr="//Layout/t[@t-set-slot='control-panel-create-button']" position="replace">
<t t-set-slot="control-panel-create-button">
<!-- Custom Create Program button for spp.program model -->
<t t-if="model.root.resModel == 'spp.program'">
<button
t-if="!editedRecord and activeActions.create and props.showButtons and canCreateProgram"
type="button"
class="btn btn-primary o_list_button_add_program"
accesskey="c"
title="Create a New Program"
t-on-click="load_wizard"
>Create Program</button>
</t>
<!-- Default "New" button for all other models -->
<t t-else="">
<button
t-if="!editedRecord and activeActions.create and props.showButtons"
type="button"
class="btn btn-primary o_list_button_add"
data-hotkey="c"
t-on-click="onClickCreate"
data-bounce-button=""
>New</button>
<t t-if="props.showButtons and !env.inDialog" t-call="web.ListView.EditableButtons" />
</t>
</t>
</xpath>
</t>

<!-- Hide the default Create button in Form view for spp.program -->
<t t-inherit="web.FormView" t-inherit-mode="extension">
<xpath expr="//button[hasclass('o_form_button_create')]" position="attributes">
<attribute name="t-if">model.root.resModel != 'spp.program'</attribute>
</xpath>
</t>
<!-- No ListView/FormView slot replacement here.
The generic customListCreateButton pattern lives in spp_base_common.
See spp_base_common/static/src/xml/custom_list_create_template.xml -->

</templates>
Loading