Skip to content

spp_base_common: the form "New" button ignores canCreate #582

Closed

Description

@reichie020212

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:

  1. create: False in the action context. This is the standard way to open a form read-only: web/views/view.js turns it into create="0" on the arch.
  2. create="0" on the form arch itself.
  3. No create permission in the ACL. activeActions.create is derived from the model's access rights.
  4. 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 AccessError on 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_record and spp_demo all 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_programs is installed. spp_programs/static/src/js/form_controller_create.js patches FormController to hide .o_form_button_create with style.display = "none", re-applied with timers and a MutationObserver, when context.create === false or canCreate === 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 the spp_base_common override, 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.
  • History. Commit 7b8880b7b ("resolve create button conflict between Programs and Change Request") moved the override into spp_base_common. The per-module templates it replaced had the same defect: spp_programs used t-if="model.root.resModel != 'spp.program'" and spp_change_request_v2 used model.root.resModel != 'spp.change.request'. Both also dropped canCreate. The bug has existed since those templates were introduced, and the consolidation spread it from two modules to every module that depends on spp_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

  1. Install spp_registry and spp_base_common, without spp_programs.
  2. Create a window action on res.partner (or any model) with context="{'create': False}", and open a record with it.
  3. Expected (stock Odoo): no New button. Actual: New is shown, and clicking it opens an editable blank record.
  4. 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 = true to hide the default New button". The flag can still only hide the button, never show it, and the modules that use it (spp_programs create_program.js, spp_change_request_v2, spp_drims) keep working unchanged.
  • Follow-up (optional): once this lands, the display:none / MutationObserver workaround in spp_programs/static/src/js/form_controller_create.js is no longer needed for the context.create === false and canCreate === false cases. Only the MODELS_WITHOUT_CREATE list still needs handling, and that can be done by setting this.hideFormCreateButton = true for 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 FormController unit test, that opens a form with context: {create: false} on an install without spp_programs and asserts no .o_form_button_create is in the DOM. Testing without spp_programs matters: 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

  1. aldnav commented on Oct 8, 2026

    @aldnav
    Contributor
  2. gonzalesedwin1123 commented on Oct 8, 2026

    @gonzalesedwin1123
    Member

    Sorry @aldnav @reichie020212 I did not notice this issue. I will work on this now

  3. gonzalesedwin1123 commented on Oct 8, 2026

    @gonzalesedwin1123
    Member

    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.0 and the Odoo 19 source, with one small correction below.

    What the verification turned up, in case it is useful downstream:

    • Only the control-panel button was affected. Stock Odoo has two o_form_button_create buttons: the one in
      web.FormView (control panel) and the one in web.FormView.Buttons (dialog footers). The extension targets
      web.FormView only, and client-side template inheritance takes the first xpath match inside that template, so forms
      opened in dialogs always kept canCreate.
    • Three of the four paths were exposed, not four. preventCreate only reaches FormController through
      FormViewDialog, which also turns the control panel off (form_view_dialog.js, and FormController.setup forces
      display.controlPanel = false whenever env.inDialog). So that path never rendered the button. Action context,
      arch create="0" and ACL were the real ones. Also, on stacks with spp_programs the hidden button was not
      hotkey-reachable either: Odoo's hotkey service only considers visible elements. The exposure you describe applied
      in full to stacks without spp_programs.
    • Fix is the one you proposed: 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_common goes to 19.0.2.0.3.
    • Tests. Tours and Hoot tests do not run in this repo's CI (no Chrome in the test image), so the gate is a Python
      test that applies the extension to the real stock template with Odoo's own odoo.tools.template_inheritance and
      asserts the resulting condition exactly; it fails on the old template. The Playwright suite (real Chromium) also
      asserts .o_form_button_create has DOM count 0 on the change-request detail form the wizard opens with
      create: False (test 16) and on an approval review form as the CR HQ Validator (test 20). Count rather than
      visibility, for the reason you gave: with spp_programs installed the DOM workaround only sets display: none.
    • One framing note. The button was a UI affordance, not a security boundary: a user whose ACL allows create can
      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.
    • Follow-up for the spp_programs workaround (form_controller_create.js: drop the context.create and
      canCreate branches, express MODELS_WITHOUT_CREATE through the flag, fix the comment) and the spp_drims comment
      is spp_programs: retire the DOM workaround for the form "New" button now that #582 keeps canCreate #589. Your downstream FormController.setup patch becomes a no-op once this ships and can be removed.
  4. added a commit that references this issue on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions