spp_base_common: the form "New" button ignores canCreate #582
Description
Module: spp_base_common, static/src/xml/custom_list_create_template.xml
Checked against: OpenSPP2 19.0 at 612b99b1d (2026-10-02) and at 86072ff8. The line is the same in both.
Severity: Medium. A form that is meant to be read-only still offers a working New button. Where ACLs allow create, that button lets a user create records outside the intended workflow.
Summary
spp_base_common extends web.FormView and replaces the t-if of the form's New button instead of adding to it:
<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>Stock Odoo 19 renders that button as t-if="canCreate" (web/static/src/views/form/form_controller.xml, line 9, the control-panel button). After the override, the button's visibility depends only on the opt-in hideFormCreateButton flag. Odoo's own create permission no longer counts.
Why this is a problem
canCreate (form_controller.js, this.canCreate = create && !this.props.preventCreate) is how Odoo hides New in all of these cases:
create: Falsein the action context. This is the standard way to open a form read-only:web/views/view.jsturns it intocreate="0"on the arch.create="0"on the form arch itself.- No create permission in the ACL.
activeActions.createis derived from the model's access rights. preventCreate, which the caller sets.
With the override in place, New renders in every one of these cases. Clicking it opens a blank, editable record of that model. So:
- If the user can create but the action deliberately disabled it (case 1 or 2), they can create a record from a screen that was designed to stop them. For example, a read-only "View" action on a registry record becomes a way to create new registrants, bypassing whatever workflow the form was locked down for (change requests, import, an approval flow).
- If the user cannot create (case 3), they get a button that opens an editable form and then fails with an
AccessErroron save. It is a confusing dead end.
This is not just about how the screen looks: the button is live and keyboard-reachable (data-hotkey="c").
Who and what are affected
- Every deployment with
spp_base_common. That is effectively all of them:spp_area,spp_approval,spp_change_request_v2,spp_programs,spp_event_data,spp_import_match,spp_cel_domain,spp_studio,spp_drims,spp_land_recordandspp_demoall depend on it. - Every form, on every model, opened with create disabled by context, arch or ACL. The override is not scoped to SPP models.
- The damage is hidden wherever
spp_programsis installed.spp_programs/static/src/js/form_controller_create.jspatchesFormControllerto hide.o_form_button_createwithstyle.display = "none", re-applied with timers and aMutationObserver, whencontext.create === falseorcanCreate === false. Its comment puts the blame on Odoo 19 ("t-if="canCreate"on the form template does not always prevent the 'New' button from rendering"). The cause is actually thespp_base_commonoverride, and stock Odoo hides the button correctly.- Deployments without
spp_programs(registry-only, change-request-only, farmer registry, and so on) show New on every read-only form. - Deployments with it depend on a DOM workaround. The button is still rendered and only hidden after mount.
- Deployments without
- History. Commit
7b8880b7b("resolve create button conflict between Programs and Change Request") moved the override intospp_base_common. The per-module templates it replaced had the same defect:spp_programsusedt-if="model.root.resModel != 'spp.program'"andspp_change_request_v2usedmodel.root.resModel != 'spp.change.request'. Both also droppedcanCreate. The bug has existed since those templates were introduced, and the consolidation spread it from two modules to every module that depends onspp_base_common.
How we found it
In one of our downstream deployments, a read-only registry form opened from an action with context="{'create': False, 'edit': False}" (a custom "view household" action and a post-import results screen) still rendered New, which opened a blank editable household. Users could create households outside the Change Request workflow. It reproduced on any database without spp_programs. That deployment happens to install spp_programs through other modules, so in production it was mostly hidden by the DOM workaround above.
Steps to reproduce
- Install
spp_registryandspp_base_common, withoutspp_programs. - Create a window action on
res.partner(or any model) withcontext="{'create': False}", and open a record with it. - Expected (stock Odoo): no New button. Actual: New is shown, and clicking it opens an editable blank record.
- Variant: log in as a user with read-only ACL on the model and open its form. New is shown, and saving fails with
AccessError.
Recommended fix
Keep Odoo's guard and add the opt-in flag to it (one line):
<t t-inherit="web.FormView" t-inherit-mode="extension">
<xpath expr="//button[hasclass('o_form_button_create')]" position="attributes">
<attribute name="t-if">canCreate and !hideFormCreateButton</attribute>
</xpath>
</t>- This keeps the extension point's documented contract: "any module can set
this.hideFormCreateButton = trueto hide the default New button". The flag can still only hide the button, never show it, and the modules that use it (spp_programscreate_program.js,spp_change_request_v2,spp_drims) keep working unchanged. - Follow-up (optional): once this lands, the
display:none/MutationObserverworkaround inspp_programs/static/src/js/form_controller_create.jsis no longer needed for thecontext.create === falseandcanCreate === falsecases. Only theMODELS_WITHOUT_CREATElist still needs handling, and that can be done by settingthis.hideFormCreateButton = truefor those models, the same way the other modules do. Its comment blaming Odoo 19 should be corrected either way. - Suggested regression test: a tour, or a
FormControllerunit test, that opens a form withcontext: {create: false}on an install withoutspp_programsand asserts no.o_form_button_createis in the DOM. Testing withoutspp_programsmatters: with it installed, its DOM patch makes the test pass even when the template is wrong.
What we did downstream (for reference)
Until this is fixed upstream, our downstream modules patch FormController.setup() to set this.hideFormCreateButton = true whenever !this.canCreate. This restores the stock rule without touching spp_base_common and never clears a flag another module set. We'd like to remove that patch once the template fix is in a release.
Activity
aldnav commentedon Oct 8, 2026
Any news @reichie020212 @gonzalesedwin1123 ?
gonzalesedwin1123 commentedon Oct 8, 2026
Sorry @aldnav @reichie020212 I did not notice this issue. I will work on this now
test: address review findings on the #582 regression guards
docs/test(spp_base_common): preventCreate was never exposed; assert t…
gonzalesedwin1123 commentedon Oct 8, 2026
Thanks @reichie020212, this is a precise report, and thanks for the hostile review on #588 as well. The analysis holds
up on every point I checked against
19.0and the Odoo 19 source, with one small correction below.What the verification turned up, in case it is useful downstream:
o_form_button_createbuttons: the one inweb.FormView(control panel) and the one inweb.FormView.Buttons(dialog footers). The extension targetsweb.FormViewonly, and client-side template inheritance takes the first xpath match inside that template, so formsopened in dialogs always kept
canCreate.preventCreateonly reachesFormControllerthroughFormViewDialog, which also turns the control panel off (form_view_dialog.js, andFormController.setupforcesdisplay.controlPanel = falsewheneverenv.inDialog). So that path never rendered the button. Action context,arch
create="0"and ACL were the real ones. Also, on stacks withspp_programsthe hidden button was nothotkey-reachable either: Odoo's hotkey service only considers visible elements. The exposure you describe applied
in full to stacks without
spp_programs.t-if="canCreate and !hideFormCreateButton". The flag can only hide the button;the three modules that set it (
spp_programs,spp_change_request_v2,spp_drims) are unchanged.spp_base_commongoes to19.0.2.0.3.test that applies the extension to the real stock template with Odoo's own
odoo.tools.template_inheritanceandasserts the resulting condition exactly; it fails on the old template. The Playwright suite (real Chromium) also
asserts
.o_form_button_createhas DOM count 0 on the change-request detail form the wizard opens withcreate: False(test 16) and on an approval review form as the CR HQ Validator (test 20). Count rather thanvisibility, for the reason you gave: with
spp_programsinstalled the DOM workaround only setsdisplay: none.still create through RPC, import or quick-create. Where a workflow must be the only way to create a record, the
enforcement belongs in ACLs and record rules; this fix restores the UI to match them.
spp_programsworkaround (form_controller_create.js: drop thecontext.createandcanCreatebranches, expressMODELS_WITHOUT_CREATEthrough the flag, fix the comment) and thespp_drimscommentis spp_programs: retire the DOM workaround for the form "New" button now that #582 keeps canCreate #589. Your downstream
FormController.setuppatch becomes a no-op once this ships and can be removed.Merge pull request #588 from OpenSPP/fix-582-form-create-button-cancr…