@@ -1477,10 +1477,164 @@ action.
1477
1477
Scheduled actions
1478
1478
-----------------
1479
1479
1480
- ... also known as **crons **...
1480
+ **Scheduled actions **, also known as cron jobs, are automated tasks that run periodically at
1481
+ predefined intervals. They enable the automation of recurring operations and allow to offload
1482
+ compute-intensive tasks to dedicated workers. Scheduled actions are typically used for background
1483
+ operations such as data cleanup, third-party synchronization, report generation, and other tasks
1484
+ that don't require immediate user interaction.
1481
1485
1482
- .. todo: explain magic commands
1483
- .. todo: ex: 6,0,0 to associate tags to properties in data
1486
+ In Odoo, scheduled actions are implemented through the `ir.cron ` model. When triggered, they execute
1487
+ arbitrary code on a specified model, most commonly by calling a model method that implements the
1488
+ desired business logic. Creating a scheduled action is simply a matter of adding a record to
1489
+ `ir.cron `, after which a cron worker will execute it at the specified intervals.
1490
+
1491
+ .. example ::
1492
+ The following example implements a scheduled action that automatically reassigns inactive
1493
+ products or products without sellers to the default seller.
1494
+
1495
+ .. code-block :: xml
1496
+
1497
+ <record id =" reassign_inactive_products_cron" model =" ir.cron" >
1498
+ <field name =" name" >Reassign Inactive Products</field >
1499
+ <field name =" model_id" ref =" model_product" />
1500
+ <field name =" code" >model._reassign_inactive_products()</field >
1501
+ <field name =" interval_number" >1</field >
1502
+ <field name =" interval_type" >weeks</field >
1503
+ </record >
1504
+
1505
+ .. code-block :: python
1506
+
1507
+ from odoo import api, models
1508
+ from odoo.fields import Command
1509
+
1510
+
1511
+ class Product (models .Model ):
1512
+
1513
+ @api.model
1514
+ def _reassign_inactive_products (self ):
1515
+ # Clear sellers from underperfoming products.
1516
+ underperforming_products = self .search([(' sales_count' , ' <' , 10 )])
1517
+ underperforming_products.write({
1518
+ ' seller_ids' : [Command.clear()], # Remove all sellers.
1519
+ })
1520
+
1521
+ # Assign the default seller to products without sellers.
1522
+ products_without_sellers = self .search([(' seller_ids' , ' =' , False )])
1523
+ if products_without_sellers:
1524
+ default_seller = self .env.ref(' product.default_seller' )
1525
+ products_without_sellers.write({
1526
+ ' seller_ids' : [Command.set(default_seller.ids)] # Replace with default seller.
1527
+ })
1528
+
1529
+ .. note ::
1530
+ - The cron is scheduled to run weekly thanks to `interval_number=1 ` and
1531
+ `interval_type='weeks' `.
1532
+ - The `@api.model ` decorator indicates the method operates on the model and records in `self `
1533
+ are not relevant. This serves both as documentation and enables RPC calls without requiring
1534
+ record IDs.
1535
+ - Field commands are required for `One2many ` and `Many2many ` fields since they cannot be
1536
+ assigned values directly.
1537
+ - `Command.set ` takes a list of IDs as argument, which the `ids ` recordset attribute
1538
+ conveniently provides.
1539
+
1540
+ .. seealso ::
1541
+ - Reference documentation on :ref: `scheduled actions <reference/actions/cron >`.
1542
+ - Reference documentation on the :meth: `@api.model <odoo.api.model> ` decorator.
1543
+ - Reference documentation on :ref: `field commands <reference/fields/command >`.
1544
+
1545
+ .. exercise ::
1546
+ #. Create a scheduled action that automatically refuses offers that have expired.
1547
+ #. Create a scheduled action that automatically applies a 10% discount and adds the "Price
1548
+ Reduced" tag to inactive properties. A property is considered inactive if it didn't receive
1549
+ any offers 2 months after it was listed.
1550
+
1551
+ .. tip ::
1552
+ To test your crons manually, activate the :doc: `developer mode
1553
+ </applications/general/developer_mode>`, then go to :menuselection: `Settings --> Technical
1554
+ --> Scheduled Actions `, and click :guilabel: `Run Manually ` in form view.
1555
+
1556
+ .. spoiler :: Solution
1557
+
1558
+ .. code-block :: python
1559
+ :caption: `__manifest__.py`
1560
+ :emphasize- lines: 3
1561
+
1562
+ ' data' : [
1563
+ # Model data
1564
+ ' data/ir_cron_data.xml' ,
1565
+ [... ]
1566
+ ],
1567
+
1568
+ .. code-block :: xml
1569
+ :caption: `ir_cron_data.xml`
1570
+
1571
+ <?xml version =" 1.0" encoding =" utf-8" ?>
1572
+ <odoo >
1573
+
1574
+ <record id =" real_estate.discount_inactive_properties_cron" model =" ir.cron" >
1575
+ <field name =" name" >Real Estate: Discount Inactive Properties</field >
1576
+ <field name =" model_id" ref =" model_real_estate_property" />
1577
+ <field name =" code" >model._discount_inactive_properties()</field >
1578
+ <field name =" interval_number" >1</field >
1579
+ <field name =" interval_type" >days</field >
1580
+ </record >
1581
+
1582
+ <record id =" real_estate.refuse_expired_offers_cron" model =" ir.cron" >
1583
+ <field name =" name" >Real Estate: Refuse Expired Offers</field >
1584
+ <field name =" model_id" <
9E81
/span> ref =" model_real_estate_offer" />
1585
+ <field name =" code" >model._refuse_expired_offers()</field >
1586
+ <field name =" interval_number" >1</field >
1587
+ <field name =" interval_type" >days</field >
1588
+ </record >
1589
+
1590
+ </odoo >
1591
+
1592
+ .. code-block :: python
1593
+ :caption: `real_estate_offer.py`
1594
+ :emphasize- lines: 1 - 4
1595
+
1596
+ @api.model
1597
+ def _refuse_expired_offers (self ):
1598
+ expired_offers = self .search([(' expiry_date' , ' <' , fields.Date.today())])
1599
+ expired_offers.action_refuse()
1600
+
1601
+ .. code-block :: xml
1602
+ :caption: `real_estate_tag_data.xml`
1603
+ :emphasize-lines: 1-4
1604
+
1605
+ <record id =" real_estate.tag_price_reduced" model =" real.estate.tag" >
1606
+ <field name =" name" >Price Reduced</field >
1607
+ <field name =" color" >1</field >
1608
+ </record >
1609
+
1610
+ .. code-block :: python
1611
+ :caption: `real_estate_property.py`
1612
+ :emphasize- lines: 3 ,10 - 24
1613
+
1614
+ from odoo import _, api, fields, models
1615
+ from odoo.exceptions import UserError, ValidationError
1616
+ from odoo.fields import Command
1617
+ from odoo.tools import date_utils
1618
+
1619
+
1620
+ class RealEstateProperty (models .Model ):
1621
+ [... ]
1622
+
1623
+ @api.model
1624
+ def _discount_inactive_properties (self ):
1625
+ two_months_ago = fields.Date.today() - date_utils.relativedelta(months = 2 )
1626
+ price_reduced_tag = self .env.ref(' real_estate.tag_price_reduced' )
1627
+ inactive_properties = self .search([
1628
+ (' create_date' , ' <' , two_months_ago),
1629
+ (' active' , ' =' , True ),
1630
+ (' state' , ' =' , ' new' ),
1631
+ (' tag_ids' , ' not in' , price_reduced_tag.ids), # Only discount once.
1632
+ ])
1633
+ for property in inactive_properties:
1634
+ property .write({
1635
+ ' selling_price' : property .selling_price * 0.9 ,
1636
+ ' tag_ids' : [Command.link(price_reduced_tag.id)],
1637
+ })
1484
1638
1485
1639
----
1486
1640
0 commit comments