Counted buttons
On a customer record you want to see “12 orders” and click through. The display is not the problem: the module that owns the customer record must know nothing about the sales module. A direct import between business modules is forbidden (rule 8), for a good reason: the day a customer uninstalls sales, the customer record must not break.
The consuming module declares the link on the other model, and neither knows the other.
from kewos.smart_buttons import has_smart_button, register_smart_button
def register(registry): ... if not has_smart_button("kws_partner", "invoices"): # boot re-imports register_smart_button( "kws_partner", # the record that will carry the button (core) "invoices", # its key, unique on that model label_key="account.smartbutton.partner_moves", target_model="account_move", # what is counted (account module) filter_field="partner_id", # the field pointing back at the record icon="receipt", )The account module writes those lines. core, which owns kws_partner, never mentions
account. Remove account and the button goes; the partner record does not move.
What the server returns
One generic endpoint, for any model:
GET /api/v1/library_title/1/smart-buttons
[{"key":"copies","label_key":"library.smartbutton.copies","icon":"book", "count":2,"target_model":"library_copy","filter_field":"title_id", "filter_value":1,"target_module":"library"}]The count is real (select count(*) from library_copy where title_id = 1 does return 2), and the
frontend needs to know nothing specific: with target_module, target_model, filter_field and
filter_value it can render the button and where the click leads. No screen is hardcoded
(rule 48).
The count is not a naive count(*)
It runs inside the filtered session, with record rules and the caller’s read permission. Two consequences worth holding on to:
- two users can see two different numbers on the same record, and that is correct: each counts what they may see. A salesperson who only sees their own orders reads their own total;
- the number does not leak what the list would hide. A global count would reveal that invisible records exist, which is an information leak even without showing their content.
The record itself is loaded through the filtered session before any counting: asking for the buttons of a record you may not read answers 404, not 403. Not being allowed and not existing look alike, deliberately.
GET /api/v1/library_title/9999/smart-buttons -> 404The groups field
register_smart_button(..., groups=["accounting_auditor"])It hides the button from everyone else. It is not a security control: it is screen tidying. The security is the filtered count and the 404 above, which hold even when somebody calls the endpoint by hand.