3. Completing a screen
The two fields exist and the API returns them. They appear nowhere, because a view lists the
fields it shows, and pipeline’s does not know about ours.
The temptation is to declare our own pipeline_lead.form.json. That is the trap. A full
view replaces the module’s, so:
- you copy its thirty fields to add two;
- the next release of
pipelineadds a field, and your copy does not have it; - a second extension does the same, and you fight over the same slot.
The patch
A <model>.<kind>.patch.json file layers onto the existing view instead of replacing it.
modules/pipeline_rivals/views/pipeline_lead.form.patch.json:
{ "model": "pipeline_lead", "kind": "form", "module": "pipeline_rivals", "patch": [ { "op": "add", "field": { "name": "rivals_competitor_id" }, "after": "source_id", "section": "section.general" }, { "op": "add", "field": { "name": "rivals_won_by_id" }, "after": "lost_reason_id", "section": "section.general" } ]}Nothing to call: register_views_from_dir, which your module already calls for its own
screens, recognises the suffix and routes the file.
module is load-bearing. It becomes the registration’s scope key, which is what lets two
extensions complete the same screen without overwriting each other. An anonymous patch would
be indistinguishable from another one, so the kernel refuses it.
The available operations are add, hide, replace and move. Anchoring is before or
after an existing field, and section says which block of the form to place it in.
The same gesture on the list
modules/pipeline_rivals/views/pipeline_lead.list.patch.json:
{ "model": "pipeline_lead", "kind": "list", "module": "pipeline_rivals", "patch": [ { "op": "add", "field": { "name": "rivals_competitor_id" }, "before": "amount_weighted_amount" } ]}The labels
A field placed on a screen shows the key <model>.<field>. It is yours, even though the
model is not:
{ "module.pipeline_rivals": "Competitors", "model.rivals_competitor": "Competitors", "rivals_competitor.name": "Name", "rivals_competitor.tier": "Kind", "pipeline_lead.rivals_competitor_id": "Up against", "pipeline_lead.rivals_won_by_id": "Lost to"}pipeline will never translate your fields: they do not exist for it.
What it gives you
pipeline’s list, with one more column exactly where the anchor put it:

And the form, where “Up against” follows “Source” and “Lost to” follows “Lost reason”, inside
pipeline’s own “General” section:

The rest of the screen is untouched. This is not a lookalike copy: it is pipeline’s view,
with two operations applied as it is read.
Next
The module adds data and shows it. What remains is making it react, still without importing what it extends: Reacting, and leaving.