Search code examples
pythonodooodoo-14

Odoo 14, triggering a function upon stock.quant.write(), but only once


I want to trigger an action when a stock.quant is written to, i.e. when the total stock of a product is changed. My current approach is this:

class StockQuantInherit(models.Model):
_inherit = "stock.quant"

def write(self, vals):
    res = super(StockQuantInherit, self).write(vals)
    available = self.product_id.product_tmpl_id.mapped('virtual_available')[0]
    log(available) # see result below
    api_call_to_external_site(self.product_id.default_code, available)
    return res

When the stock of a product is changed, this results in stock.quant being written to three times, as seen below. (It's shown as an error, so I can easily spot it in a stream of info)

2021-08-08 12:25:44,278 11335 ERROR odoo odoo.addons.stock_enhanced.models.stock_move: 87.0 
2021-08-08 12:25:44,281 11335 ERROR odoo odoo.addons.stock_enhanced.models.stock_move: 87.0 
2021-08-08 12:25:44,299 11335 ERROR odoo odoo.addons.stock_enhanced.models.stock_move: 96.0 

Or it may also look like this:

2021-08-08 12:32:43,987 11335 ERROR odoo odoo.addons.stock_enhanced.models.stock_move: 100.0 
2021-08-08 12:32:43,990 11335 ERROR odoo odoo.addons.stock_enhanced.models.stock_move: 100.0 
2021-08-08 12:32:44,009 11335 ERROR odoo odoo.addons.stock_enhanced.models.stock_move: 100.0 

The first two values are not of interest to me, since they may be correct or not. Only the last one is of interest. So in the name of efficiency and not calling the same function, which will make a call to an external API, three times within a split-second, I would like to trigger the function only after the last write event.

Now comes my question, how do I do that? It'd be easy enough to only trigger it the first time, but I am at a loss on how to trigger it only at the last time.


Solution

  • OK I managed to solve the problem. What I was looking for is called "Event Debouncing".

    I tried to copy-paste this decorator: https://gist.github.com/walkermatt/2871026

    However, this did not work, because apparently a new instance (?) of the debounce function was created each time, resulting in three threads that all successfully waited for 1 second to pass.

    So I implemented the function in my module, to make sure it is always the same one that gets called. Apologies if my interpretation of why the decorator didn't work is incorrect.

    Anyway, this solution now solved my problem:

    from threading import Timer
    
    def call_api(sku, quantity_available):
        def call_it():
            log(quantity_available)
        try:
            call_api.t.cancel()
        except(AttributeError):
            pass
        call_api.t = Timer(1, call_it)
        call_api.t.start()
    
    
    class StockQuantInherit(models.Model):
        _inherit = "stock.quant"
    
        def write(self, vals):
            res = super(StockQuantInherit, self).write(vals)
            if self.product_id.default_code: 
                available = self.product_id.product_tmpl_id.mapped('virtual_available')[0]
                call_api(self.product_id.default_code, available)
            return res