model actions

This commit is contained in:
Antoine Vandevenne (anv) 2025-02-19 11:47:21 +01:00
parent df1e582b93
commit 5fcfde48a4
8 changed files with 216 additions and 62 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -1131,10 +1131,9 @@ accessed directly using :code:`record.field`.
.. code-block:: python
:caption: `real_estate_property.py`
:emphasize-lines: 1,5-27
:emphasize-lines: 1,4-26
address_id = fields.Many2one(string="Address", comodel_name='res.partner')
[...]
@api.model_create_multi
@ -1174,43 +1173,61 @@ preconfigured tasks.
We have already seen how to :ref:`link menu items to XML-defined window actions
<tutorials/server_framework_101/define_window_actions>`. To link a **button** to an XML-defined
action, a `button` element must be added to the view, with its `type` attribute set to `action`. The
`name` attribute should reference the XML ID of the action to be executed, following the format
action, add a `button` element to the view, with its `type` attribute set to `action`. Use the
`name` attribute to reference the XML ID of the action to execute, following the format
`%(XML_ID)d`.
.. example::
In the following example, a button is added to the product form view to display all products in
the same category.
In the following example, a button is added to the product form view to display all other
products in the same category.
.. code-block:: xml
<form>
<sheet>
<div name="button_box">
<button
string="Similar Products"
type="action"
name="product.view_products_action"
context="{'search_default_category_id': category_id.id, 'create': False, 'edit': False}"
/>
</div>
</sheet>
</form>
<record id="view_similar_products_action" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="res_model">product</field>
<field name="domain">
[("id", "!=", active_id), ("category_id", "=", context.get('current_category_id'))]
</field>
<field name="view_mode">list,form</field>
</record>
<record id="product_form" model="ir.ui.view">
<form>
<sheet>
<div name="button_box">
<button
string="Similar Products"
type="action"
name="product.view_similar_products_action"
context="{
'current_category_id': category_id,
'create': False,
'edit': False,
}"
/>
</div>
</sheet>
</form>
</record>
.. note::
- The button is placed at the top of the form view by using a button container (`button_box`).
- The `context` attribute is used to:
- Filter the products to display only those in the same category as the current product.
- Pass the current category ID to the action which filters out products from other
categories.
- Prevent users from creating or editing products when browsing them through the button.
.. seealso::
Reference documentation on :ref:`button containers
<reference/view_architectures/form/button_container>`.
- Reference documentation on :ref:`button containers
<reference/view_architectures/form/button_container>`.
- Reference documentation on :ref:`environment variables for Python expressions in views
<reference/view_architectures/python_expression>`.
.. exercise::
Replace the property form view's :guilabel:`Offers` notebook page with a **stat button**. This
button should:
Replace the :guilabel:`Offers` notebook page in the property form view with a **stat button**.
This button should:
- Be placed at the top of the property form view.
- Display the total number of offers for the property.
@ -1222,22 +1239,21 @@ action, a `button` element must be added to the view, with its `type` attribute
<reference/view_architectures/form/button>` in form views.
- Find icon codes (`fa-<something>`) in the `Font Awesome v4 catalog
<https://fontawesome.com/v4/icons/>`_.
- Ensure that your count computations :ref:`scale with the number of records to process
<performance/good_practices/batch>`.
- Assign the `default_<field>` context key to a button to define default values when creating
new records opened through that button.
- Ensure your count computations :ref:`remain efficient as the number of records to process
grow <performance/good_practices/batch>`.
- Use the `default_<field>` context key to set default values when creating new records
through that button.
.. spoiler:: Solution
.. code-block:: python
:caption: `real_estate_property.py`
:emphasize-lines: 4,8-15
:emphasize-lines: 4,7-14
offer_ids = fields.One2many(
string="Offers", comodel_name='real.estate.offer', inverse_name='property_id'
)
offer_count = fields.Integer(string="Offer Count", compute='_compute_offer_count')
[...]
@api.depends('offer_ids')
@ -1272,21 +1288,26 @@ action, a `button` element must be added to the view, with its `type` attribute
.. code-block:: xml
:caption: `real_estate_property_views.xml`
:emphasize-lines: 2-12
:emphasize-lines: 4-14
<sheet>
<div name="button_box" class="oe_button_box">
<button
string="Offers"
icon="fa-handshake-o"
type="action"
name="real_estate.view_offers_action"
context="{'default_property_id': id}"
>
<field string="Offers" name="offer_count" widget="statinfo"/>
</button>
</div>
<record id="real_estate.property_form" model="ir.ui.view">
[...]
<sheet>
<div name="button_box">
<button
string="Offers"
icon="fa-handshake-o"
type="action"
name="real_estate.view_offers_action"
context="{'default_property_id': id}"
>
<field string="Offers" name="offer_count" widget="statinfo"/>
</button>
</div>
[...]
</sheet>
[...]
</record>
.. _tutorials/server_framework_101/model_actions:
@ -1298,38 +1319,169 @@ methods that execute custom business logic. These methods enable more complex wo
processing the current records, configuring client actions depending on these records, or
integrating with external systems.
To link a button to a model-defined action, its `type` attribute must be set to `object`, and its
`name` attribute must be set to the name of the model method to call when the button is clicked. The
method receives the current recordset through `self` and should return a dictionary acting as an
action descriptor.
To link a button to a model-defined action, set its `type` attribute to `object`, and use the `name`
attribute to specify the model method that should be called when the button is clicked. The method
receives the current recordset through `self` and should return a value indicating the result of the
action.
.. example::
In the following example,
In the following example, a button is added to the product category form view to ensure that all
products in the category have a positive margin.
.. code-block:: xml
<button string="Ensure Positive Margins" type="object" name="action_ensure_positive_margins"/>
.. code-block:: python
def action_ensure_positive_margins(self):
self.product_ids.filtered(lambda p: p.margin <= 0).margin = 0
return True
.. note::
- Action methods should be public :dfn:`not prefixed with an underscore` to make them callable
by the client. Such methods should always return something as they are automatically part of
the :doc:`external API <../../reference/external_api>`.
Action methods should be public :dfn:`not prefixed with an underscore` to make them callable
by the client. Since these methods are automatically exposed in the :doc:`external API
<../../reference/external_api>`, they should always return a meaningful value. In case of
doubt, return `True`.
.. exercise::
#. tmp ... in the header.
#. Add a button next to the :guilabel:`Salesperson` field of properties to assign the current
user as the salesperson.
#. Add two buttons for each offer in the offer list view.
- :guilabel:`Accept`: Accepts the offer, which changes the property state to :guilabel:`Under
Option`, and automatically refuses other offers.
- :guilabel:`Refuse`: Rejects only that offer.
#. Add a button in the header of the property form view to remove the property listing. The
button should:
#. Display a confirmation warning.
#. Refuse all offers.
#. Set the property to a new :guilabel:`Cancelled` state.
#. Archive the property.
.. tip::
- Refer to the documentation on :ref:`headers <reference/view_architectures/form/header>` in
form views.
.. todo: accept/refuse offer buttons -> auto refuse others when accepting (write)
.. todo: multi-checkbox refuse offers in bulk
.. todo: "assign myself as salesperson" action
- Refer to the documentation on :ref:`fields <reference/view_architectures/form/field>`,
:ref:`labels <reference/view_architectures/form/label>`, and :ref:`headers
<reference/view_architectures/form/header>` in form views, as well as the documentation on
:ref:`buttons <reference/view_architectures/list/button>` in list views.
- Use the `o_row` class to fit multiple elements on one line.
- Use the `btn-success` and `btn-danger` classes to style your buttons.
.. spoiler:: Solution
.. code-block:: xml
:caption: `real_estate_property_views.xml`
:emphasize-lines: 4-9,18-26
<record id="real_estate.property_form" model="ir.ui.view">
[...]
<header>
<button
string="Cancel Listing"
type="object"
name="action_cancel_listing"
confirm="Are you sure you want to cancel this property listing?"
/>
<field name="state" widget="statusbar" options="{'clickable': True}"/>
</header>
[...]
<page string="Other Info">
<group>
<group>
<field name="address_id"/>
<field name="seller_id"/>
<label for="salesperson_id"/>
<div class="o_row">
<field name="salesperson_id" class="oe_inline"/>
<button
string="Assign Myself"
type="object"
name="action_assign_user_as_salesperson"
/>
</div>
</group>
</group>
</page>
[...]
</record>
.. code-block:: python
:caption: `real_estate_property.py`
:emphasize-lines: 1
:emphasize-lines: 8,15-24
state = fields.Selection(
string="State",
selection=[
('new', "New"),
('offer_received', "Offer Received"),
('under_option', "Under Option"),
('sold', "Sold"),
('cancelled', "Cancelled"),
],
required=True,
default='new',
)
[...]
def action_cancel_listing(self):
for property in self:
property.offer_ids.action_refuse()
property.write({
'state': 'cancelled',
'active': False,
})
return True
def action_assign_user_as_salesperson(self):
self.salesperson_id = self.env.user.id
return True
.. code-block:: xml
:caption: `real_estate_offer_views.xml`
:emphasize-lines: 4,11-12
<record id="real_estate.view_offers_action" model="ir.actions.act_window">
<field name="name">Offers</field>
<field name="res_model">real.estate.offer</field>
<field name="domain">[("property_id", "=", active_id)]</field>
[...]
</record>
<record id="real_estate.offer_list" model="ir.ui.view">
[...]
<field name="state"/>
<button string="Accept" type="object" name="action_accept" class="btn-success"/>
<button string="Refuse" type="object" name="action_refuse" class="btn-danger"/>
[...]
</record>
.. code-block:: python
:caption: `real_estate_offer.py`
:emphasize-lines: 1-7
def action_accept(self):
self.state = 'accepted'
self.property_id.state = 'under_option'
(self.property_id.offer_ids - self).state = 'refused'
return True
def action_refuse(self):
self.state = 'refused'
return True
.. _tutorials/server_framework_101/scheduled_actions:
Scheduled actions
-----------------
... also known as **crons**...
.. todo: explain magic commands
.. todo: ex: 6,0,0 to associate tags to properties in data
----
.. todo: add incentive for chapter 6

View File

@ -14,6 +14,10 @@ tmp
.. todo: context active_test False on the category_id field of products to see archived categories
.. todo: sequence widget on tags
.. todo: compute display_name for offers in form view
.. todo: multi edit offers state
.. todo: hide the 'Cancelled' state from the statusbar widget unless selected
.. todo: pills for the offer state in list view
.. todo: adapt existing or add new stat button relying on a model method that returns a dictionary acting as an action descriptor
----

View File

@ -7,10 +7,8 @@ tmp
.. todo: inherit from mail.tread mixin and add a chatter
.. todo: self.env._context
.. todo: inherit from account.move
.. todo: explain magic commands
.. todo: ex: override of real.estate.property::create to set vals['address_id'] = (0, 0, {})
.. todo: ex: create invoice with lines (6, 0, 0)
.. todo: ex: 6,0,0 to associate tags to properties in data
----

View File

@ -4,6 +4,6 @@ Chapter 10: Unit testing
tmp
----
.. todo: run with coverage
.. todo: add incentive for next chapter
----