Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 52 additions & 29 deletions ddmrp/models/stock_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

import logging
import operator as py_operator
import threading
from collections import defaultdict
from datetime import datetime, timedelta
from math import pi

from odoo import _, api, exceptions, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import float_compare, float_round
from odoo.tools.misc import split_every

_logger = logging.getLogger(__name__)
try:
Expand Down Expand Up @@ -47,6 +49,8 @@ class StockBuffer(models.Model):
_description = "Stock Buffer"
_order = "planning_priority_level asc, net_flow_position asc"

CRON_DDMRP_CHUNKS = 50

@api.model
def default_get(self, fields):
res = super().default_get(fields)
Expand Down Expand Up @@ -1461,30 +1465,46 @@ def action_view_supply_outside_dlt_window(self):
@api.model
def cron_ddmrp_adu(self, automatic=False):
"""calculate ADU for each DDMRP buffer. Called by cronjob."""
auto_commit = not getattr(threading.currentThread(), "testing", False)
_logger.info("Start cron_ddmrp_adu.")
buffers = self.search([])
buffer_ids = self.search([]).ids
i = 0
j = len(buffers)
for b in buffers:
try:
i += 1
_logger.debug("ddmrp cron_adu: {}. ({}/{})".format(b.name, i, j))
if automatic:
with self.env.cr.savepoint():
j = len(buffer_ids)
for buffer_chunk_ids in split_every(self.CRON_DDMRP_CHUNKS, buffer_ids):
for b in self.browse(buffer_chunk_ids).exists():
Comment thread
LoisRForgeFlow marked this conversation as resolved.
try:
i += 1
_logger.debug("ddmrp cron_adu: {}. ({}/{})".format(b.name, i, j))
if automatic:
with self.env.cr.savepoint():
b._calc_adu()
else:
b._calc_adu()
else:
b._calc_adu()
except Exception:
_logger.exception("Fail to compute ADU for buffer %s", b.name)
if not automatic:
raise
except Exception:
_logger.exception("Fail to compute ADU for buffer %s", b.name)
if not automatic:
raise
if auto_commit:
self._cr.commit()
_logger.info("End cron_ddmrp_adu.")
return True

def cron_actions(self, only_nfp=False):
"""This method is meant to be inherited by other modules in order to
enhance extensibility."""
self.ensure_one()
self.invalidate_cache(
fnames=[
"product_location_qty",
"incoming_location_qty",
"outgoing_location_qty",
"virtual_location_qty",
"product_location_qty_available_not_res",
"dlt",
"distributed_source_location_qty",
],
ids=self.ids,
)
if not only_nfp or only_nfp == "out":
self._calc_qualified_demand()
if not only_nfp or only_nfp == "in":
Expand All @@ -1505,24 +1525,27 @@ def cron_actions(self, only_nfp=False):
def cron_ddmrp(self, automatic=False):
"""Calculate key DDMRP parameters for each buffer.
Called by cronjob."""
auto_commit = not getattr(threading.currentThread(), "testing", False)
_logger.info("Start cron_ddmrp.")
buffers = self.search([])
buffer_ids = self.search([]).ids
i = 0
j = len(buffers)
buffers.refresh()
for b in buffers:
i += 1
_logger.debug("ddmrp cron: {}. ({}/{})".format(b.name, i, j))
try:
if automatic:
with self.env.cr.savepoint():
j = len(buffer_ids)
for buffer_chunk_ids in split_every(self.CRON_DDMRP_CHUNKS, buffer_ids):
for b in self.browse(buffer_chunk_ids).exists():
i += 1
_logger.debug("ddmrp cron: {}. ({}/{})".format(b.name, i, j))
try:
if automatic:
with self.env.cr.savepoint():
b.cron_actions()
else:
b.cron_actions()
else:
b.cron_actions()
except Exception:
_logger.exception("Fail updating buffer %s", b.name)
if not automatic:
raise
except Exception:
_logger.exception("Fail updating buffer %s", b.name)
if not automatic:
raise
if auto_commit:
self._cr.commit()
_logger.info("End cron_ddmrp.")
return True

Expand Down
10 changes: 10 additions & 0 deletions ddmrp/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ class TestDdmrpCommon(common.SavepointCase):
def setUpClass(cls):
super().setUpClass()

cls.env = cls.env(
context=dict(
cls.env.context,
tracking_disable=True,
Comment thread
guewen marked this conversation as resolved.
# compatibility with ddmrp_cron_actions_as_job,
# that would delay calls to "cron_actions" in these tests
test_queue_job_no_delay=True,
)
)

# Models
cls.productModel = cls.env["product.product"]
cls.templateModel = cls.env["product.template"]
Expand Down
1 change: 1 addition & 0 deletions ddmrp_cron_actions_as_job/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
15 changes: 15 additions & 0 deletions ddmrp_cron_actions_as_job/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2020 Camptocamp (https://www.camptocamp.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).

{
"name": "DDMRP Buffer Calculation as job",
"version": "13.0.1.0.0",
"summary": "Run DDMRP Buffer Calculation as jobs",
"author": "Camptocamp, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/ddmrp",
"category": "Warehouse Management",
"depends": ["ddmrp", "queue_job"],
"data": ["data/queue_job_channel_data.xml", "data/queue_job_function_data.xml"],
"license": "LGPL-3",
"installable": True,
}
6 changes: 6 additions & 0 deletions ddmrp_cron_actions_as_job/data/queue_job_channel_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<odoo noupdate="1">
<record model="queue.job.channel" id="channel_ddmrp">
<field name="name">ddmrp</field>
<field name="parent_id" ref="queue_job.channel_root" />
</record>
</odoo>
7 changes: 7 additions & 0 deletions ddmrp_cron_actions_as_job/data/queue_job_function_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<odoo noupdate="1">
<record id="job_function_stock_buffer_cron_actions" model="queue.job.function">
<field name="model_id" ref="ddmrp.model_stock_buffer" />
<field name="method">cron_actions</field>
<field name="channel_id" ref="channel_ddmrp" />
</record>
</odoo>
2 changes: 2 additions & 0 deletions ddmrp_cron_actions_as_job/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import stock_buffer
from . import stock_move
51 changes: 51 additions & 0 deletions ddmrp_cron_actions_as_job/models/stock_buffer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2020 Camptocamp (https://www.camptocamp.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).

from odoo import models

from odoo.addons.queue_job.job import identity_exact


class Buffer(models.Model):
_inherit = "stock.buffer"

def cron_actions_job_options(self, only_nfp=False):
return {
"identity_key": identity_exact,
"priority": 15,
"description": "DDMRP Buffer calculation ({})".format(self.display_name),
}

def _calc_adu_job_options(self):
return {
"identity_key": identity_exact,
"priority": 15,
"description": "DDMRP Buffer ADU calculation ({})".format(
self.display_name
),
}

def _register_hook(self):
self._patch_method(
"cron_actions",
self._patch_job_auto_delay(
"cron_actions", context_key="auto_delay_ddmrp_cron_actions"
),
)
self._patch_method(
"_calc_adu",
self._patch_job_auto_delay(
"_calc_adu", context_key="auto_delay_ddmrp_calc_adu"
),
)
return super()._register_hook()

def cron_ddmrp(self, automatic=False):
return super(
Buffer, self.with_context(auto_delay_ddmrp_cron_actions=True)
).cron_ddmrp(automatic=automatic)

def cron_ddmrp_adu(self, automatic=False):
return super(
Buffer, self.with_context(auto_delay_ddmrp_calc_adu=True)
).cron_ddmrp_adu(automatic=automatic)
13 changes: 13 additions & 0 deletions ddmrp_cron_actions_as_job/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2020 Camptocamp
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).

from odoo import models


class StockMove(models.Model):
_inherit = "stock.move"

def _update_ddmrp_nfp(self):
return super(
StockMove, self.with_context(auto_delay_ddmrp_cron_actions=True)
)._update_ddmrp_nfp()
1 change: 1 addition & 0 deletions ddmrp_cron_actions_as_job/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
14 changes: 14 additions & 0 deletions ddmrp_cron_actions_as_job/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
DDMRP Buffer calculations are now run with Queue Jobs.

When auto-update of NFP is active, each time the state of a stock move changes,
a new computation is triggered, but thanks to identity keys on jobs, only one
job at a time is generated for the same buffer.

The ``<stock.buffer>.cron_actions`` method is automatically delayed when the
context contains ``auto_delay_ddmrp_cron_actions=True``.

The scheduled action for buffers ADU computation also generates jobs instead
of recomputing all the buffers at once.

The ``<stock.buffer>._calc_adu`` method is automatically delayed when the
context contains ``auto_delay_ddmrp_calc_adu=True``.
1 change: 1 addition & 0 deletions ddmrp_cron_actions_as_job/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_cron_actions_as_job
56 changes: 56 additions & 0 deletions ddmrp_cron_actions_as_job/tests/test_cron_actions_as_job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2020 Camptocamp (https://www.camptocamp.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).


from odoo.tests import tagged

from odoo.addons.ddmrp.tests.common import TestDdmrpCommon
from odoo.addons.queue_job.job import identity_exact
from odoo.addons.queue_job.tests.common import mock_with_delay


@tagged("post_install", "-at_install")
class TestDdmrpCronActionsAsJob(TestDdmrpCommon):
def test_cron_actions_delay_job(self):
context = dict(self.env.context, auto_delay_ddmrp_cron_actions=True)
del context["test_queue_job_no_delay"]
buffer_a = self.buffer_a.with_context(context)

with mock_with_delay() as (delayable_cls, delayable):
buffer_a.cron_actions(only_nfp=True)

# check 'with_delay()' part:
self.assertEqual(delayable_cls.call_count, 1)
# arguments passed in 'with_delay()'
delay_args, delay_kwargs = delayable_cls.call_args
self.assertEqual(delay_args, (self.buffer_a,))
self.assertEqual(delay_kwargs.get("priority"), 15)
self.assertEqual(delay_kwargs.get("identity_key"), identity_exact)

# check what's passed to the job method 'cron_actions'
self.assertEqual(delayable.cron_actions.call_count, 1)
delay_args, delay_kwargs = delayable.cron_actions.call_args
self.assertEqual(delay_args, ())
self.assertDictEqual(delay_kwargs, {"only_nfp": True})

def test_calc_adu_delay_job(self):
context = dict(self.env.context, auto_delay_ddmrp_calc_adu=True)
del context["test_queue_job_no_delay"]
buffer_a = self.buffer_a.with_context(context)

with mock_with_delay() as (delayable_cls, delayable):
buffer_a._calc_adu()

# check 'with_delay()' part:
self.assertEqual(delayable_cls.call_count, 1)
# arguments passed in 'with_delay()'
delay_args, delay_kwargs = delayable_cls.call_args
self.assertEqual(delay_args, (self.buffer_a,))
self.assertEqual(delay_kwargs.get("priority"), 15)
self.assertEqual(delay_kwargs.get("identity_key"), identity_exact)

# check what's passed to the job method '_calc_adu'
self.assertEqual(delayable._calc_adu.call_count, 1)
delay_args, delay_kwargs = delayable._calc_adu.call_args
self.assertEqual(delay_args, ())
self.assertDictEqual(delay_kwargs, {})
1 change: 1 addition & 0 deletions oca_dependencies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ stock-logistics-warehouse
web
manufacture
server-tools
queue
6 changes: 6 additions & 0 deletions setup/ddmrp_cron_actions_as_job/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)