From 61b62525ca97c89e92b094d76ac426c045bc5103 Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Mon, 24 Feb 2025 17:07:07 +0100 Subject: [PATCH] scheduled actions --- .../05_connect_the_dots.rst | 160 +++++++++++++++++- 1 file changed, 157 insertions(+), 3 deletions(-) diff --git a/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst b/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst index 92539670e..f4d2b76c2 100644 --- a/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst +++ b/content/developer/tutorials/server_framework_101/05_connect_the_dots.rst @@ -1477,10 +1477,164 @@ action. Scheduled actions ----------------- -... also known as **crons**... +**Scheduled actions**, also known as cron jobs, are automated tasks that run periodically at +predefined intervals. They enable the automation of recurring operations and allow to offload +compute-intensive tasks to dedicated workers. Scheduled actions are typically used for background +operations such as data cleanup, third-party synchronization, report generation, and other tasks +that don't require immediate user interaction. -.. todo: explain magic commands -.. todo: ex: 6,0,0 to associate tags to properties in data +In Odoo, scheduled actions are implemented through the `ir.cron` model. When triggered, they execute +arbitrary code on a specified model, most commonly by calling a model method that implements the +desired business logic. Creating a scheduled action is simply a matter of adding a record to +`ir.cron`, after which a cron worker will execute it at the specified intervals. + +.. example:: + The following example implements a scheduled action that automatically reassigns inactive + products or products without sellers to the default seller. + + .. code-block:: xml + + + Reassign Inactive Products + + model._reassign_inactive_products() + 1 + weeks + + + .. code-block:: python + + from odoo import api, models + from odoo.fields import Command + + + class Product(models.Model): + + @api.model + def _reassign_inactive_products(self): + # Clear sellers from underperfoming products. + underperforming_products = self.search([('sales_count', '<', 10)]) + underperforming_products.write({ + 'seller_ids': [Command.clear()], # Remove all sellers. + }) + + # Assign the default seller to products without sellers. + products_without_sellers = self.search([('seller_ids', '=', False)]) + if products_without_sellers: + default_seller = self.env.ref('product.default_seller') + products_without_sellers.write({ + 'seller_ids': [Command.set(default_seller.ids)] # Replace with default seller. + }) + + .. note:: + - The cron is scheduled to run weekly thanks to `interval_number=1` and + `interval_type='weeks'`. + - The `@api.model` decorator indicates the method operates on the model and records in `self` + are not relevant. This serves both as documentation and enables RPC calls without requiring + record IDs. + - Field commands are required for `One2many` and `Many2many` fields since they cannot be + assigned values directly. + - `Command.set` takes a list of IDs as argument, which the `ids` recordset attribute + conveniently provides. + +.. seealso:: + - Reference documentation on :ref:`scheduled actions `. + - Reference documentation on the :meth:`@api.model ` decorator. + - Reference documentation on :ref:`field commands `. + +.. exercise:: + #. Create a scheduled action that automatically refuses offers that have expired. + #. Create a scheduled action that automatically applies a 10% discount and adds the "Price + Reduced" tag to inactive properties. A property is considered inactive if it didn't receive + any offers 2 months after it was listed. + + .. tip:: + To test your crons manually, activate the :doc:`developer mode + `, then go to :menuselection:`Settings --> Technical + --> Scheduled Actions`, and click :guilabel:`Run Manually` in form view. + +.. spoiler:: Solution + + .. code-block:: python + :caption: `__manifest__.py` + :emphasize-lines: 3 + + 'data': [ + # Model data + 'data/ir_cron_data.xml', + [...] + ], + + .. code-block:: xml + :caption: `ir_cron_data.xml` + + + + + + Real Estate: Discount Inactive Properties + + model._discount_inactive_properties() + 1 + days + + + + Real Estate: Refuse Expired Offers + + model._refuse_expired_offers() + 1 + days + + + + + .. code-block:: python + :caption: `real_estate_offer.py` + :emphasize-lines: 1-4 + + @api.model + def _refuse_expired_offers(self): + expired_offers = self.search([('expiry_date', '<', fields.Date.today())]) + expired_offers.action_refuse() + + .. code-block:: xml + :caption: `real_estate_tag_data.xml` + :emphasize-lines: 1-4 + + + Price Reduced + 1 + + + .. code-block:: python + :caption: `real_estate_property.py` + :emphasize-lines: 3,10-24 + + from odoo import _, api, fields, models + from odoo.exceptions import UserError, ValidationError + from odoo.fields import Command + from odoo.tools import date_utils + + + class RealEstateProperty(models.Model): + [...] + + @api.model + def _discount_inactive_properties(self): + two_months_ago = fields.Date.today() - date_utils.relativedelta(months=2) + price_reduced_tag = self.env.ref('real_estate.tag_price_reduced') + inactive_properties = self.search([ + ('create_date', '<', two_months_ago), + ('active', '=', True), + ('state', '=', 'new'), + ('tag_ids', 'not in', price_reduced_tag.ids), # Only discount once. + ]) + for property in inactive_properties: + property.write({ + 'selling_price': property.selling_price * 0.9, + 'tag_ids': [Command.link(price_reduced_tag.id)], + }) ----