Tower: unpublish queue_job — remove source from 16.0 branch
This commit is contained in:
@@ -1,707 +0,0 @@
|
||||
.. image:: https://odoo-community.org/readme-banner-image
|
||||
:target: https://odoo-community.org/get-involved?utm_source=readme
|
||||
:alt: Odoo Community Association
|
||||
|
||||
=========
|
||||
Job Queue
|
||||
=========
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:b92d06dbbf161572f2bf02e0c6a59282cea11cc5e903378094bead986f0125de
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Mature
|
||||
.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fqueue-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/queue/tree/16.0/queue_job
|
||||
:alt: OCA/queue
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/queue-16-0/queue-16-0-queue_job
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This addon adds an integrated Job Queue to Odoo.
|
||||
|
||||
It allows to postpone method calls executed asynchronously.
|
||||
|
||||
Jobs are executed in the background by a ``Jobrunner``, in their own transaction.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
class MyModel(models.Model):
|
||||
_name = 'my.model'
|
||||
|
||||
def my_method(self, a, k=None):
|
||||
_logger.info('executed with a: %s and k: %s', a, k)
|
||||
|
||||
|
||||
class MyOtherModel(models.Model):
|
||||
_name = 'my.other.model'
|
||||
|
||||
def button_do_stuff(self):
|
||||
self.env['my.model'].with_delay().my_method('a', k=2)
|
||||
|
||||
|
||||
In the snippet of code above, when we call ``button_do_stuff``, a job **capturing
|
||||
the method and arguments** will be postponed. It will be executed as soon as the
|
||||
Jobrunner has a free bucket, which can be instantaneous if no other job is
|
||||
running.
|
||||
|
||||
|
||||
Features:
|
||||
|
||||
* Views for jobs, jobs are stored in PostgreSQL
|
||||
* Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL's NOTIFY
|
||||
* Channels: give a capacity for the root channel and its sub-channels and
|
||||
segregate jobs in them. Allow for instance to restrict heavy jobs to be
|
||||
executed one at a time while little ones are executed 4 at a times.
|
||||
* Retries: Ability to retry jobs by raising a type of exception
|
||||
* Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next tries,
|
||||
retry after 1 minutes, ...
|
||||
* Job properties: priorities, estimated time of arrival (ETA), custom
|
||||
description, number of retries
|
||||
* Related Actions: link an action on the job view, such as open the record
|
||||
concerned by the job
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Be sure to have the ``requests`` library.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
* Using environment variables and command line:
|
||||
|
||||
* Adjust environment variables (optional):
|
||||
|
||||
- ``ODOO_QUEUE_JOB_CHANNELS=root:4`` or any other channels configuration.
|
||||
The default is ``root:1``
|
||||
|
||||
- if ``xmlrpc_port`` is not set: ``ODOO_QUEUE_JOB_PORT=8069``
|
||||
|
||||
* Start Odoo with ``--load=web,queue_job``
|
||||
and ``--workers`` greater than 1. [1]_
|
||||
|
||||
* Keep in mind that the number of workers should be greater than the number of
|
||||
channels. ``queue_job`` will reuse normal Odoo workers to process jobs. It
|
||||
will not spawn its own workers.
|
||||
|
||||
* Using the Odoo configuration file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[options]
|
||||
(...)
|
||||
workers = 6
|
||||
server_wide_modules = web,queue_job
|
||||
|
||||
(...)
|
||||
[queue_job]
|
||||
channels = root:2
|
||||
|
||||
* Environment variables have priority over the configuration file.
|
||||
|
||||
* Confirm the runner is starting correctly by checking the odoo log file:
|
||||
|
||||
.. code-block::
|
||||
|
||||
...INFO...queue_job.jobrunner.runner: starting
|
||||
...INFO...queue_job.jobrunner.runner: initializing database connections
|
||||
...INFO...queue_job.jobrunner.runner: queue job runner ready for db <dbname>
|
||||
...INFO...queue_job.jobrunner.runner: database connections ready
|
||||
|
||||
* Create jobs (eg using ``base_import_async``) and observe they
|
||||
start immediately and in parallel.
|
||||
|
||||
* Tip: to enable debug logging for the queue job, use
|
||||
``--log-handler=odoo.addons.queue_job:DEBUG``
|
||||
|
||||
.. [1] It works with the threaded Odoo server too, although this way
|
||||
of running Odoo is obviously not for production purposes.
|
||||
|
||||
* Jobs that remain in `enqueued` or `started` state (because, for instance, their worker has been killed) will be automatically re-queued.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To use this module, you need to:
|
||||
|
||||
#. Go to ``Job Queue`` menu
|
||||
|
||||
Developers
|
||||
~~~~~~~~~~
|
||||
|
||||
Delaying jobs
|
||||
-------------
|
||||
|
||||
The fast way to enqueue a job for a method is to use ``with_delay()`` on a record
|
||||
or model:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_done(self):
|
||||
self.with_delay().print_confirmation_document(self.state)
|
||||
self.write({"state": "done"})
|
||||
return True
|
||||
|
||||
Here, the method ``print_confirmation_document()`` will be executed asynchronously
|
||||
as a job. ``with_delay()`` can take several parameters to define more precisely how
|
||||
the job is executed (priority, ...).
|
||||
|
||||
All the arguments passed to the method being delayed are stored in the job and
|
||||
passed to the method when it is executed asynchronously, including ``self``, so
|
||||
the current record is maintained during the job execution (warning: the context
|
||||
is not kept).
|
||||
|
||||
Dependencies can be expressed between jobs. To start a graph of jobs, use ``delayable()``
|
||||
on a record or model. The following is the equivalent of ``with_delay()`` but using the
|
||||
long form:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_done(self):
|
||||
delayable = self.delayable()
|
||||
delayable.print_confirmation_document(self.state)
|
||||
delayable.delay()
|
||||
self.write({"state": "done"})
|
||||
return True
|
||||
|
||||
Methods of Delayable objects return itself, so it can be used as a builder pattern,
|
||||
which in some cases allow to build the jobs dynamically:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_generate_simple_with_delayable(self):
|
||||
self.ensure_one()
|
||||
# Introduction of a delayable object, using a builder pattern
|
||||
# allowing to chain jobs or set properties. The delay() method
|
||||
# on the delayable object actually stores the delayable objects
|
||||
# in the queue_job table
|
||||
(
|
||||
self.delayable()
|
||||
.generate_thumbnail((50, 50))
|
||||
.set(priority=30)
|
||||
.set(description=_("generate xxx"))
|
||||
.delay()
|
||||
)
|
||||
|
||||
The simplest way to define a dependency is to use ``.on_done(job)`` on a Delayable:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_chain_done(self):
|
||||
self.ensure_one()
|
||||
job1 = self.browse(1).delayable().generate_thumbnail((50, 50))
|
||||
job2 = self.browse(1).delayable().generate_thumbnail((50, 50))
|
||||
job3 = self.browse(1).delayable().generate_thumbnail((50, 50))
|
||||
# job 3 is executed when job 2 is done which is executed when job 1 is done
|
||||
job1.on_done(job2.on_done(job3)).delay()
|
||||
|
||||
Delayables can be chained to form more complex graphs using the ``chain()`` and
|
||||
``group()`` primitives.
|
||||
A chain represents a sequence of jobs to execute in order, a group represents
|
||||
jobs which can be executed in parallel. Using ``chain()`` has the same effect as
|
||||
using several nested ``on_done()`` but is more readable. Both can be combined to
|
||||
form a graph, for instance we can group [A] of jobs, which blocks another group
|
||||
[B] of jobs. When and only when all the jobs of the group [A] are executed, the
|
||||
jobs of the group [B] are executed. The code would look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo.addons.queue_job.delay import group, chain
|
||||
|
||||
def button_done(self):
|
||||
group_a = group(self.delayable().method_foo(), self.delayable().method_bar())
|
||||
group_b = group(self.delayable().method_baz(1), self.delayable().method_baz(2))
|
||||
chain(group_a, group_b).delay()
|
||||
self.write({"state": "done"})
|
||||
return True
|
||||
|
||||
When a failure happens in a graph of jobs, the execution of the jobs that depend on the
|
||||
failed job stops. They remain in a state ``wait_dependencies`` until their "parent" job is
|
||||
successful. This can happen in two ways: either the parent job retries and is successful
|
||||
on a second try, either the parent job is manually "set to done" by a user. In these two
|
||||
cases, the dependency is resolved and the graph will continue to be processed. Alternatively,
|
||||
the failed job and all its dependent jobs can be canceled by a user. The other jobs of the
|
||||
graph that do not depend on the failed job continue their execution in any case.
|
||||
|
||||
Note: ``delay()`` must be called on the delayable, chain, or group which is at the top
|
||||
of the graph. In the example above, if it was called on ``group_a``, then ``group_b``
|
||||
would never be delayed (but a warning would be shown).
|
||||
|
||||
It is also possible to split a job into several jobs, each one processing a part of the
|
||||
work. This can be useful to avoid very long jobs, parallelize some task and get more specific
|
||||
errors. Usage is as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_split_delayable(self):
|
||||
(
|
||||
self # Can be a big recordset, let's say 1000 records
|
||||
.delayable()
|
||||
.generate_thumbnail((50, 50))
|
||||
.set(priority=30)
|
||||
.set(description=_("generate xxx"))
|
||||
.split(50) # Split the job in 20 jobs of 50 records each
|
||||
.delay()
|
||||
)
|
||||
|
||||
The ``split()`` method takes a ``chain`` boolean keyword argument. If set to
|
||||
True, the jobs will be chained, meaning that the next job will only start when the previous
|
||||
one is done:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_increment_var(self):
|
||||
(
|
||||
self
|
||||
.delayable()
|
||||
.increment_counter()
|
||||
.split(1, chain=True) # Will exceute the jobs one after the other
|
||||
.delay()
|
||||
)
|
||||
|
||||
|
||||
Enqueing Job Options
|
||||
--------------------
|
||||
|
||||
* priority: default is 10, the closest it is to 0, the faster it will be
|
||||
executed
|
||||
* eta: Estimated Time of Arrival of the job. It will not be executed before this
|
||||
date/time
|
||||
* max_retries: default is 5, maximum number of retries before giving up and set
|
||||
the job state to 'failed'. A value of 0 means infinite retries.
|
||||
* description: human description of the job. If not set, description is computed
|
||||
from the function doc or method name
|
||||
* channel: the complete name of the channel to use to process the function. If
|
||||
specified it overrides the one defined on the function
|
||||
* identity_key: key uniquely identifying the job, if specified and a job with
|
||||
the same key has not yet been run, the new job will not be created
|
||||
|
||||
Configure default options for jobs
|
||||
----------------------------------
|
||||
|
||||
In earlier versions, jobs could be configured using the ``@job`` decorator.
|
||||
This is now obsolete, they can be configured using optional ``queue.job.function``
|
||||
and ``queue.job.channel`` XML records.
|
||||
|
||||
Example of channel:
|
||||
|
||||
.. code-block:: XML
|
||||
|
||||
<record id="channel_sale" model="queue.job.channel">
|
||||
<field name="name">sale</field>
|
||||
<field name="parent_id" ref="queue_job.channel_root" />
|
||||
</record>
|
||||
|
||||
Example of job function:
|
||||
|
||||
.. code-block:: XML
|
||||
|
||||
<record id="job_function_sale_order_action_done" model="queue.job.function">
|
||||
<field name="model_id" ref="sale.model_sale_order" />
|
||||
<field name="method">action_done</field>
|
||||
<field name="channel_id" ref="channel_sale" />
|
||||
<field name="related_action" eval='{"func_name": "custom_related_action"}' />
|
||||
<field name="retry_pattern" eval="{1: 60, 2: 180, 3: 10, 5: 300}" />
|
||||
</record>
|
||||
|
||||
The general form for the ``name`` is: ``<model.name>.method``.
|
||||
|
||||
The channel, related action and retry pattern options are optional, they are
|
||||
documented below.
|
||||
|
||||
When writing modules, if 2+ modules add a job function or channel with the same
|
||||
name (and parent for channels), they'll be merged in the same record, even if
|
||||
they have different xmlids. On uninstall, the merged record is deleted when all
|
||||
the modules using it are uninstalled.
|
||||
|
||||
|
||||
**Job function: model**
|
||||
|
||||
If the function is defined in an abstract model, you can not write
|
||||
``<field name="model_id" ref="xml_id_of_the_abstract_model"</field>``
|
||||
but you have to define a function for each model that inherits from the abstract model.
|
||||
|
||||
|
||||
**Job function: channel**
|
||||
|
||||
The channel where the job will be delayed. The default channel is ``root``.
|
||||
|
||||
**Job function: related action**
|
||||
|
||||
The *Related Action* appears as a button on the Job's view.
|
||||
The button will execute the defined action.
|
||||
|
||||
The default one is to open the view of the record related to the job (form view
|
||||
when there is a single record, list view for several records).
|
||||
In many cases, the default related action is enough and doesn't need
|
||||
customization, but it can be customized by providing a dictionary on the job
|
||||
function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"enable": False,
|
||||
"func_name": "related_action_partner",
|
||||
"kwargs": {"name": "Partner"},
|
||||
}
|
||||
|
||||
* ``enable``: when ``False``, the button has no effect (default: ``True``)
|
||||
* ``func_name``: name of the method on ``queue.job`` that returns an action
|
||||
* ``kwargs``: extra arguments to pass to the related action method
|
||||
|
||||
Example of related action code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class QueueJob(models.Model):
|
||||
_inherit = 'queue.job'
|
||||
|
||||
def related_action_partner(self, name):
|
||||
self.ensure_one()
|
||||
model = self.model_name
|
||||
partner = self.records
|
||||
action = {
|
||||
'name': name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': model,
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_id': partner.id,
|
||||
}
|
||||
return action
|
||||
|
||||
|
||||
**Job function: retry pattern**
|
||||
|
||||
When a job fails with a retryable error type, it is automatically
|
||||
retried later. By default, the retry is always 10 minutes later.
|
||||
|
||||
A retry pattern can be configured on the job function. What a pattern represents
|
||||
is "from X tries, postpone to Y seconds". It is expressed as a dictionary where
|
||||
keys are tries and values are seconds to postpone as integers:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
1: 10,
|
||||
5: 20,
|
||||
10: 30,
|
||||
15: 300,
|
||||
}
|
||||
|
||||
Based on this configuration, we can tell that:
|
||||
|
||||
* 5 first retries are postponed 10 seconds later
|
||||
* retries 5 to 10 postponed 20 seconds later
|
||||
* retries 10 to 15 postponed 30 seconds later
|
||||
* all subsequent retries postponed 5 minutes later
|
||||
|
||||
**Job Context**
|
||||
|
||||
The context of the recordset of the job, or any recordset passed in arguments of
|
||||
a job, is transferred to the job according to an allow-list.
|
||||
|
||||
The default allow-list is `("tz", "lang", "allowed_company_ids", "force_company", "active_test")`. It can
|
||||
be customized in ``Base._job_prepare_context_before_enqueue_keys``.
|
||||
**Bypass jobs on running Odoo**
|
||||
|
||||
When you are developing (ie: connector modules) you might want
|
||||
to bypass the queue job and run your code immediately.
|
||||
|
||||
To do so you can set `QUEUE_JOB__NO_DELAY=1` in your environment.
|
||||
|
||||
**Bypass jobs in tests**
|
||||
|
||||
When writing tests on job-related methods is always tricky to deal with
|
||||
delayed recordsets. To make your testing life easier
|
||||
you can set `queue_job__no_delay=True` in the context.
|
||||
|
||||
Tip: you can do this at test case level like this
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context=dict(
|
||||
cls.env.context,
|
||||
queue_job__no_delay=True, # no jobs thanks
|
||||
))
|
||||
|
||||
Then all your tests execute the job methods synchronously
|
||||
without delaying any jobs.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
**Asserting enqueued jobs**
|
||||
|
||||
The recommended way to test jobs, rather than running them directly and synchronously is to
|
||||
split the tests in two parts:
|
||||
|
||||
* one test where the job is mocked (trap jobs with ``trap_jobs()`` and the test
|
||||
only verifies that the job has been delayed with the expected arguments
|
||||
* one test that only calls the method of the job synchronously, to validate the
|
||||
proper behavior of this method only
|
||||
|
||||
Proceeding this way means that you can prove that jobs will be enqueued properly
|
||||
at runtime, and it ensures your code does not have a different behavior in tests
|
||||
and in production (because running your jobs synchronously may have a different
|
||||
behavior as they are in the same transaction / in the middle of the method).
|
||||
Additionally, it gives more control on the arguments you want to pass when
|
||||
calling the job's method (synchronously, this time, in the second type of
|
||||
tests), and it makes tests smaller.
|
||||
|
||||
The best way to run such assertions on the enqueued jobs is to use
|
||||
``odoo.addons.queue_job.tests.common.trap_jobs()``.
|
||||
|
||||
Inside this context manager, instead of being added in the database's queue,
|
||||
jobs are pushed in an in-memory list. The context manager then provides useful
|
||||
helpers to verify that jobs have been enqueued with the expected arguments. It
|
||||
even can run the jobs of its list synchronously! Details in
|
||||
``odoo.addons.queue_job.tests.common.JobsTester``.
|
||||
|
||||
A very small example (more details in ``tests/common.py``):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# code
|
||||
def my_job_method(self, name, count):
|
||||
self.write({"name": " ".join([name] * count)
|
||||
|
||||
def method_to_test(self):
|
||||
count = self.env["other.model"].search_count([])
|
||||
self.with_delay(priority=15).my_job_method("Hi!", count=count)
|
||||
return count
|
||||
|
||||
# tests
|
||||
from odoo.addons.queue_job.tests.common import trap_jobs
|
||||
|
||||
# first test only check the expected behavior of the method and the proper
|
||||
# enqueuing of jobs
|
||||
def test_method_to_test(self):
|
||||
with trap_jobs() as trap:
|
||||
result = self.env["model"].method_to_test()
|
||||
expected_count = 12
|
||||
|
||||
trap.assert_jobs_count(1, only=self.env["model"].my_job_method)
|
||||
trap.assert_enqueued_job(
|
||||
self.env["model"].my_job_method,
|
||||
args=("Hi!",),
|
||||
kwargs=dict(count=expected_count),
|
||||
properties=dict(priority=15)
|
||||
)
|
||||
self.assertEqual(result, expected_count)
|
||||
|
||||
|
||||
# second test to validate the behavior of the job unitarily
|
||||
def test_my_job_method(self):
|
||||
record = self.env["model"].browse(1)
|
||||
record.my_job_method("Hi!", count=12)
|
||||
self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!")
|
||||
|
||||
If you prefer, you can still test the whole thing in a single test, by calling
|
||||
``jobs_tester.perform_enqueued_jobs()`` in your test.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_method_to_test(self):
|
||||
with trap_jobs() as trap:
|
||||
result = self.env["model"].method_to_test()
|
||||
expected_count = 12
|
||||
|
||||
trap.assert_jobs_count(1, only=self.env["model"].my_job_method)
|
||||
trap.assert_enqueued_job(
|
||||
self.env["model"].my_job_method,
|
||||
args=("Hi!",),
|
||||
kwargs=dict(count=expected_count),
|
||||
properties=dict(priority=15)
|
||||
)
|
||||
self.assertEqual(result, expected_count)
|
||||
|
||||
trap.perform_enqueued_jobs()
|
||||
|
||||
record = self.env["model"].browse(1)
|
||||
record.my_job_method("Hi!", count=12)
|
||||
self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!")
|
||||
|
||||
**Execute jobs synchronously when running Odoo**
|
||||
|
||||
When you are developing (ie: connector modules) you might want
|
||||
to bypass the queue job and run your code immediately.
|
||||
|
||||
To do so you can set ``QUEUE_JOB__NO_DELAY=1`` in your environment.
|
||||
|
||||
.. WARNING:: Do not do this in production
|
||||
|
||||
**Execute jobs synchronously in tests**
|
||||
|
||||
You should use ``trap_jobs``, really, but if for any reason you could not use it,
|
||||
and still need to have job methods executed synchronously in your tests, you can
|
||||
do so by setting ``queue_job__no_delay=True`` in the context.
|
||||
|
||||
Tip: you can do this at test case level like this
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context=dict(
|
||||
cls.env.context,
|
||||
queue_job__no_delay=True, # no jobs thanks
|
||||
))
|
||||
|
||||
Then all your tests execute the job methods synchronously without delaying any
|
||||
jobs.
|
||||
|
||||
In tests you'll have to mute the logger like:
|
||||
|
||||
@mute_logger('odoo.addons.queue_job.models.base')
|
||||
|
||||
.. NOTE:: in graphs of jobs, the ``queue_job__no_delay`` context key must be in at
|
||||
least one job's env of the graph for the whole graph to be executed synchronously
|
||||
|
||||
|
||||
Tips and tricks
|
||||
---------------
|
||||
|
||||
* **Idempotency** (https://www.restapitutorial.com/lessons/idempotency.html): The queue_job should be idempotent so they can be retried several times without impact on the data.
|
||||
* **The job should test at the very beginning its relevance**: the moment the job will be executed is unknown by design. So the first task of a job should be to check if the related work is still relevant at the moment of the execution.
|
||||
|
||||
Patterns
|
||||
--------
|
||||
Through the time, two main patterns emerged:
|
||||
|
||||
1. For data exposed to users, a model should store the data and the model should be the creator of the job. The job is kept hidden from the users
|
||||
2. For technical data, that are not exposed to the users, it is generally alright to create directly jobs with data passed as arguments to the job, without intermediary models.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* After creating a new database or installing ``queue_job`` on an
|
||||
existing database, Odoo must be restarted for the runner to detect it.
|
||||
|
||||
* When Odoo shuts down normally, it waits for running jobs to finish.
|
||||
However, when the Odoo server crashes or is otherwise force-stopped,
|
||||
running jobs are interrupted while the runner has no chance to know
|
||||
they have been aborted. In such situations, jobs may remain in
|
||||
``started`` or ``enqueued`` state after the Odoo server is halted.
|
||||
Since the runner has no way to know if they are actually running or
|
||||
not, and does not know for sure if it is safe to restart the jobs,
|
||||
it does not attempt to restart them automatically. Such stale jobs
|
||||
therefore fill the running queue and prevent other jobs to start.
|
||||
You must therefore requeue them manually, either from the Jobs view,
|
||||
or by running the following SQL statement *before starting Odoo*:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
update queue_job set state='pending' where state in ('started', 'enqueued')
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
.. [ The change log. The goal of this file is to help readers
|
||||
understand changes between version. The primary audience is
|
||||
end users and integrators. Purely technical changes such as
|
||||
code refactoring must not be mentioned here.
|
||||
|
||||
This file may contain ONE level of section titles, underlined
|
||||
with the ~ (tilde) character. Other section markers are
|
||||
forbidden and will likely break the structure of the README.rst
|
||||
or other documents where this fragment is included. ]
|
||||
|
||||
Next
|
||||
~~~~
|
||||
|
||||
* [ADD] Run jobrunner as a worker process instead of a thread in the main
|
||||
process (when running with --workers > 0)
|
||||
* [REF] ``@job`` and ``@related_action`` deprecated, any method can be delayed,
|
||||
and configured using ``queue.job.function`` records
|
||||
* [MIGRATION] from 13.0 branched at rev. e24ff4b
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/queue/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/queue/issues/new?body=module:%20queue_job%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Camptocamp
|
||||
* ACSONE SA/NV
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
||||
* Stéphane Bidoul <stephane.bidoul@acsone.eu>
|
||||
* Matthieu Dietrich <matthieu.dietrich@camptocamp.com>
|
||||
* Jos De Graeve <Jos.DeGraeve@apertoso.be>
|
||||
* David Lefever <dl@taktik.be>
|
||||
* Laurent Mignon <laurent.mignon@acsone.eu>
|
||||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu>
|
||||
* Cédric Pigeon <cedric.pigeon@acsone.eu>
|
||||
* Tatiana Deribina <tatiana.deribina@avoin.systems>
|
||||
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
|
||||
* Eric Antones <eantones@nuobit.com>
|
||||
* Simone Orsi <simone.orsi@camptocamp.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
.. |maintainer-guewen| image:: https://github.com/guewen.png?size=40px
|
||||
:target: https://github.com/guewen
|
||||
:alt: guewen
|
||||
|
||||
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|
||||
|
||||
|maintainer-guewen|
|
||||
|
||||
This module is part of the `OCA/queue <https://github.com/OCA/queue/tree/16.0/queue_job>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
@@ -1,10 +0,0 @@
|
||||
from . import controllers
|
||||
from . import fields
|
||||
from . import models
|
||||
from . import wizards
|
||||
from . import jobrunner
|
||||
from .post_init_hook import post_init_hook
|
||||
from .post_load import post_load
|
||||
|
||||
# shortcuts
|
||||
from .job import identity_exact
|
||||
@@ -1,35 +0,0 @@
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
{
|
||||
"name": "Job Queue",
|
||||
"version": "16.0.2.12.0",
|
||||
"author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/queue",
|
||||
"license": "LGPL-3",
|
||||
"category": "Generic Modules",
|
||||
"depends": ["mail", "base_sparse_field", "web"],
|
||||
"external_dependencies": {"python": ["requests"]},
|
||||
"data": [
|
||||
"security/security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"views/queue_job_views.xml",
|
||||
"views/queue_job_channel_views.xml",
|
||||
"views/queue_job_function_views.xml",
|
||||
"wizards/queue_jobs_to_done_views.xml",
|
||||
"wizards/queue_jobs_to_cancelled_views.xml",
|
||||
"wizards/queue_requeue_job_views.xml",
|
||||
"views/queue_job_menus.xml",
|
||||
"data/queue_data.xml",
|
||||
"data/queue_job_function_data.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"/queue_job/static/src/views/**/*",
|
||||
],
|
||||
},
|
||||
"installable": True,
|
||||
"development_status": "Mature",
|
||||
"maintainers": ["guewen"],
|
||||
"post_init_hook": "post_init_hook",
|
||||
"post_load": "post_load",
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from . import main
|
||||
@@ -1,320 +0,0 @@
|
||||
# Copyright (c) 2015-2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# Copyright 2013-2016 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
import traceback
|
||||
from io import StringIO
|
||||
|
||||
from psycopg2 import OperationalError, errorcodes
|
||||
from werkzeug.exceptions import BadRequest, Forbidden
|
||||
|
||||
from odoo import SUPERUSER_ID, _, api, http, registry, tools
|
||||
from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY
|
||||
|
||||
from ..delay import chain, group
|
||||
from ..exception import FailedJobError, NothingToDoJob, RetryableJobError
|
||||
from ..job import ENQUEUED, Job
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
PG_RETRY = 5 # seconds
|
||||
|
||||
DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE = 5
|
||||
|
||||
|
||||
class RunJobController(http.Controller):
|
||||
def _try_perform_job(self, env, job):
|
||||
"""Try to perform the job."""
|
||||
job.set_started()
|
||||
job.store()
|
||||
env.cr.commit()
|
||||
job.lock()
|
||||
|
||||
_logger.debug("%s started", job)
|
||||
|
||||
job.perform()
|
||||
# Triggers any stored computed fields before calling 'set_done'
|
||||
# so that will be part of the 'exec_time'
|
||||
env.flush_all()
|
||||
job.set_done()
|
||||
job.store()
|
||||
env.flush_all()
|
||||
env.cr.commit()
|
||||
_logger.debug("%s done", job)
|
||||
|
||||
def _enqueue_dependent_jobs(self, env, job):
|
||||
tries = 0
|
||||
while True:
|
||||
try:
|
||||
job.enqueue_waiting()
|
||||
except OperationalError as err:
|
||||
# Automatically retry the typical transaction serialization
|
||||
# errors
|
||||
if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
|
||||
raise
|
||||
if tries >= DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE:
|
||||
_logger.info(
|
||||
"%s, maximum number of tries reached to update dependencies",
|
||||
errorcodes.lookup(err.pgcode),
|
||||
)
|
||||
raise
|
||||
wait_time = random.uniform(0.0, 2**tries)
|
||||
tries += 1
|
||||
_logger.info(
|
||||
"%s, retry %d/%d in %.04f sec...",
|
||||
errorcodes.lookup(err.pgcode),
|
||||
tries,
|
||||
DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE,
|
||||
wait_time,
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
break
|
||||
|
||||
@http.route(
|
||||
"/queue_job/runjob",
|
||||
type="http",
|
||||
auth="none",
|
||||
save_session=False,
|
||||
readonly=False,
|
||||
)
|
||||
def runjob(self, db, job_uuid, **kw):
|
||||
http.request.session.db = db
|
||||
env = http.request.env(user=SUPERUSER_ID)
|
||||
|
||||
def retry_postpone(job, message, seconds=None):
|
||||
job.env.clear()
|
||||
with registry(job.env.cr.dbname).cursor() as new_cr:
|
||||
job.env = api.Environment(new_cr, SUPERUSER_ID, {})
|
||||
job.postpone(result=message, seconds=seconds)
|
||||
job.set_pending(reset_retry=False)
|
||||
job.store()
|
||||
|
||||
# ensure the job to run is in the correct state and lock the record
|
||||
env.cr.execute(
|
||||
"SELECT state FROM queue_job WHERE uuid=%s AND state=%s FOR UPDATE",
|
||||
(job_uuid, ENQUEUED),
|
||||
)
|
||||
if not env.cr.fetchone():
|
||||
_logger.warning(
|
||||
"was requested to run job %s, but it does not exist, "
|
||||
"or is not in state %s",
|
||||
job_uuid,
|
||||
ENQUEUED,
|
||||
)
|
||||
return ""
|
||||
|
||||
job = Job.load(env, job_uuid)
|
||||
assert job and job.state == ENQUEUED
|
||||
|
||||
try:
|
||||
try:
|
||||
self._try_perform_job(env, job)
|
||||
except OperationalError as err:
|
||||
# Automatically retry the typical transaction serialization
|
||||
# errors
|
||||
if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
|
||||
raise
|
||||
|
||||
_logger.debug("%s OperationalError, postponed", job)
|
||||
raise RetryableJobError(
|
||||
tools.ustr(err.pgerror, errors="replace"), seconds=PG_RETRY
|
||||
) from err
|
||||
|
||||
except NothingToDoJob as err:
|
||||
if str(err):
|
||||
msg = str(err)
|
||||
else:
|
||||
msg = _("Job interrupted and set to Done: nothing to do.")
|
||||
job.set_done(msg)
|
||||
job.store()
|
||||
env.cr.commit()
|
||||
|
||||
except RetryableJobError as err:
|
||||
# delay the job later, requeue
|
||||
retry_postpone(job, str(err), seconds=err.seconds)
|
||||
_logger.debug("%s postponed", job)
|
||||
# Do not trigger the error up because we don't want an exception
|
||||
# traceback in the logs we should have the traceback when all
|
||||
# retries are exhausted
|
||||
env.cr.rollback()
|
||||
return ""
|
||||
|
||||
except (FailedJobError, Exception) as orig_exception:
|
||||
buff = StringIO()
|
||||
traceback.print_exc(file=buff)
|
||||
traceback_txt = buff.getvalue()
|
||||
_logger.error(traceback_txt)
|
||||
job.env.clear()
|
||||
with registry(job.env.cr.dbname).cursor() as new_cr:
|
||||
job.env = job.env(cr=new_cr)
|
||||
vals = self._get_failure_values(job, traceback_txt, orig_exception)
|
||||
job.set_failed(**vals)
|
||||
job.store()
|
||||
buff.close()
|
||||
raise
|
||||
|
||||
_logger.debug("%s enqueue depends started", job)
|
||||
self._enqueue_dependent_jobs(env, job)
|
||||
_logger.debug("%s enqueue depends done", job)
|
||||
|
||||
return ""
|
||||
|
||||
def _get_failure_values(self, job, traceback_txt, orig_exception):
|
||||
"""Collect relevant data from exception."""
|
||||
exception_name = orig_exception.__class__.__name__
|
||||
if hasattr(orig_exception, "__module__"):
|
||||
exception_name = orig_exception.__module__ + "." + exception_name
|
||||
exc_message = (
|
||||
orig_exception.args[0] if orig_exception.args else str(orig_exception)
|
||||
)
|
||||
return {
|
||||
"exc_info": traceback_txt,
|
||||
"exc_name": exception_name,
|
||||
"exc_message": exc_message,
|
||||
}
|
||||
|
||||
# flake8: noqa: C901
|
||||
@http.route("/queue_job/create_test_job", type="http", auth="user")
|
||||
def create_test_job(
|
||||
self,
|
||||
priority=None,
|
||||
max_retries=None,
|
||||
channel=None,
|
||||
description="Test job",
|
||||
size=1,
|
||||
failure_rate=0,
|
||||
):
|
||||
"""Create test jobs
|
||||
|
||||
Examples of urls:
|
||||
|
||||
* http://127.0.0.1:8069/queue_job/create_test_job: single job
|
||||
* http://127.0.0.1:8069/queue_job/create_test_job?size=10: a graph of 10 jobs
|
||||
* http://127.0.0.1:8069/queue_job/create_test_job?size=10&failure_rate=0.5:
|
||||
a graph of 10 jobs, half will fail
|
||||
|
||||
"""
|
||||
if not http.request.env.user.has_group("base.group_erp_manager"):
|
||||
raise Forbidden(_("Access Denied"))
|
||||
|
||||
if failure_rate is not None:
|
||||
try:
|
||||
failure_rate = float(failure_rate)
|
||||
except (ValueError, TypeError):
|
||||
failure_rate = 0
|
||||
|
||||
if not (0 <= failure_rate <= 1):
|
||||
raise BadRequest("failure_rate must be between 0 and 1")
|
||||
|
||||
if size is not None:
|
||||
try:
|
||||
size = int(size)
|
||||
except (ValueError, TypeError):
|
||||
size = 1
|
||||
|
||||
if priority is not None:
|
||||
try:
|
||||
priority = int(priority)
|
||||
except ValueError:
|
||||
priority = None
|
||||
|
||||
if max_retries is not None:
|
||||
try:
|
||||
max_retries = int(max_retries)
|
||||
except ValueError:
|
||||
max_retries = None
|
||||
|
||||
if size == 1:
|
||||
return self._create_single_test_job(
|
||||
priority=priority,
|
||||
max_retries=max_retries,
|
||||
channel=channel,
|
||||
description=description,
|
||||
failure_rate=failure_rate,
|
||||
)
|
||||
|
||||
if size > 1:
|
||||
return self._create_graph_test_jobs(
|
||||
size,
|
||||
priority=priority,
|
||||
max_retries=max_retries,
|
||||
channel=channel,
|
||||
description=description,
|
||||
failure_rate=failure_rate,
|
||||
)
|
||||
return ""
|
||||
|
||||
def _create_single_test_job(
|
||||
self,
|
||||
priority=None,
|
||||
max_retries=None,
|
||||
channel=None,
|
||||
description="Test job",
|
||||
size=1,
|
||||
failure_rate=0,
|
||||
):
|
||||
delayed = (
|
||||
http.request.env["queue.job"]
|
||||
.with_delay(
|
||||
priority=priority,
|
||||
max_retries=max_retries,
|
||||
channel=channel,
|
||||
description=description,
|
||||
)
|
||||
._test_job(failure_rate=failure_rate)
|
||||
)
|
||||
return "job uuid: %s" % (delayed.db_record().uuid,)
|
||||
|
||||
TEST_GRAPH_MAX_PER_GROUP = 5
|
||||
|
||||
def _create_graph_test_jobs(
|
||||
self,
|
||||
size,
|
||||
priority=None,
|
||||
max_retries=None,
|
||||
channel=None,
|
||||
description="Test job",
|
||||
failure_rate=0,
|
||||
):
|
||||
model = http.request.env["queue.job"]
|
||||
current_count = 0
|
||||
|
||||
possible_grouping_methods = (chain, group)
|
||||
|
||||
tails = [] # we can connect new graph chains/groups to tails
|
||||
root_delayable = None
|
||||
while current_count < size:
|
||||
jobs_count = min(
|
||||
size - current_count, random.randint(1, self.TEST_GRAPH_MAX_PER_GROUP)
|
||||
)
|
||||
|
||||
jobs = []
|
||||
for __ in range(jobs_count):
|
||||
current_count += 1
|
||||
jobs.append(
|
||||
model.delayable(
|
||||
priority=priority,
|
||||
max_retries=max_retries,
|
||||
channel=channel,
|
||||
description="%s #%d" % (description, current_count),
|
||||
)._test_job(failure_rate=failure_rate)
|
||||
)
|
||||
|
||||
grouping = random.choice(possible_grouping_methods)
|
||||
delayable = grouping(*jobs)
|
||||
if not root_delayable:
|
||||
root_delayable = delayable
|
||||
else:
|
||||
tail_delayable = random.choice(tails)
|
||||
tail_delayable.on_done(delayable)
|
||||
tails.append(delayable)
|
||||
|
||||
root_delayable.delay()
|
||||
|
||||
return "graph uuid: %s" % (
|
||||
list(root_delayable._head())[0]._generated_job.graph_uuid,
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Queue-job-related subtypes for messaging / Chatter -->
|
||||
<record id="mt_job_failed" model="mail.message.subtype">
|
||||
<field name="name">Job failed</field>
|
||||
<field name="res_model">queue.job</field>
|
||||
<field name="default" eval="True" />
|
||||
</record>
|
||||
<record id="ir_cron_autovacuum_queue_jobs" model="ir.cron">
|
||||
<field name="name">AutoVacuum Job Queue</field>
|
||||
<field ref="model_queue_job" name="model_id" />
|
||||
<field eval="True" name="active" />
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field eval="False" name="doall" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.autovacuum()</field>
|
||||
</record>
|
||||
</data>
|
||||
<data noupdate="0">
|
||||
<record model="queue.job.channel" id="channel_root">
|
||||
<field name="name">root</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,6 +0,0 @@
|
||||
<odoo noupdate="1">
|
||||
<record id="job_function_queue_job__test_job" model="queue.job.function">
|
||||
<field name="model_id" ref="queue_job.model_queue_job" />
|
||||
<field name="method">_test_job</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,666 +0,0 @@
|
||||
# Copyright 2019 Camptocamp
|
||||
# Copyright 2019 Guewen Baconnier
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import uuid
|
||||
from collections import defaultdict, deque
|
||||
|
||||
from .job import Job
|
||||
from .utils import must_run_without_delay
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def group(*delayables):
|
||||
"""Return a group of delayable to form a graph
|
||||
|
||||
A group means that jobs can be executed concurrently.
|
||||
A job or a group of jobs depending on a group can be executed only after
|
||||
all the jobs of the group are done.
|
||||
|
||||
Shortcut to :class:`~odoo.addons.queue_job.delay.DelayableGroup`.
|
||||
|
||||
Example::
|
||||
|
||||
g1 = group(delayable1, delayable2)
|
||||
g2 = group(delayable3, delayable4)
|
||||
g1.on_done(g2)
|
||||
g1.delay()
|
||||
"""
|
||||
return DelayableGroup(*delayables)
|
||||
|
||||
|
||||
def chain(*delayables):
|
||||
"""Return a chain of delayable to form a graph
|
||||
|
||||
A chain means that jobs must be executed sequentially.
|
||||
A job or a group of jobs depending on a group can be executed only after
|
||||
the last job of the chain is done.
|
||||
|
||||
Shortcut to :class:`~odoo.addons.queue_job.delay.DelayableChain`.
|
||||
|
||||
Example::
|
||||
|
||||
chain1 = chain(delayable1, delayable2, delayable3)
|
||||
chain2 = chain(delayable4, delayable5, delayable6)
|
||||
chain1.on_done(chain2)
|
||||
chain1.delay()
|
||||
"""
|
||||
return DelayableChain(*delayables)
|
||||
|
||||
|
||||
class Graph:
|
||||
"""Acyclic directed graph holding vertices of any hashable type
|
||||
|
||||
This graph is not specifically designed to hold :class:`~Delayable`
|
||||
instances, although ultimately it is used for this purpose.
|
||||
"""
|
||||
|
||||
__slots__ = "_graph"
|
||||
|
||||
def __init__(self, graph=None):
|
||||
if graph:
|
||||
self._graph = graph
|
||||
else:
|
||||
self._graph = {}
|
||||
|
||||
def add_vertex(self, vertex):
|
||||
"""Add a vertex
|
||||
|
||||
Has no effect if called several times with the same vertex
|
||||
"""
|
||||
self._graph.setdefault(vertex, set())
|
||||
|
||||
def add_edge(self, parent, child):
|
||||
"""Add an edge between a parent and a child vertex
|
||||
|
||||
Has no effect if called several times with the same pair of vertices
|
||||
"""
|
||||
self.add_vertex(child)
|
||||
self._graph.setdefault(parent, set()).add(child)
|
||||
|
||||
def vertices(self):
|
||||
"""Return the vertices (nodes) of the graph"""
|
||||
return set(self._graph)
|
||||
|
||||
def edges(self):
|
||||
"""Return the edges (links) of the graph"""
|
||||
links = []
|
||||
for vertex, neighbours in self._graph.items():
|
||||
for neighbour in neighbours:
|
||||
links.append((vertex, neighbour))
|
||||
return links
|
||||
|
||||
# from
|
||||
# https://codereview.stackexchange.com/questions/55767/finding-all-paths-from-a-given-graph
|
||||
def paths(self, vertex):
|
||||
"""Generate the maximal cycle-free paths in graph starting at vertex.
|
||||
|
||||
>>> g = {1: [2, 3], 2: [3, 4], 3: [1], 4: []}
|
||||
>>> sorted(self.paths(1))
|
||||
[[1, 2, 3], [1, 2, 4], [1, 3]]
|
||||
>>> sorted(self.paths(3))
|
||||
[[3, 1, 2, 4]]
|
||||
"""
|
||||
path = [vertex] # path traversed so far
|
||||
seen = {vertex} # set of vertices in path
|
||||
|
||||
def search():
|
||||
dead_end = True
|
||||
for neighbour in self._graph[path[-1]]:
|
||||
if neighbour not in seen:
|
||||
dead_end = False
|
||||
seen.add(neighbour)
|
||||
path.append(neighbour)
|
||||
yield from search()
|
||||
path.pop()
|
||||
seen.remove(neighbour)
|
||||
if dead_end:
|
||||
yield list(path)
|
||||
|
||||
yield from search()
|
||||
|
||||
def topological_sort(self):
|
||||
"""Yields a proposed order of nodes to respect dependencies
|
||||
|
||||
The order is not unique, the result may vary, but it is guaranteed
|
||||
that a node depending on another is not yielded before.
|
||||
It assumes the graph has no cycle.
|
||||
"""
|
||||
depends_per_node = defaultdict(int)
|
||||
for __, tail in self.edges():
|
||||
depends_per_node[tail] += 1
|
||||
|
||||
# the queue contains only elements for which all dependencies
|
||||
# are resolved
|
||||
queue = deque(self.root_vertices())
|
||||
while queue:
|
||||
vertex = queue.popleft()
|
||||
yield vertex
|
||||
for node in self._graph[vertex]:
|
||||
depends_per_node[node] -= 1
|
||||
if not depends_per_node[node]:
|
||||
queue.append(node)
|
||||
|
||||
def root_vertices(self):
|
||||
"""Returns the root vertices
|
||||
|
||||
meaning they do not depend on any other job.
|
||||
"""
|
||||
dependency_vertices = set()
|
||||
for dependencies in self._graph.values():
|
||||
dependency_vertices.update(dependencies)
|
||||
return set(self._graph.keys()) - dependency_vertices
|
||||
|
||||
def __repr__(self):
|
||||
paths = [path for vertex in self.root_vertices() for path in self.paths(vertex)]
|
||||
lines = []
|
||||
for path in paths:
|
||||
lines.append(" → ".join(repr(vertex) for vertex in path))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class DelayableGraph(Graph):
|
||||
"""Directed Graph for :class:`~Delayable` dependencies
|
||||
|
||||
It connects together the :class:`~Delayable`, :class:`~DelayableGroup` and
|
||||
:class:`~DelayableChain` graphs, and creates then enqueued the jobs.
|
||||
"""
|
||||
|
||||
def _merge_graph(self, graph):
|
||||
"""Merge a graph in the current graph
|
||||
|
||||
It takes each vertex, which can be :class:`~Delayable`,
|
||||
:class:`~DelayableChain` or :class:`~DelayableGroup`, and updates the
|
||||
current graph with the edges between Delayable objects (connecting
|
||||
heads and tails of the groups and chains), so that at the end, the
|
||||
graph contains only Delayable objects and their links.
|
||||
"""
|
||||
for vertex, neighbours in graph._graph.items():
|
||||
tails = vertex._tail()
|
||||
for tail in tails:
|
||||
# connect the tails with the heads of each node
|
||||
heads = {head for n in neighbours for head in n._head()}
|
||||
self._graph.setdefault(tail, set()).update(heads)
|
||||
|
||||
def _connect_graphs(self):
|
||||
"""Visit the vertices' graphs and connect them, return the whole graph
|
||||
|
||||
Build a new graph, walk the vertices and their related vertices, merge
|
||||
their graph in the new one, until we have visited all the vertices
|
||||
"""
|
||||
graph = DelayableGraph()
|
||||
graph._merge_graph(self)
|
||||
|
||||
seen = set()
|
||||
visit_stack = deque([self])
|
||||
while visit_stack:
|
||||
current = visit_stack.popleft()
|
||||
if current in seen:
|
||||
continue
|
||||
|
||||
vertices = current.vertices()
|
||||
for vertex in vertices:
|
||||
vertex_graph = vertex._graph
|
||||
graph._merge_graph(vertex_graph)
|
||||
visit_stack.append(vertex_graph)
|
||||
|
||||
seen.add(current)
|
||||
|
||||
return graph
|
||||
|
||||
def _has_to_execute_directly(self, vertices):
|
||||
"""Used for tests to run tests directly instead of storing them
|
||||
|
||||
In tests, prefer to use
|
||||
:func:`odoo.addons.queue_job.tests.common.trap_jobs`.
|
||||
"""
|
||||
envs = {vertex.recordset.env for vertex in vertices}
|
||||
for env in envs:
|
||||
if must_run_without_delay(env):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _ensure_same_graph_uuid(jobs):
|
||||
"""Set the same graph uuid on all jobs of the same graph"""
|
||||
jobs_count = len(jobs)
|
||||
if jobs_count == 0:
|
||||
raise ValueError("Expecting jobs")
|
||||
elif jobs_count == 1:
|
||||
if jobs[0].graph_uuid:
|
||||
raise ValueError(
|
||||
"Job %s is a single job, it should not"
|
||||
" have a graph uuid" % (jobs[0],)
|
||||
)
|
||||
else:
|
||||
graph_uuids = {job.graph_uuid for job in jobs if job.graph_uuid}
|
||||
if len(graph_uuids) > 1:
|
||||
raise ValueError("Jobs cannot have dependencies between several graphs")
|
||||
elif len(graph_uuids) == 1:
|
||||
graph_uuid = graph_uuids.pop()
|
||||
else:
|
||||
graph_uuid = str(uuid.uuid4())
|
||||
for job in jobs:
|
||||
job.graph_uuid = graph_uuid
|
||||
|
||||
def delay(self):
|
||||
"""Build the whole graph, creates jobs and delay them"""
|
||||
graph = self._connect_graphs()
|
||||
|
||||
vertices = graph.vertices()
|
||||
|
||||
for vertex in vertices:
|
||||
vertex._build_job()
|
||||
|
||||
self._ensure_same_graph_uuid([vertex._generated_job for vertex in vertices])
|
||||
|
||||
if self._has_to_execute_directly(vertices):
|
||||
self._execute_graph_direct(graph)
|
||||
return
|
||||
|
||||
for vertex, neighbour in graph.edges():
|
||||
neighbour._generated_job.add_depends({vertex._generated_job})
|
||||
|
||||
# If all the jobs of the graph have another job with the same identity,
|
||||
# we do not create them. Maybe we should check that the found jobs are
|
||||
# part of the same graph, but not sure it's really required...
|
||||
# Also, maybe we want to check only the root jobs.
|
||||
existing_mapping = {}
|
||||
for vertex in vertices:
|
||||
if not vertex.identity_key:
|
||||
continue
|
||||
generated_job = vertex._generated_job
|
||||
existing = generated_job.job_record_with_same_identity_key()
|
||||
if not existing:
|
||||
# at least one does not exist yet, we'll delay the whole graph
|
||||
existing_mapping.clear()
|
||||
break
|
||||
existing_mapping[vertex] = existing
|
||||
|
||||
# We'll replace the generated jobs by the existing ones, so callers
|
||||
# can retrieve the existing job in "_generated_job".
|
||||
# existing_mapping contains something only if *all* the job with an
|
||||
# identity have an existing one.
|
||||
for vertex, existing in existing_mapping.items():
|
||||
vertex._generated_job = existing
|
||||
return
|
||||
|
||||
for vertex in vertices:
|
||||
vertex._generated_job.store()
|
||||
|
||||
def _execute_graph_direct(self, graph):
|
||||
for delayable in graph.topological_sort():
|
||||
delayable._execute_direct()
|
||||
|
||||
|
||||
class DelayableChain:
|
||||
"""Chain of delayables to form a graph
|
||||
|
||||
Delayables can be other :class:`~Delayable`, :class:`~DelayableChain` or
|
||||
:class:`~DelayableGroup` objects.
|
||||
|
||||
A chain means that jobs must be executed sequentially.
|
||||
A job or a group of jobs depending on a group can be executed only after
|
||||
the last job of the chain is done.
|
||||
|
||||
Chains can be connected to other Delayable, DelayableChain or
|
||||
DelayableGroup objects by using :meth:`~done`.
|
||||
|
||||
A Chain is enqueued by calling :meth:`~delay`, which delays the whole
|
||||
graph.
|
||||
Important: :meth:`~delay` must be called on the top-level
|
||||
delayable/chain/group object of the graph.
|
||||
"""
|
||||
|
||||
__slots__ = ("_graph", "__head", "__tail")
|
||||
|
||||
def __init__(self, *delayables):
|
||||
self._graph = DelayableGraph()
|
||||
iter_delayables = iter(delayables)
|
||||
head = next(iter_delayables)
|
||||
self.__head = head
|
||||
self._graph.add_vertex(head)
|
||||
for neighbour in iter_delayables:
|
||||
self._graph.add_edge(head, neighbour)
|
||||
head = neighbour
|
||||
self.__tail = head
|
||||
|
||||
def _head(self):
|
||||
return self.__head._tail()
|
||||
|
||||
def _tail(self):
|
||||
return self.__tail._head()
|
||||
|
||||
def __repr__(self):
|
||||
inner_graph = "\n\t".join(repr(self._graph).split("\n"))
|
||||
return "DelayableChain(\n\t{}\n)".format(inner_graph)
|
||||
|
||||
def on_done(self, *delayables):
|
||||
"""Connects the current chain to other delayables/chains/groups
|
||||
|
||||
The delayables/chains/groups passed in the parameters will be executed
|
||||
when the current Chain is done.
|
||||
"""
|
||||
for delayable in delayables:
|
||||
self._graph.add_edge(self.__tail, delayable)
|
||||
return self
|
||||
|
||||
def delay(self):
|
||||
"""Delay the whole graph"""
|
||||
self._graph.delay()
|
||||
|
||||
|
||||
class DelayableGroup:
|
||||
"""Group of delayables to form a graph
|
||||
|
||||
Delayables can be other :class:`~Delayable`, :class:`~DelayableChain` or
|
||||
:class:`~DelayableGroup` objects.
|
||||
|
||||
A group means that jobs must be executed sequentially.
|
||||
A job or a group of jobs depending on a group can be executed only after
|
||||
the all the jobs of the group are done.
|
||||
|
||||
Groups can be connected to other Delayable, DelayableChain or
|
||||
DelayableGroup objects by using :meth:`~done`.
|
||||
|
||||
A group is enqueued by calling :meth:`~delay`, which delays the whole
|
||||
graph.
|
||||
Important: :meth:`~delay` must be called on the top-level
|
||||
delayable/chain/group object of the graph.
|
||||
"""
|
||||
|
||||
__slots__ = ("_graph", "_delayables")
|
||||
|
||||
def __init__(self, *delayables):
|
||||
self._graph = DelayableGraph()
|
||||
self._delayables = set(delayables)
|
||||
for delayable in delayables:
|
||||
self._graph.add_vertex(delayable)
|
||||
|
||||
def _head(self):
|
||||
return itertools.chain.from_iterable(node._head() for node in self._delayables)
|
||||
|
||||
def _tail(self):
|
||||
return itertools.chain.from_iterable(node._tail() for node in self._delayables)
|
||||
|
||||
def __repr__(self):
|
||||
inner_graph = "\n\t".join(repr(self._graph).split("\n"))
|
||||
return "DelayableGroup(\n\t{}\n)".format(inner_graph)
|
||||
|
||||
def on_done(self, *delayables):
|
||||
"""Connects the current group to other delayables/chains/groups
|
||||
|
||||
The delayables/chains/groups passed in the parameters will be executed
|
||||
when the current Group is done.
|
||||
"""
|
||||
for parent in self._delayables:
|
||||
for child in delayables:
|
||||
self._graph.add_edge(parent, child)
|
||||
return self
|
||||
|
||||
def delay(self):
|
||||
"""Delay the whole graph"""
|
||||
self._graph.delay()
|
||||
|
||||
|
||||
class Delayable:
|
||||
"""Unit of a graph, one Delayable will lead to an enqueued job
|
||||
|
||||
Delayables can have dependencies on each others, as well as dependencies on
|
||||
:class:`~DelayableGroup` or :class:`~DelayableChain` objects.
|
||||
|
||||
This class will generally not be used directly, it is used internally
|
||||
by :meth:`~odoo.addons.queue_job.models.base.Base.delayable`. Look
|
||||
in the base model for more details.
|
||||
|
||||
Delayables can be connected to other Delayable, DelayableChain or
|
||||
DelayableGroup objects by using :meth:`~done`.
|
||||
|
||||
Properties of the future job can be set using the :meth:`~set` method,
|
||||
which always return ``self``::
|
||||
|
||||
delayable.set(priority=15).set({"max_retries": 5, "eta": 15}).delay()
|
||||
|
||||
It can be used for example to set properties dynamically.
|
||||
|
||||
A Delayable is enqueued by calling :meth:`delay()`, which delays the whole
|
||||
graph.
|
||||
Important: :meth:`delay()` must be called on the top-level
|
||||
delayable/chain/group object of the graph.
|
||||
"""
|
||||
|
||||
_properties = (
|
||||
"priority",
|
||||
"eta",
|
||||
"max_retries",
|
||||
"description",
|
||||
"channel",
|
||||
"identity_key",
|
||||
)
|
||||
__slots__ = _properties + (
|
||||
"recordset",
|
||||
"_graph",
|
||||
"_job_method",
|
||||
"_job_args",
|
||||
"_job_kwargs",
|
||||
"_generated_job",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
recordset,
|
||||
priority=None,
|
||||
eta=None,
|
||||
max_retries=None,
|
||||
description=None,
|
||||
channel=None,
|
||||
identity_key=None,
|
||||
):
|
||||
self._graph = DelayableGraph()
|
||||
self._graph.add_vertex(self)
|
||||
|
||||
self.recordset = recordset
|
||||
|
||||
self.priority = priority
|
||||
self.eta = eta
|
||||
self.max_retries = max_retries
|
||||
self.description = description
|
||||
self.channel = channel
|
||||
self.identity_key = identity_key
|
||||
|
||||
self._job_method = None
|
||||
self._job_args = ()
|
||||
self._job_kwargs = {}
|
||||
|
||||
self._generated_job = None
|
||||
|
||||
def _head(self):
|
||||
return [self]
|
||||
|
||||
def _tail(self):
|
||||
return [self]
|
||||
|
||||
def __repr__(self):
|
||||
return "Delayable({}.{}({}, {}))".format(
|
||||
self.recordset,
|
||||
self._job_method.__name__ if self._job_method else "",
|
||||
self._job_args,
|
||||
self._job_kwargs,
|
||||
)
|
||||
|
||||
def __del__(self):
|
||||
if not self._generated_job:
|
||||
_logger.warning("Delayable %s was prepared but never delayed", self)
|
||||
|
||||
def _set_from_dict(self, properties):
|
||||
for key, value in properties.items():
|
||||
if key not in self._properties:
|
||||
raise ValueError("No property %s" % (key,))
|
||||
setattr(self, key, value)
|
||||
|
||||
def set(self, *args, **kwargs):
|
||||
"""Set job properties and return self
|
||||
|
||||
The values can be either a dictionary and/or keywork args
|
||||
"""
|
||||
if args:
|
||||
# args must be a dict
|
||||
self._set_from_dict(*args)
|
||||
self._set_from_dict(kwargs)
|
||||
return self
|
||||
|
||||
def on_done(self, *delayables):
|
||||
"""Connects the current Delayable to other delayables/chains/groups
|
||||
|
||||
The delayables/chains/groups passed in the parameters will be executed
|
||||
when the current Delayable is done.
|
||||
"""
|
||||
for child in delayables:
|
||||
self._graph.add_edge(self, child)
|
||||
return self
|
||||
|
||||
def delay(self):
|
||||
"""Delay the whole graph"""
|
||||
self._graph.delay()
|
||||
|
||||
def split(self, size, chain=False):
|
||||
"""Split the Delayables.
|
||||
|
||||
Use `DelayableGroup` or `DelayableChain`
|
||||
if `chain` is True containing batches of size `size`
|
||||
"""
|
||||
if not self._job_method:
|
||||
raise ValueError("No method set on the Delayable")
|
||||
|
||||
total_records = len(self.recordset)
|
||||
|
||||
delayables = []
|
||||
for index in range(0, total_records, size):
|
||||
recordset = self.recordset[index : index + size]
|
||||
delayable = Delayable(
|
||||
recordset,
|
||||
priority=self.priority,
|
||||
eta=self.eta,
|
||||
max_retries=self.max_retries,
|
||||
description=self.description,
|
||||
channel=self.channel,
|
||||
identity_key=self.identity_key,
|
||||
)
|
||||
# Update the __self__
|
||||
delayable._job_method = getattr(recordset, self._job_method.__name__)
|
||||
delayable._job_args = self._job_args
|
||||
delayable._job_kwargs = self._job_kwargs
|
||||
|
||||
delayables.append(delayable)
|
||||
|
||||
description = self.description or (
|
||||
self._job_method.__doc__.splitlines()[0].strip()
|
||||
if self._job_method.__doc__
|
||||
else "{}.{}".format(self.recordset._name, self._job_method.__name__)
|
||||
)
|
||||
for index, delayable in enumerate(delayables):
|
||||
delayable.set(
|
||||
description="%s (split %s/%s)"
|
||||
% (description, index + 1, len(delayables))
|
||||
)
|
||||
|
||||
# Prevent warning on deletion
|
||||
self._generated_job = True
|
||||
|
||||
return (DelayableChain if chain else DelayableGroup)(*delayables)
|
||||
|
||||
def _build_job(self):
|
||||
if self._generated_job:
|
||||
return self._generated_job
|
||||
self._generated_job = Job(
|
||||
self._job_method,
|
||||
args=self._job_args,
|
||||
kwargs=self._job_kwargs,
|
||||
priority=self.priority,
|
||||
max_retries=self.max_retries,
|
||||
eta=self.eta,
|
||||
description=self.description,
|
||||
channel=self.channel,
|
||||
identity_key=self.identity_key,
|
||||
)
|
||||
return self._generated_job
|
||||
|
||||
def _store_args(self, *args, **kwargs):
|
||||
self._job_args = args
|
||||
self._job_kwargs = kwargs
|
||||
return self
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.__slots__:
|
||||
return super().__getattr__(name)
|
||||
if name in self.recordset:
|
||||
raise AttributeError(
|
||||
"only methods can be delayed (%s called on %s)" % (name, self.recordset)
|
||||
)
|
||||
recordset_method = getattr(self.recordset, name)
|
||||
self._job_method = recordset_method
|
||||
return self._store_args
|
||||
|
||||
def _execute_direct(self):
|
||||
assert self._generated_job
|
||||
self._generated_job.perform()
|
||||
|
||||
|
||||
class DelayableRecordset:
|
||||
"""Allow to delay a method for a recordset (shortcut way)
|
||||
|
||||
Usage::
|
||||
|
||||
delayable = DelayableRecordset(recordset, priority=20)
|
||||
delayable.method(args, kwargs)
|
||||
|
||||
The method call will be processed asynchronously in the job queue, with
|
||||
the passed arguments.
|
||||
|
||||
This class will generally not be used directly, it is used internally
|
||||
by :meth:`~odoo.addons.queue_job.models.base.Base.with_delay`
|
||||
"""
|
||||
|
||||
__slots__ = ("delayable",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
recordset,
|
||||
priority=None,
|
||||
eta=None,
|
||||
max_retries=None,
|
||||
description=None,
|
||||
channel=None,
|
||||
identity_key=None,
|
||||
):
|
||||
self.delayable = Delayable(
|
||||
recordset,
|
||||
priority=priority,
|
||||
eta=eta,
|
||||
max_retries=max_retries,
|
||||
description=description,
|
||||
channel=channel,
|
||||
identity_key=identity_key,
|
||||
)
|
||||
|
||||
@property
|
||||
def recordset(self):
|
||||
return self.delayable.recordset
|
||||
|
||||
def __getattr__(self, name):
|
||||
def _delay_delayable(*args, **kwargs):
|
||||
getattr(self.delayable, name)(*args, **kwargs).delay()
|
||||
return self.delayable._generated_job
|
||||
|
||||
return _delay_delayable
|
||||
|
||||
def __str__(self):
|
||||
return "DelayableRecordset(%s%s)" % (
|
||||
self.delayable.recordset._name,
|
||||
getattr(self.delayable.recordset, "_ids", ""),
|
||||
)
|
||||
|
||||
__repr__ = __str__
|
||||
@@ -1,43 +0,0 @@
|
||||
# Copyright 2012-2016 Camptocamp
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
|
||||
class BaseQueueJobError(Exception):
|
||||
"""Base queue job error"""
|
||||
|
||||
|
||||
class JobError(BaseQueueJobError):
|
||||
"""A job had an error"""
|
||||
|
||||
|
||||
class NoSuchJobError(JobError):
|
||||
"""The job does not exist."""
|
||||
|
||||
|
||||
class FailedJobError(JobError):
|
||||
"""A job had an error having to be resolved."""
|
||||
|
||||
|
||||
class RetryableJobError(JobError):
|
||||
"""A job had an error but can be retried.
|
||||
|
||||
The job will be retried after the given number of seconds. If seconds is
|
||||
empty, it will be retried according to the ``retry_pattern`` of the job or
|
||||
by :const:`odoo.addons.queue_job.job.RETRY_INTERVAL` if nothing is defined.
|
||||
|
||||
If ``ignore_retry`` is True, the retry counter will not be increased.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, seconds=None, ignore_retry=False):
|
||||
super().__init__(msg)
|
||||
self.seconds = seconds
|
||||
self.ignore_retry = ignore_retry
|
||||
|
||||
|
||||
# TODO: remove support of NothingToDo: too dangerous
|
||||
class NothingToDoJob(JobError):
|
||||
"""The Job has nothing to do."""
|
||||
|
||||
|
||||
class ChannelNotFound(BaseQueueJobError):
|
||||
"""A channel could not be found"""
|
||||
@@ -1,123 +0,0 @@
|
||||
# copyright 2016 Camptocamp
|
||||
# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
|
||||
import dateutil
|
||||
import lxml
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo.tools.func import lazy
|
||||
|
||||
|
||||
class JobSerialized(fields.Field):
|
||||
"""Provide the storage for job fields stored as json
|
||||
|
||||
A base_type must be set, it must be dict, list or tuple.
|
||||
When the field is not set, the json will be the corresponding
|
||||
json string ("{}" or "[]").
|
||||
|
||||
Support for some custom types has been added to the json decoder/encoder
|
||||
(see JobEncoder and JobDecoder).
|
||||
"""
|
||||
|
||||
type = "job_serialized"
|
||||
column_type = ("text", "text")
|
||||
|
||||
_base_type = None
|
||||
|
||||
# these are the default values when we convert an empty value
|
||||
_default_json_mapping = {
|
||||
dict: "{}",
|
||||
list: "[]",
|
||||
tuple: "[]",
|
||||
models.BaseModel: lambda env: json.dumps(
|
||||
{"_type": "odoo_recordset", "model": "base", "ids": [], "uid": env.uid}
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, string=fields.Default, base_type=fields.Default, **kwargs):
|
||||
super().__init__(string=string, _base_type=base_type, **kwargs)
|
||||
|
||||
def _setup_attrs(self, model, name): # pylint: disable=missing-return
|
||||
super()._setup_attrs(model, name)
|
||||
if self._base_type not in self._default_json_mapping:
|
||||
raise ValueError("%s is not a supported base type" % (self._base_type))
|
||||
|
||||
def _base_type_default_json(self, env):
|
||||
default_json = self._default_json_mapping.get(self._base_type)
|
||||
if not isinstance(default_json, str):
|
||||
default_json = default_json(env)
|
||||
return default_json
|
||||
|
||||
def convert_to_column(self, value, record, values=None, validate=True):
|
||||
return self.convert_to_cache(value, record, validate=validate)
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
# cache format: json.dumps(value) or None
|
||||
if isinstance(value, self._base_type):
|
||||
return json.dumps(value, cls=JobEncoder)
|
||||
else:
|
||||
return value or None
|
||||
|
||||
def convert_to_record(self, value, record):
|
||||
default = self._base_type_default_json(record.env)
|
||||
return json.loads(value or default, cls=JobDecoder, env=record.env)
|
||||
|
||||
|
||||
class JobEncoder(json.JSONEncoder):
|
||||
"""Encode Odoo recordsets so that we can later recompose them"""
|
||||
|
||||
def _get_record_context(self, obj):
|
||||
return obj._job_prepare_context_before_enqueue()
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, models.BaseModel):
|
||||
return {
|
||||
"_type": "odoo_recordset",
|
||||
"model": obj._name,
|
||||
"ids": obj.ids,
|
||||
"uid": obj.env.uid,
|
||||
"su": obj.env.su,
|
||||
"context": self._get_record_context(obj),
|
||||
}
|
||||
elif isinstance(obj, datetime):
|
||||
return {"_type": "datetime_isoformat", "value": obj.isoformat()}
|
||||
elif isinstance(obj, date):
|
||||
return {"_type": "date_isoformat", "value": obj.isoformat()}
|
||||
elif isinstance(obj, lxml.etree._Element):
|
||||
return {
|
||||
"_type": "etree_element",
|
||||
"value": lxml.etree.tostring(obj, encoding=str),
|
||||
}
|
||||
elif isinstance(obj, lazy):
|
||||
return obj._value
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class JobDecoder(json.JSONDecoder):
|
||||
"""Decode json, recomposing recordsets"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
env = kwargs.pop("env")
|
||||
super().__init__(object_hook=self.object_hook, *args, **kwargs)
|
||||
assert env
|
||||
self.env = env
|
||||
|
||||
def object_hook(self, obj):
|
||||
if "_type" not in obj:
|
||||
return obj
|
||||
type_ = obj["_type"]
|
||||
if type_ == "odoo_recordset":
|
||||
model = self.env(user=obj.get("uid"), su=obj.get("su"))[obj["model"]]
|
||||
if obj.get("context"):
|
||||
model = model.with_context(**obj.get("context"))
|
||||
return model.browse(obj["ids"])
|
||||
elif type_ == "datetime_isoformat":
|
||||
return dateutil.parser.parse(obj["value"])
|
||||
elif type_ == "date_isoformat":
|
||||
return dateutil.parser.parse(obj["value"]).date()
|
||||
elif type_ == "etree_element":
|
||||
return lxml.etree.fromstring(obj["value"])
|
||||
return obj
|
||||
@@ -1,964 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * queue_job
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-07-29 07:25+0000\n"
|
||||
"Last-Translator: Enric Tobella <etobella@creublanca.es>\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ca\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.10.4\n"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid ""
|
||||
"<br/>\n"
|
||||
" <span class=\"oe_grey oe_inline\"> If the max. "
|
||||
"retries is 0, the number of retries is infinite.</span>"
|
||||
msgstr ""
|
||||
"<br/>\n"
|
||||
" <span class=\"oe_grey oe_inline\"> Si el máx. "
|
||||
"reintents es 0, el nombre de reintents es infinit.</span>"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "Access Denied"
|
||||
msgstr "Accés denegat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
|
||||
msgid "Action Needed"
|
||||
msgstr "Acció requerida"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
|
||||
msgid "Activities"
|
||||
msgstr "Activitats"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
|
||||
msgid "Activity Exception Decoration"
|
||||
msgstr "Decoració de l'activitat d'exepció"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
|
||||
msgid "Activity State"
|
||||
msgstr "Estat de l'activitat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
|
||||
msgid "Activity Type Icon"
|
||||
msgstr "Icona del tipus d'activitat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
|
||||
msgid "Args"
|
||||
msgstr "Arguments"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
|
||||
msgid "Attachment Count"
|
||||
msgstr "Nombre d'adjunts"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server
|
||||
#: model:ir.cron,cron_name:queue_job.ir_cron_autovacuum_queue_jobs
|
||||
msgid "AutoVacuum Job Queue"
|
||||
msgstr "Buidat automàtic de la cua de Treballs"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_base
|
||||
msgid "Base"
|
||||
msgstr "Base"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel·lar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
|
||||
msgid "Cancel all selected jobs"
|
||||
msgstr "Cancel·lar tots els treballs seleccionats"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Cancel job"
|
||||
msgstr "Cancel·lar treball"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
msgid "Cancel jobs"
|
||||
msgstr "Cancel·lar treballs"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Cancelled"
|
||||
msgstr "Cancel·lat"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Cancelled by %s"
|
||||
msgstr "Cancel·lat per %s"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Cannot change the root channel"
|
||||
msgstr "No es pot cambiar el canal arrel"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Cannot remove the root channel"
|
||||
msgstr "No es pot eliminar el canal arrel"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Channel"
|
||||
msgstr "Canal"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
|
||||
msgid "Channel complete name must be unique"
|
||||
msgstr "El nom complet del canal ha de ser únic"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
|
||||
msgid "Channels"
|
||||
msgstr "Canals"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
|
||||
msgid "Company"
|
||||
msgstr "Companyia"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
|
||||
msgid "Complete Method Name"
|
||||
msgstr "Nom complet del métode"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
|
||||
msgid "Complete Name"
|
||||
msgstr "Nom complet"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
|
||||
msgid "Created Date"
|
||||
msgstr "Data de creació"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Creat per"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Created date"
|
||||
msgstr "Data de creació"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Creat el"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
|
||||
msgid "Current try"
|
||||
msgstr "Intent actual"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Current try / max. retries"
|
||||
msgstr "Intent actual / reintents màx."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
|
||||
msgid "Date Cancelled"
|
||||
msgstr "Data de cancel·lació"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
|
||||
msgid "Date Done"
|
||||
msgstr "Data de realització"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Dependencies"
|
||||
msgstr "Dependències"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
|
||||
msgid "Dependency Graph"
|
||||
msgstr "Gràfic de dependència"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
|
||||
msgid "Description"
|
||||
msgstr "Descripció"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nom a mostrar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Done"
|
||||
msgstr "Realitzat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
|
||||
msgid "Enqueue Time"
|
||||
msgstr "Encua el treball"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Enqueued"
|
||||
msgstr "Encuat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Exception"
|
||||
msgstr "Exepció"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
|
||||
msgid "Exception Info"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Exception Information"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
|
||||
msgid "Exception Message"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Exception message"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Exception:"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
|
||||
msgid "Execute only after"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
|
||||
msgid "Execution Time (avg)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Failed"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
|
||||
msgid "Field Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_ir_model_fields
|
||||
msgid "Fields"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
|
||||
msgid "Followers"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
|
||||
msgid "Followers (Partners)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
|
||||
msgid "Font awesome icon e.g. fa-tasks"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Graph"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Graph Jobs"
|
||||
msgstr "Gràfic dels treballs"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
|
||||
msgid "Graph Jobs Count"
|
||||
msgstr "Nombre de gràfics de treballs"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
|
||||
msgid "Graph UUID"
|
||||
msgstr "UUID del gràfic"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Group By"
|
||||
msgstr "Agrupar per"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
|
||||
msgid "Has Message"
|
||||
msgstr "Té missatges"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
|
||||
msgid "Icon"
|
||||
msgstr "Icona"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
|
||||
msgid "Icon to indicate an exception activity."
|
||||
msgstr "Icona per indicar una activitat d'excepció."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
|
||||
msgid "Identity Key"
|
||||
msgstr "Clau identificadora"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
|
||||
msgid "If checked, new messages require your attention."
|
||||
msgstr "Si està marcat, nous missatges requereixen la teva atenció."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
|
||||
msgid "If checked, some messages have a delivery error."
|
||||
msgstr "Si està marcat, alguns missatges tenen error d'enviament."
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid "Invalid job function: {}"
|
||||
msgstr "Funció de treball invàlida: {}"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
|
||||
msgid "Is Follower"
|
||||
msgstr "Es seguidor"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job_channel
|
||||
msgid "Job Channels"
|
||||
msgstr "Canals de treball"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Job Function"
|
||||
msgstr "Funció de treball"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job_function
|
||||
#: model:ir.model,name:queue_job.model_queue_job_function
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_function
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
msgid "Job Functions"
|
||||
msgstr "Funcions de treball"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.module.category,name:queue_job.module_category_queue_job
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_root
|
||||
msgid "Job Queue"
|
||||
msgstr "Cua de treballs"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:res.groups,name:queue_job.group_queue_job_manager
|
||||
msgid "Job Queue Manager"
|
||||
msgstr "Gestor de la Cua de treballs"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
|
||||
msgid "Job Serialized"
|
||||
msgstr "Treball seralitzat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:mail.message.subtype,name:queue_job.mt_job_failed
|
||||
msgid "Job failed"
|
||||
msgstr "Treball fallit"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "Job interrupted and set to Done: nothing to do."
|
||||
msgstr "Treball interromput i marcat com a realitzat: res a realitzar."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Jobs"
|
||||
msgstr "Treballs"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Jobs for graph %s"
|
||||
msgstr "Gràfic de treball %s"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
|
||||
msgid "Kwargs"
|
||||
msgstr "Kwargs"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 24 hours"
|
||||
msgstr "Últimes 24 hores"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 30 days"
|
||||
msgstr "Últims 30 dies"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 7 days"
|
||||
msgstr "Últims 7 dies"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Última modificació el"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Última actualització per"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Última actualització el"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_main_attachment_id
|
||||
msgid "Main Attachment"
|
||||
msgstr "Adjunt principal"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Manually set to done by %s"
|
||||
msgstr "Marcat manualment com realitzat per %s"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
|
||||
msgid "Max. retries"
|
||||
msgstr "Màx. reintents"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
|
||||
msgid "Message Delivery error"
|
||||
msgstr "Error d'enviament del missatge"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
|
||||
msgid "Messages"
|
||||
msgstr "Missatges"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
|
||||
msgid "Method"
|
||||
msgstr "Mètode"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
|
||||
msgid "Method Name"
|
||||
msgstr "Nom del mètode"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Model"
|
||||
msgstr "Model"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid "Model {} not found"
|
||||
msgstr "Model {} no trobat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
|
||||
msgid "My Activity Deadline"
|
||||
msgstr "Data límit de la meva activitat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
|
||||
msgid "Next Activity Deadline"
|
||||
msgstr "Data límit de la següent activitat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
|
||||
msgid "Next Activity Summary"
|
||||
msgstr "Resum de la següent activitat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
|
||||
msgid "Next Activity Type"
|
||||
msgstr "Tipus de la següent activitat"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "No action available for this job"
|
||||
msgstr "No hi ha accions disponibles per aquest treball"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Not allowed to change field(s): {}"
|
||||
msgstr "No està permés cambiar els camps: {}"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
|
||||
msgid "Number of Actions"
|
||||
msgstr "Nombre d'accions"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
|
||||
msgid "Number of errors"
|
||||
msgstr "Nombre d'errors"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
|
||||
msgid "Number of messages requiring action"
|
||||
msgstr "Nombre de missatges que requereixen accions"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
|
||||
msgid "Number of messages with delivery error"
|
||||
msgstr "Nombre de missatges amb error d'enviament"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
|
||||
msgid "Parent Channel"
|
||||
msgstr "Canal pare"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Parent channel required."
|
||||
msgstr "Canal pare obligatori."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
|
||||
msgid ""
|
||||
"Pattern expressing from the count of retries on retryable errors, the number "
|
||||
"of of seconds to postpone the next execution. Setting the number of seconds "
|
||||
"to a 2-element tuple or list will randomize the retry interval between the 2 "
|
||||
"values.\n"
|
||||
"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
|
||||
"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
|
||||
"See the module description for details."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Pending"
|
||||
msgstr "Pendent"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Priority"
|
||||
msgstr "Prioritat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue
|
||||
msgid "Queue"
|
||||
msgstr "Cua"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
|
||||
msgid "Queue Job"
|
||||
msgstr "Cua de treballs"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job_lock
|
||||
msgid "Queue Job Lock"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Queue jobs must be created by calling 'with_delay()'."
|
||||
msgstr "Els treballs en cua es creen mitjançant la funció 'with_delay()'."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids
|
||||
msgid "Record"
|
||||
msgstr "Registre"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
|
||||
msgid "Record(s)"
|
||||
msgstr "Registre(s)"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Related"
|
||||
msgstr "Relacionat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
|
||||
msgid "Related Action"
|
||||
msgstr "Acció relacionada"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
|
||||
msgid "Related Action (serialized)"
|
||||
msgstr "Acció relacionada (en serie)"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Related Record"
|
||||
msgstr "Registre relacionat"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Related Records"
|
||||
msgstr "Registres relacionats"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
|
||||
msgid "Remaining days to execute"
|
||||
msgstr "Dies restants per executar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
|
||||
msgid "Removal Interval"
|
||||
msgstr "Interval d'eliminació"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "Requeue"
|
||||
msgstr "Reencua"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Requeue Job"
|
||||
msgstr "Reencua el treball"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_requeue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "Requeue Jobs"
|
||||
msgstr "Reencua el treball"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
|
||||
msgid "Responsible User"
|
||||
msgstr "Usuari responsable"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Result"
|
||||
msgstr "Resultat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Results"
|
||||
msgstr "Resultats"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
|
||||
msgid "Retry Pattern"
|
||||
msgstr "Patró de reintents"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
|
||||
msgid "Retry Pattern (serialized)"
|
||||
msgstr "Patró de reintents (serialitzat)"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_jobs_to_done
|
||||
msgid "Set all selected jobs to done"
|
||||
msgstr "Marcar tots els treballs seleccionats com realitzats"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Set jobs done"
|
||||
msgstr "Marcar treballs com realitzats"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
|
||||
msgid "Set jobs to done"
|
||||
msgstr "Marcar treballs com realitzats"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Set to 'Done'"
|
||||
msgstr "Marcar com 'Realitzat'"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Set to done"
|
||||
msgstr "Marcar com realitzat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
|
||||
msgid "Single shared identifier of a Graph. Empty for a single job."
|
||||
msgstr "Identificador únic del gràfic. Buit per a un únic treball."
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Something bad happened during the execution of job %s. More details in the "
|
||||
"'Exception Information' section."
|
||||
msgstr ""
|
||||
"Ha sorgit un problema durant l'execució del treball %s. Més detalls a la "
|
||||
"secció 'Informació sobre l'exepció'."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
|
||||
msgid "Start Date"
|
||||
msgstr "Data d'inici"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Started"
|
||||
msgstr "Iniciat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "State"
|
||||
msgstr "Estat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
|
||||
msgid ""
|
||||
"Status based on activities\n"
|
||||
"Overdue: Due date is already passed\n"
|
||||
"Today: Activity date is today\n"
|
||||
"Planned: Future activities."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
|
||||
msgid "Task"
|
||||
msgstr "Tasca"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action
|
||||
msgid ""
|
||||
"The action when the button *Related Action* is used on a job. The default "
|
||||
"action is to open the view of the record related to the job. Configured as a "
|
||||
"dictionary with optional keys: enable, func_name, kwargs.\n"
|
||||
"See the module description for details."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
|
||||
msgid ""
|
||||
"The job will fail if the number of tries reach the max. retries.\n"
|
||||
"Retries are infinite when empty."
|
||||
msgstr ""
|
||||
"El treball fallarà si arriba al nombre de reintents màxim.\n"
|
||||
"Els reintents són infinits quan es deixa buit."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
msgid "The selected jobs will be cancelled."
|
||||
msgstr "Els treballs seleccionats seran cancel·lats."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "The selected jobs will be requeued."
|
||||
msgstr "Els treballs seleccionats es tornaran a posar en cua."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "The selected jobs will be set to done."
|
||||
msgstr "Els treballs seleccionats es marcaran com realitzats."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Time (s)"
|
||||
msgstr "Temps (s)"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
|
||||
msgid "Time required to execute this job in seconds. Average when grouped."
|
||||
msgstr ""
|
||||
"Temps necessari per executar el treball en segons. Mitjana quan s'agrupa."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Tried many times"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
|
||||
msgid "Type of the exception activity on record."
|
||||
msgstr "Tipus d'acitivitat de l'exepció en el registre."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
|
||||
msgid "UUID"
|
||||
msgstr "UUID"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unexpected format of Related Action for {}.\n"
|
||||
"Example of valid format:\n"
|
||||
"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
|
||||
"\"kwargs\" {{\"limit\": 10}}}}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unexpected format of Retry Pattern for {}.\n"
|
||||
"Example of valid formats:\n"
|
||||
"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
|
||||
"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
|
||||
msgid "User ID"
|
||||
msgstr "ID de l'usuari"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Wait Dependencies"
|
||||
msgstr "Esperant dependències"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_requeue_job
|
||||
msgid "Wizard to requeue a selection of jobs"
|
||||
msgstr "Assistent per tornar a posar en cua una selecció de treballs"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
|
||||
msgid "Worker Pid"
|
||||
msgstr "PID del treballador"
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,946 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * queue_job
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid ""
|
||||
"<br/>\n"
|
||||
" <span class=\"oe_grey oe_inline\"> If the max. retries is 0, the number of retries is infinite.</span>"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "Access Denied"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
|
||||
msgid "Action Needed"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
|
||||
msgid "Activities"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
|
||||
msgid "Activity Exception Decoration"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
|
||||
msgid "Activity State"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
|
||||
msgid "Activity Type Icon"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
|
||||
msgid "Args"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
|
||||
msgid "Attachment Count"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server
|
||||
#: model:ir.cron,cron_name:queue_job.ir_cron_autovacuum_queue_jobs
|
||||
msgid "AutoVacuum Job Queue"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_base
|
||||
msgid "Base"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
|
||||
msgid "Cancel all selected jobs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Cancel job"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
msgid "Cancel jobs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Cancelled"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Cancelled by %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Cannot change the root channel"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Cannot remove the root channel"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Channel"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
|
||||
msgid "Channel complete name must be unique"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
|
||||
msgid "Channels"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
|
||||
msgid "Company"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
|
||||
msgid "Complete Method Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
|
||||
msgid "Complete Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
|
||||
msgid "Created Date"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Created date"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
|
||||
msgid "Current try"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Current try / max. retries"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
|
||||
msgid "Date Cancelled"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
|
||||
msgid "Date Done"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Dependencies"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
|
||||
msgid "Dependency Graph"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
|
||||
msgid "Enqueue Time"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Enqueued"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Exception"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
|
||||
msgid "Exception Info"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Exception Information"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
|
||||
msgid "Exception Message"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Exception message"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Exception:"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
|
||||
msgid "Execute only after"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
|
||||
msgid "Execution Time (avg)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Failed"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
|
||||
msgid "Field Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_ir_model_fields
|
||||
msgid "Fields"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
|
||||
msgid "Followers"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
|
||||
msgid "Followers (Partners)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
|
||||
msgid "Font awesome icon e.g. fa-tasks"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Graph"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Graph Jobs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
|
||||
msgid "Graph Jobs Count"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
|
||||
msgid "Graph UUID"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Group By"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
|
||||
msgid "Has Message"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
|
||||
msgid "Icon"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
|
||||
msgid "Icon to indicate an exception activity."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
|
||||
msgid "Identity Key"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
|
||||
msgid "If checked, new messages require your attention."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
|
||||
msgid "If checked, some messages have a delivery error."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid "Invalid job function: {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
|
||||
msgid "Is Follower"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job_channel
|
||||
msgid "Job Channels"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Job Function"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job_function
|
||||
#: model:ir.model,name:queue_job.model_queue_job_function
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_function
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
msgid "Job Functions"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.module.category,name:queue_job.module_category_queue_job
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_root
|
||||
msgid "Job Queue"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:res.groups,name:queue_job.group_queue_job_manager
|
||||
msgid "Job Queue Manager"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
|
||||
msgid "Job Serialized"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:mail.message.subtype,name:queue_job.mt_job_failed
|
||||
msgid "Job failed"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "Job interrupted and set to Done: nothing to do."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Jobs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Jobs for graph %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
|
||||
msgid "Kwargs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 24 hours"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 30 days"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 7 days"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_main_attachment_id
|
||||
msgid "Main Attachment"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Manually set to done by %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
|
||||
msgid "Max. retries"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
|
||||
msgid "Message Delivery error"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
|
||||
msgid "Messages"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
|
||||
msgid "Method"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
|
||||
msgid "Method Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid "Model {} not found"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
|
||||
msgid "My Activity Deadline"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
|
||||
msgid "Next Activity Deadline"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
|
||||
msgid "Next Activity Summary"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
|
||||
msgid "Next Activity Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "No action available for this job"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Not allowed to change field(s): {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
|
||||
msgid "Number of Actions"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
|
||||
msgid "Number of errors"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
|
||||
msgid "Number of messages requiring action"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
|
||||
msgid "Number of messages with delivery error"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
|
||||
msgid "Parent Channel"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Parent channel required."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
|
||||
msgid ""
|
||||
"Pattern expressing from the count of retries on retryable errors, the number of of seconds to postpone the next execution. Setting the number of seconds to a 2-element tuple or list will randomize the retry interval between the 2 values.\n"
|
||||
"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
|
||||
"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
|
||||
"See the module description for details."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Priority"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue
|
||||
msgid "Queue"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
|
||||
msgid "Queue Job"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job_lock
|
||||
msgid "Queue Job Lock"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Queue jobs must be created by calling 'with_delay()'."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids
|
||||
msgid "Record"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
|
||||
msgid "Record(s)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Related"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
|
||||
msgid "Related Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
|
||||
msgid "Related Action (serialized)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Related Record"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Related Records"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
|
||||
msgid "Remaining days to execute"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
|
||||
msgid "Removal Interval"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "Requeue"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Requeue Job"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_requeue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "Requeue Jobs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
|
||||
msgid "Responsible User"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Result"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Results"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
|
||||
msgid "Retry Pattern"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
|
||||
msgid "Retry Pattern (serialized)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_jobs_to_done
|
||||
msgid "Set all selected jobs to done"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Set jobs done"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
|
||||
msgid "Set jobs to done"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Set to 'Done'"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Set to done"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
|
||||
msgid "Single shared identifier of a Graph. Empty for a single job."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Something bad happened during the execution of job %s. More details in the "
|
||||
"'Exception Information' section."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
|
||||
msgid "Start Date"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Started"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
|
||||
msgid ""
|
||||
"Status based on activities\n"
|
||||
"Overdue: Due date is already passed\n"
|
||||
"Today: Activity date is today\n"
|
||||
"Planned: Future activities."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
|
||||
msgid "Task"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action
|
||||
msgid ""
|
||||
"The action when the button *Related Action* is used on a job. The default action is to open the view of the record related to the job. Configured as a dictionary with optional keys: enable, func_name, kwargs.\n"
|
||||
"See the module description for details."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
|
||||
msgid ""
|
||||
"The job will fail if the number of tries reach the max. retries.\n"
|
||||
"Retries are infinite when empty."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
msgid "The selected jobs will be cancelled."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "The selected jobs will be requeued."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "The selected jobs will be set to done."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Time (s)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
|
||||
msgid "Time required to execute this job in seconds. Average when grouped."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Tried many times"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
|
||||
msgid "Type of the exception activity on record."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
|
||||
msgid "UUID"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unexpected format of Related Action for {}.\n"
|
||||
"Example of valid format:\n"
|
||||
"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs\" {{\"limit\": 10}}}}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unexpected format of Retry Pattern for {}.\n"
|
||||
"Example of valid formats:\n"
|
||||
"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
|
||||
"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
|
||||
msgid "User ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Wait Dependencies"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_requeue_job
|
||||
msgid "Wizard to requeue a selection of jobs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
|
||||
msgid "Worker Pid"
|
||||
msgstr ""
|
||||
@@ -1,995 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * queue_job
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-06-13 15:27+0000\n"
|
||||
"Last-Translator: Betül Öğmen <betulo@eska.biz>\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: tr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.10.4\n"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid ""
|
||||
"<br/>\n"
|
||||
" <span class=\"oe_grey oe_inline\"> If the max. "
|
||||
"retries is 0, the number of retries is infinite.</span>"
|
||||
msgstr ""
|
||||
"<br/>\n"
|
||||
" <span class=\"oe_grey oe_inline\"> Eğer maks. "
|
||||
"deneme sayısı 0 ise, deneme sayısı sonsuz olur.</span>"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "Access Denied"
|
||||
msgstr "Erişim Engellendi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
|
||||
msgid "Action Needed"
|
||||
msgstr "Eylem Gerekli"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
|
||||
msgid "Activities"
|
||||
msgstr "Aktiviteler"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
|
||||
msgid "Activity Exception Decoration"
|
||||
msgstr "Aktivite İstisna Dekorasyonu"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
|
||||
msgid "Activity State"
|
||||
msgstr "Aktivite Durumu"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
|
||||
msgid "Activity Type Icon"
|
||||
msgstr "Aktivite Türü Simgesi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
|
||||
msgid "Args"
|
||||
msgstr "Argümanlar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
|
||||
msgid "Attachment Count"
|
||||
msgstr "Ek Sayısı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server
|
||||
#: model:ir.cron,cron_name:queue_job.ir_cron_autovacuum_queue_jobs
|
||||
msgid "AutoVacuum Job Queue"
|
||||
msgstr "Otomatik Temizleme İş Kuyruğu"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_base
|
||||
msgid "Base"
|
||||
msgstr "Temel"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Cancel"
|
||||
msgstr "İptal"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
|
||||
msgid "Cancel all selected jobs"
|
||||
msgstr "Tüm seçili işleri iptal et"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Cancel job"
|
||||
msgstr "İşi iptal et"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
msgid "Cancel jobs"
|
||||
msgstr "İşleri iptal et"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Cancelled"
|
||||
msgstr "İptal Edildi"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Cancelled by %s"
|
||||
msgstr "%s tarafından iptal edildi"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Cannot change the root channel"
|
||||
msgstr "Kök kanal değiştirilemez"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Cannot remove the root channel"
|
||||
msgstr "Kök kanal silinemez"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Channel"
|
||||
msgstr "Kanal"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
|
||||
msgid "Channel complete name must be unique"
|
||||
msgstr "Kanal tam adı benzersiz olmalıdır"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
|
||||
msgid "Channels"
|
||||
msgstr "Kanallar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
|
||||
msgid "Company"
|
||||
msgstr "Şirket"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
|
||||
msgid "Complete Method Name"
|
||||
msgstr "Tam Metot Adı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
|
||||
msgid "Complete Name"
|
||||
msgstr "Tam Adı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
|
||||
msgid "Created Date"
|
||||
msgstr "Oluşturulma Tarihi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Oluşturan"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Created date"
|
||||
msgstr "Oluşturulma tarihi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Oluşturuldu"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
|
||||
msgid "Current try"
|
||||
msgstr "Şu anki deneme"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Current try / max. retries"
|
||||
msgstr "Şu anki deneme / maks. deneme"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
|
||||
msgid "Date Cancelled"
|
||||
msgstr "İptal Edilme Tarihi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
|
||||
msgid "Date Done"
|
||||
msgstr "Tamamlanma Tarihi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Dependencies"
|
||||
msgstr "Bağımlılıklar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
|
||||
msgid "Dependency Graph"
|
||||
msgstr "Bağımlılık Grafiği"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
|
||||
msgid "Description"
|
||||
msgstr "Açıklama"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Görünüm Adı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Done"
|
||||
msgstr "Tamamlandı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
|
||||
msgid "Enqueue Time"
|
||||
msgstr "Sıraya Alınma Zamanı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Enqueued"
|
||||
msgstr "Sıraya Alındı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Exception"
|
||||
msgstr "İstisna"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
|
||||
msgid "Exception Info"
|
||||
msgstr "İstisna Bilgisi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Exception Information"
|
||||
msgstr "İstisna Bilgisi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
|
||||
msgid "Exception Message"
|
||||
msgstr "İstisna Mesajı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Exception message"
|
||||
msgstr "İstisna mesajı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Exception:"
|
||||
msgstr "İstisna:"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
|
||||
msgid "Execute only after"
|
||||
msgstr "Bundan sonra çalıştır"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
|
||||
msgid "Execution Time (avg)"
|
||||
msgstr "Çalıştırma Zamanı (ort)"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Failed"
|
||||
msgstr "Başarısız"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
|
||||
msgid "Field Type"
|
||||
msgstr "Alan Tipi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_ir_model_fields
|
||||
msgid "Fields"
|
||||
msgstr "Alanlar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
|
||||
msgid "Followers"
|
||||
msgstr "Takipçiler"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
|
||||
msgid "Followers (Partners)"
|
||||
msgstr "Takipçiler (İş Ortakları)"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
|
||||
msgid "Font awesome icon e.g. fa-tasks"
|
||||
msgstr "Font awesome simgeleri ör. fa-tasks"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Graph"
|
||||
msgstr "Grafik"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Graph Jobs"
|
||||
msgstr "Grafiğin İşleri"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
|
||||
msgid "Graph Jobs Count"
|
||||
msgstr "Grafiğin İş Sayısı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
|
||||
msgid "Graph UUID"
|
||||
msgstr "Grafik UUID"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Group By"
|
||||
msgstr "Gruplandı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
|
||||
msgid "Has Message"
|
||||
msgstr "Mesajı Var"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
|
||||
msgid "Icon"
|
||||
msgstr "Simge"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
|
||||
msgid "Icon to indicate an exception activity."
|
||||
msgstr "İstisna etkinliğini gösteren simge."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
|
||||
msgid "Identity Key"
|
||||
msgstr "Benzersiz Anahtar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
|
||||
msgid "If checked, new messages require your attention."
|
||||
msgstr "İşaretlenirse, sizi bekleyen mesajlar var."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
|
||||
msgid "If checked, some messages have a delivery error."
|
||||
msgstr "İşaretlenirse, bazı mesajlar teslimat hatası içerir."
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid "Invalid job function: {}"
|
||||
msgstr "Geçersiz iş fonksiyonu: {}"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
|
||||
msgid "Is Follower"
|
||||
msgstr "Takipçi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job_channel
|
||||
msgid "Job Channels"
|
||||
msgstr "İş Kanalları"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Job Function"
|
||||
msgstr "İş Fonksiyonu"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job_function
|
||||
#: model:ir.model,name:queue_job.model_queue_job_function
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_function
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
msgid "Job Functions"
|
||||
msgstr "İş Fonksiyonları"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.module.category,name:queue_job.module_category_queue_job
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_root
|
||||
msgid "Job Queue"
|
||||
msgstr "İş Kuyruğu"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:res.groups,name:queue_job.group_queue_job_manager
|
||||
msgid "Job Queue Manager"
|
||||
msgstr "İş Kuyruğu Yöneticisi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
|
||||
msgid "Job Serialized"
|
||||
msgstr "Serileştirilmiş İş"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:mail.message.subtype,name:queue_job.mt_job_failed
|
||||
msgid "Job failed"
|
||||
msgstr "İş başarısız oldu"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "Job interrupted and set to Done: nothing to do."
|
||||
msgstr "İş yarıda kesildi ve bitti olarak ayarlandı: yapılacak bir şey yok."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Jobs"
|
||||
msgstr "İşler"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Jobs for graph %s"
|
||||
msgstr "%s grafiğinin işleri"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
|
||||
msgid "Kwargs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 24 hours"
|
||||
msgstr "Son 24 saat"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 30 days"
|
||||
msgstr "Son 30 gün"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 7 days"
|
||||
msgstr "Son 7 gün"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Son Değiştirme"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Son Güncelleyen"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Son Güncelleme"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_main_attachment_id
|
||||
msgid "Main Attachment"
|
||||
msgstr "Ana Ek"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Manually set to done by %s"
|
||||
msgstr "%s tarafından tamamlandı olarak ayarlandı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
|
||||
msgid "Max. retries"
|
||||
msgstr "Maks. deneme"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
|
||||
msgid "Message Delivery error"
|
||||
msgstr "Mesaj Teslimat hatası"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
|
||||
msgid "Messages"
|
||||
msgstr "Mesajlar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
|
||||
msgid "Method"
|
||||
msgstr "Yöntem"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
|
||||
msgid "Method Name"
|
||||
msgstr "Yöntem Adı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid "Model {} not found"
|
||||
msgstr "Model {} bulunamadı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
|
||||
msgid "My Activity Deadline"
|
||||
msgstr "Aktivite Son Tarihim"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
|
||||
msgid "Name"
|
||||
msgstr "Ad"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
|
||||
msgid "Next Activity Deadline"
|
||||
msgstr "Sonraki Aktivite Son Tarihi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
|
||||
msgid "Next Activity Summary"
|
||||
msgstr "Sonraki Aktivite Özeti"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
|
||||
msgid "Next Activity Type"
|
||||
msgstr "Sonraki Aktivite Türü"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "No action available for this job"
|
||||
msgstr "Bu iş için uygun bir eylem yok"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Not allowed to change field(s): {}"
|
||||
msgstr "Alan(lar)ı değiştirme izni yok: {}"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
|
||||
msgid "Number of Actions"
|
||||
msgstr "Eylem Sayısı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
|
||||
msgid "Number of errors"
|
||||
msgstr "Hata sayısı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
|
||||
msgid "Number of messages requiring action"
|
||||
msgstr "Eylem gerektiren mesaj sayısı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
|
||||
msgid "Number of messages with delivery error"
|
||||
msgstr "Teslimat hatası içeren mesaj sayısı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
|
||||
msgid "Parent Channel"
|
||||
msgstr "Üst Kanal"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Parent channel required."
|
||||
msgstr "Üst kanal gerekli."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
|
||||
msgid ""
|
||||
"Pattern expressing from the count of retries on retryable errors, the number "
|
||||
"of of seconds to postpone the next execution. Setting the number of seconds "
|
||||
"to a 2-element tuple or list will randomize the retry interval between the 2 "
|
||||
"values.\n"
|
||||
"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
|
||||
"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
|
||||
"See the module description for details."
|
||||
msgstr ""
|
||||
"Yeniden denenebilir hatalarda yeniden deneme sayısından, bir sonraki "
|
||||
"çalışmanın ertelenmesi için saniye sayısını ifade eden desen. Saniye "
|
||||
"sayısını 2 elemanlı bir demet veya liste olarak ayarlama, yeniden deneme "
|
||||
"aralığını 2 değer arasında rastgele hale getirir.\n"
|
||||
"Örneğin: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
|
||||
"Örneğin: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
|
||||
"Ayrıntılar için modül açıklamasına bakınız."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Pending"
|
||||
msgstr "Beklemede"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Priority"
|
||||
msgstr "Öncelik"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue
|
||||
msgid "Queue"
|
||||
msgstr "Sıra"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
|
||||
msgid "Queue Job"
|
||||
msgstr "Kuyruk İşi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job_lock
|
||||
msgid "Queue Job Lock"
|
||||
msgstr "Kuyruk İş Kilidi"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Queue jobs must be created by calling 'with_delay()'."
|
||||
msgstr "Kuyruk işleri 'with_delay()' çağrılarak oluşturulmalıdır."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids
|
||||
msgid "Record"
|
||||
msgstr "Kayıt"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
|
||||
msgid "Record(s)"
|
||||
msgstr "Kayıt(lar)"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Related"
|
||||
msgstr "İlgili"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
|
||||
msgid "Related Action"
|
||||
msgstr "İlgili Eylem"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
|
||||
msgid "Related Action (serialized)"
|
||||
msgstr "İlgili Eylem (serileştirilmiş)"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Related Record"
|
||||
msgstr "İlgili Kayıt"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Related Records"
|
||||
msgstr "İlgili Kayıtlar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
|
||||
msgid "Remaining days to execute"
|
||||
msgstr "Çalıştırılacak kalan günler"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
|
||||
msgid "Removal Interval"
|
||||
msgstr "Kaldırma Aralığı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "Requeue"
|
||||
msgstr "Yeniden sıraya al"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Requeue Job"
|
||||
msgstr "İşi yeniden sıraya al"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_requeue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "Requeue Jobs"
|
||||
msgstr "İşleri Tekrar Sıraya Al"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
|
||||
msgid "Responsible User"
|
||||
msgstr "Sorumlu Kullanıcı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Result"
|
||||
msgstr "Sonuç"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Results"
|
||||
msgstr "Sonuçlar"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
|
||||
msgid "Retry Pattern"
|
||||
msgstr "Tekrar Şablonu"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
|
||||
msgid "Retry Pattern (serialized)"
|
||||
msgstr "Tekrar Şablonu (serileştirilmiş)"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_jobs_to_done
|
||||
msgid "Set all selected jobs to done"
|
||||
msgstr "Seçili tüm işleri tamamlandı olarak işaretle"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Set jobs done"
|
||||
msgstr "İşleri tamamlandı olarak işaretle"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
|
||||
msgid "Set jobs to done"
|
||||
msgstr "İşleri tamamlandı olarak işaretle"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Set to 'Done'"
|
||||
msgstr "'Tamamlandı' olarak işaretle"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Set to done"
|
||||
msgstr "Tamamlandı olarak işaretle"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
|
||||
msgid "Single shared identifier of a Graph. Empty for a single job."
|
||||
msgstr "Bir grafiğin paylaşılan tanımlayıcısı. Tek işler için değeri boştur."
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Something bad happened during the execution of job %s. More details in the "
|
||||
"'Exception Information' section."
|
||||
msgstr ""
|
||||
"Bu işi yaparken bir şeyler ters gitti %s. 'İstisna Bilgisi' kısmında daha "
|
||||
"fazla detaya erişebilirsiniz."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
|
||||
msgid "Start Date"
|
||||
msgstr "Başlama Tarihi"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Started"
|
||||
msgstr "Başladı"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "State"
|
||||
msgstr "Durum"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
|
||||
msgid ""
|
||||
"Status based on activities\n"
|
||||
"Overdue: Due date is already passed\n"
|
||||
"Today: Activity date is today\n"
|
||||
"Planned: Future activities."
|
||||
msgstr ""
|
||||
"Aktivitelere dayalı durum\n"
|
||||
"Geçikmiş: Son tarih zaten geçti\n"
|
||||
"Bugün: Aktivite tarihi bugün\n"
|
||||
"Planlandı: Gelecekteki aktiviteler."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
|
||||
msgid "Task"
|
||||
msgstr "Görev"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action
|
||||
msgid ""
|
||||
"The action when the button *Related Action* is used on a job. The default "
|
||||
"action is to open the view of the record related to the job. Configured as a "
|
||||
"dictionary with optional keys: enable, func_name, kwargs.\n"
|
||||
"See the module description for details."
|
||||
msgstr ""
|
||||
"İşteki *İlgili Eylem* düğmesi kullanıldığında gerçekleşen eylem. Varsayılan "
|
||||
"eylem, işle ilişkili kaydın görünümünü açmaktır. İsteğe bağlı anahtarlar ile "
|
||||
"yapılandırılmış bir sözlük: etkinleştir, func_name, kwargs.\n"
|
||||
"Ayrıntılar için modül açıklamasına bakınız."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
|
||||
msgid ""
|
||||
"The job will fail if the number of tries reach the max. retries.\n"
|
||||
"Retries are infinite when empty."
|
||||
msgstr ""
|
||||
"Eğer tekrar sayısı maks. deneme sayısına ulaşırsa iş başarısız olacaktır.\n"
|
||||
"Boş olduğunda sonsuz tekrar yapar."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
msgid "The selected jobs will be cancelled."
|
||||
msgstr "Seçilen işler iptal edilecektir."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "The selected jobs will be requeued."
|
||||
msgstr "Seçilen işler tekrar kuyruğa alınacaktır."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "The selected jobs will be set to done."
|
||||
msgstr "Seçilen işler tamamlandı olarak ayarlanacaktır."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Time (s)"
|
||||
msgstr "Süre (sn)"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
|
||||
msgid "Time required to execute this job in seconds. Average when grouped."
|
||||
msgstr ""
|
||||
"Saniye cinsinden bu işi yapmak için gereken süre. Gruplandığında ortalaması "
|
||||
"alınır."
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Tried many times"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
|
||||
msgid "Type of the exception activity on record."
|
||||
msgstr "Kayıttaki istisna aktivitesinin türü."
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
|
||||
msgid "UUID"
|
||||
msgstr "UUID"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unexpected format of Related Action for {}.\n"
|
||||
"Example of valid format:\n"
|
||||
"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
|
||||
"\"kwargs\" {{\"limit\": 10}}}}"
|
||||
msgstr ""
|
||||
"İlgili eylem için beklenmeyen biçim {}.\n"
|
||||
"Doğru biçim örneği:\n"
|
||||
"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
|
||||
"\"kwargs\" {{\"limit\": 10}}}}"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unexpected format of Retry Pattern for {}.\n"
|
||||
"Example of valid formats:\n"
|
||||
"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
|
||||
"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
|
||||
msgstr ""
|
||||
"{} için beklenmeyen tekrarlama şablonu.\n"
|
||||
"Geçerli kullanım örnekleri:\n"
|
||||
"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
|
||||
"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
|
||||
msgid "User ID"
|
||||
msgstr "Kullanıcı ID"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Wait Dependencies"
|
||||
msgstr "Bağımlılıklar Bekleniyor"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_requeue_job
|
||||
msgid "Wizard to requeue a selection of jobs"
|
||||
msgstr "Seçilen işleri tekrar sıraya alan sihirbaz"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
|
||||
msgid "Worker Pid"
|
||||
msgstr "Çalışan Pid"
|
||||
|
||||
#, python-format
|
||||
#~ msgid "If both parameters are 0, ALL jobs will be requeued!"
|
||||
#~ msgstr "Her iki parametre de 0 ise, TÜM işler tekrar sıraya alınacaktır!"
|
||||
|
||||
#~ msgid "Jobs Garbage Collector"
|
||||
#~ msgstr "İş Çöp Toplayıcısı"
|
||||
@@ -1,996 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * queue_job
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 12.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2022-08-13 08:07+0000\n"
|
||||
"Last-Translator: Dong <dong@freshoo.cn>\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.3.2\n"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid ""
|
||||
"<br/>\n"
|
||||
" <span class=\"oe_grey oe_inline\"> If the max. "
|
||||
"retries is 0, the number of retries is infinite.</span>"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "Access Denied"
|
||||
msgstr "拒绝访问"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
|
||||
msgid "Action Needed"
|
||||
msgstr "前置操作"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
|
||||
msgid "Activities"
|
||||
msgstr "活动"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
|
||||
msgid "Activity Exception Decoration"
|
||||
msgstr "活动异常装饰"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
|
||||
msgid "Activity State"
|
||||
msgstr "活动状态"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
|
||||
msgid "Activity Type Icon"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
|
||||
msgid "Args"
|
||||
msgstr "位置参数"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
|
||||
msgid "Attachment Count"
|
||||
msgstr "附件数量"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server
|
||||
#: model:ir.cron,cron_name:queue_job.ir_cron_autovacuum_queue_jobs
|
||||
msgid "AutoVacuum Job Queue"
|
||||
msgstr "自动清空作业队列"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_base
|
||||
msgid "Base"
|
||||
msgstr "基础"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
|
||||
msgid "Cancel all selected jobs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Cancel job"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
msgid "Cancel jobs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Cancelled"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Cancelled by %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Cannot change the root channel"
|
||||
msgstr "无法更改root频道"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Cannot remove the root channel"
|
||||
msgstr "无法删除root频道"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Channel"
|
||||
msgstr "频道"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
|
||||
msgid "Channel complete name must be unique"
|
||||
msgstr "频道完整名称必须是唯一的"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
|
||||
msgid "Channels"
|
||||
msgstr "频道"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
|
||||
msgid "Company"
|
||||
msgstr "公司"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
|
||||
msgid "Complete Method Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
|
||||
msgid "Complete Name"
|
||||
msgstr "完整名称"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
|
||||
msgid "Created Date"
|
||||
msgstr "创建日期"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "创建者"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Created date"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
|
||||
msgid "Created on"
|
||||
msgstr "创建时间"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
|
||||
msgid "Current try"
|
||||
msgstr "当前尝试"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Current try / max. retries"
|
||||
msgstr "当前尝试/最大重试次数"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
|
||||
msgid "Date Cancelled"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
|
||||
msgid "Date Done"
|
||||
msgstr "完成日期"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Dependencies"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
|
||||
msgid "Dependency Graph"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
|
||||
msgid "Description"
|
||||
msgstr "说明"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "显示名称"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Done"
|
||||
msgstr "完成"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
|
||||
msgid "Enqueue Time"
|
||||
msgstr "排队时间"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Enqueued"
|
||||
msgstr "排队"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Exception"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
|
||||
msgid "Exception Info"
|
||||
msgstr "异常信息"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Exception Information"
|
||||
msgstr "异常信息"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
|
||||
msgid "Exception Message"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Exception message"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Exception:"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
|
||||
msgid "Execute only after"
|
||||
msgstr "仅在此之后执行"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
|
||||
msgid "Execution Time (avg)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Failed"
|
||||
msgstr "失败"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
|
||||
msgid "Field Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_ir_model_fields
|
||||
msgid "Fields"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
|
||||
msgid "Followers"
|
||||
msgstr "关注者"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
|
||||
msgid "Followers (Partners)"
|
||||
msgstr "关注者(业务伙伴)"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
|
||||
msgid "Font awesome icon e.g. fa-tasks"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Graph"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Graph Jobs"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
|
||||
msgid "Graph Jobs Count"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
|
||||
msgid "Graph UUID"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Group By"
|
||||
msgstr "分组"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
|
||||
msgid "Has Message"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
|
||||
msgid "Icon"
|
||||
msgstr "图标"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
|
||||
msgid "Icon to indicate an exception activity."
|
||||
msgstr "指示异常活动的图标。"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
|
||||
msgid "Identity Key"
|
||||
msgstr "身份密钥"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
|
||||
msgid "If checked, new messages require your attention."
|
||||
msgstr "确认后, 出现提示消息。"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
|
||||
msgid "If checked, some messages have a delivery error."
|
||||
msgstr "如果勾选此项, 某些消息将会产生传递错误。"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid "Invalid job function: {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
|
||||
msgid "Is Follower"
|
||||
msgstr "关注者"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job_channel
|
||||
msgid "Job Channels"
|
||||
msgstr "作业频道"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Job Function"
|
||||
msgstr "作业函数"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job_function
|
||||
#: model:ir.model,name:queue_job.model_queue_job_function
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_function
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
|
||||
msgid "Job Functions"
|
||||
msgstr "作业函数"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.module.category,name:queue_job.module_category_queue_job
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job_root
|
||||
msgid "Job Queue"
|
||||
msgstr "作业队列"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:res.groups,name:queue_job.group_queue_job_manager
|
||||
msgid "Job Queue Manager"
|
||||
msgstr "作业队列管理员"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
|
||||
#, fuzzy
|
||||
msgid "Job Serialized"
|
||||
msgstr "作业失败"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:mail.message.subtype,name:queue_job.mt_job_failed
|
||||
msgid "Job failed"
|
||||
msgstr "作业失败"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/controllers/main.py:0
|
||||
#, python-format
|
||||
msgid "Job interrupted and set to Done: nothing to do."
|
||||
msgstr "作业中断并设置为已完成:无需执行任何操作。"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Jobs"
|
||||
msgstr "作业"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Jobs for graph %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
|
||||
msgid "Kwargs"
|
||||
msgstr "关键字参数"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 24 hours"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 30 days"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Last 7 days"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done____last_update
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "最后修改日"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "最后更新者"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "最后更新时间"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_main_attachment_id
|
||||
msgid "Main Attachment"
|
||||
msgstr "附件"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Manually set to done by %s"
|
||||
msgstr "由%s手动设置为完成"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
|
||||
msgid "Max. retries"
|
||||
msgstr "最大重试次数"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
|
||||
msgid "Message Delivery error"
|
||||
msgstr "消息递送错误"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
|
||||
msgid "Messages"
|
||||
msgstr "消息"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
|
||||
#, fuzzy
|
||||
msgid "Method"
|
||||
msgstr "方法名称"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
|
||||
msgid "Method Name"
|
||||
msgstr "方法名称"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Model"
|
||||
msgstr "模型"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid "Model {} not found"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
|
||||
msgid "My Activity Deadline"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
|
||||
msgid "Name"
|
||||
msgstr "名称"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
|
||||
msgid "Next Activity Deadline"
|
||||
msgstr "下一活动截止日期"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
|
||||
msgid "Next Activity Summary"
|
||||
msgstr "下一活动摘要"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
|
||||
msgid "Next Activity Type"
|
||||
msgstr "下一活动类型"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "No action available for this job"
|
||||
msgstr "此作业无法执行任何操作"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Not allowed to change field(s): {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
|
||||
msgid "Number of Actions"
|
||||
msgstr "操作次数"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
|
||||
msgid "Number of errors"
|
||||
msgstr "错误数量"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
|
||||
msgid "Number of messages requiring action"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
|
||||
msgid "Number of messages with delivery error"
|
||||
msgstr "递送错误消息数量"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
|
||||
msgid "Parent Channel"
|
||||
msgstr "父频道"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_channel.py:0
|
||||
#, python-format
|
||||
msgid "Parent channel required."
|
||||
msgstr "父频道必填。"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
|
||||
msgid ""
|
||||
"Pattern expressing from the count of retries on retryable errors, the number "
|
||||
"of of seconds to postpone the next execution. Setting the number of seconds "
|
||||
"to a 2-element tuple or list will randomize the retry interval between the 2 "
|
||||
"values.\n"
|
||||
"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
|
||||
"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
|
||||
"See the module description for details."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Pending"
|
||||
msgstr "等待"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Priority"
|
||||
msgstr "优先级"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.ui.menu,name:queue_job.menu_queue
|
||||
msgid "Queue"
|
||||
msgstr "队列"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
|
||||
msgid "Queue Job"
|
||||
msgstr "队列作业"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_job_lock
|
||||
msgid "Queue Job Lock"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Queue jobs must be created by calling 'with_delay()'."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids
|
||||
msgid "Record"
|
||||
msgstr "记录"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
|
||||
#, fuzzy
|
||||
msgid "Record(s)"
|
||||
msgstr "记录"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Related"
|
||||
msgstr "相关的"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
|
||||
#, fuzzy
|
||||
msgid "Related Action"
|
||||
msgstr "相关记录"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
|
||||
msgid "Related Action (serialized)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Related Record"
|
||||
msgstr "相关记录"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid "Related Records"
|
||||
msgstr "相关记录"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
|
||||
msgid "Remaining days to execute"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
|
||||
msgid "Removal Interval"
|
||||
msgstr "清除间隔"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "Requeue"
|
||||
msgstr "重新排队"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Requeue Job"
|
||||
msgstr "重新排队作业"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_requeue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "Requeue Jobs"
|
||||
msgstr "重新排队作业"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
|
||||
msgid "Responsible User"
|
||||
msgstr "负责的用户"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Result"
|
||||
msgstr "结果"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Results"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
|
||||
msgid "Retry Pattern"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
|
||||
msgid "Retry Pattern (serialized)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_jobs_to_done
|
||||
msgid "Set all selected jobs to done"
|
||||
msgstr "将所有选定的作业设置为完成"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Set jobs done"
|
||||
msgstr "设置作业完成"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
|
||||
msgid "Set jobs to done"
|
||||
msgstr "将作业设置为完成"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Set to 'Done'"
|
||||
msgstr "设置为“完成”"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "Set to done"
|
||||
msgstr "设置为完成"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
|
||||
msgid "Single shared identifier of a Graph. Empty for a single job."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Something bad happened during the execution of job %s. More details in the "
|
||||
"'Exception Information' section."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
|
||||
msgid "Start Date"
|
||||
msgstr "开始日期"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Started"
|
||||
msgstr "开始"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "State"
|
||||
msgstr "状态"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
|
||||
msgid ""
|
||||
"Status based on activities\n"
|
||||
"Overdue: Due date is already passed\n"
|
||||
"Today: Activity date is today\n"
|
||||
"Planned: Future activities."
|
||||
msgstr ""
|
||||
"基于活动的状态\n"
|
||||
"逾期:已经超过截止日期\n"
|
||||
"现今:活动日期是当天\n"
|
||||
"计划:未来的活动。"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
|
||||
msgid "Task"
|
||||
msgstr "任务"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action
|
||||
msgid ""
|
||||
"The action when the button *Related Action* is used on a job. The default "
|
||||
"action is to open the view of the record related to the job. Configured as a "
|
||||
"dictionary with optional keys: enable, func_name, kwargs.\n"
|
||||
"See the module description for details."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
|
||||
msgid ""
|
||||
"The job will fail if the number of tries reach the max. retries.\n"
|
||||
"Retries are infinite when empty."
|
||||
msgstr ""
|
||||
"如果尝试次数达到最大重试次数,作业将失败。\n"
|
||||
"空的时候重试是无限的。"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
|
||||
msgid "The selected jobs will be cancelled."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
|
||||
msgid "The selected jobs will be requeued."
|
||||
msgstr "所选作业将重新排队。"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
|
||||
msgid "The selected jobs will be set to done."
|
||||
msgstr "所选作业将设置为完成。"
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
|
||||
msgid "Time (s)"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
|
||||
msgid "Time required to execute this job in seconds. Average when grouped."
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Tried many times"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
|
||||
msgid "Type of the exception activity on record."
|
||||
msgstr "记录的异常活动的类型。"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
|
||||
msgid "UUID"
|
||||
msgstr "UUID"
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unexpected format of Related Action for {}.\n"
|
||||
"Example of valid format:\n"
|
||||
"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
|
||||
"\"kwargs\" {{\"limit\": 10}}}}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#. odoo-python
|
||||
#: code:addons/queue_job/models/queue_job_function.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Unexpected format of Retry Pattern for {}.\n"
|
||||
"Example of valid formats:\n"
|
||||
"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
|
||||
"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
|
||||
msgid "User ID"
|
||||
msgstr "用户"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
|
||||
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
|
||||
msgid "Wait Dependencies"
|
||||
msgstr ""
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model,name:queue_job.model_queue_requeue_job
|
||||
msgid "Wizard to requeue a selection of jobs"
|
||||
msgstr "重新排队向导所选的作业"
|
||||
|
||||
#. module: queue_job
|
||||
#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
|
||||
msgid "Worker Pid"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy, python-format
|
||||
#~ msgid "If both parameters are 0, ALL jobs will be requeued!"
|
||||
#~ msgstr "所选作业将重新排队。"
|
||||
|
||||
#, python-format
|
||||
#~ msgid ""
|
||||
#~ "Something bad happened during the execution of the job. More details in "
|
||||
#~ "the 'Exception Information' section."
|
||||
#~ msgstr ""
|
||||
#~ "在执行作业期间发生了一些不好的事情。有关详细信息,请参见“异常信息”部分。"
|
||||
|
||||
#~ msgid "SMS Delivery error"
|
||||
#~ msgstr "短信传递错误"
|
||||
|
||||
#~ msgid "Number of messages which requires an action"
|
||||
#~ msgstr "需要操作消息数量"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "<span class=\"oe_grey oe_inline\"> If the max. retries is 0, the number "
|
||||
#~ "of retries is infinite.</span>"
|
||||
#~ msgstr ""
|
||||
#~ "<span class=\"oe_grey oe_inline\">如果最大重试次数是0,则重试次数是无限"
|
||||
#~ "的。</span>"
|
||||
|
||||
#~ msgid "Override Channel"
|
||||
#~ msgstr "覆盖频道"
|
||||
|
||||
#~ msgid "Number of unread messages"
|
||||
#~ msgstr "未读消息数量"
|
||||
@@ -1,891 +0,0 @@
|
||||
# Copyright 2013-2020 Camptocamp
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import hashlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
import weakref
|
||||
from datetime import datetime, timedelta
|
||||
from random import randint
|
||||
|
||||
import odoo
|
||||
|
||||
from .exception import FailedJobError, NoSuchJobError, RetryableJobError
|
||||
|
||||
WAIT_DEPENDENCIES = "wait_dependencies"
|
||||
PENDING = "pending"
|
||||
ENQUEUED = "enqueued"
|
||||
CANCELLED = "cancelled"
|
||||
DONE = "done"
|
||||
STARTED = "started"
|
||||
FAILED = "failed"
|
||||
|
||||
STATES = [
|
||||
(WAIT_DEPENDENCIES, "Wait Dependencies"),
|
||||
(PENDING, "Pending"),
|
||||
(ENQUEUED, "Enqueued"),
|
||||
(STARTED, "Started"),
|
||||
(DONE, "Done"),
|
||||
(CANCELLED, "Cancelled"),
|
||||
(FAILED, "Failed"),
|
||||
]
|
||||
|
||||
DEFAULT_PRIORITY = 10 # used by the PriorityQueue to sort the jobs
|
||||
DEFAULT_MAX_RETRIES = 5
|
||||
RETRY_INTERVAL = 10 * 60 # seconds
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO remove in 15.0 or 16.0, used to keep compatibility as the
|
||||
# class has been moved in 'delay'.
|
||||
def DelayableRecordset(*args, **kwargs):
|
||||
# prevent circular import
|
||||
from .delay import DelayableRecordset as dr
|
||||
|
||||
_logger.warning(
|
||||
"DelayableRecordset moved from the queue_job.job"
|
||||
" to the queue_job.delay python module"
|
||||
)
|
||||
return dr(*args, **kwargs)
|
||||
|
||||
|
||||
def identity_exact(job_):
|
||||
"""Identity function using the model, method and all arguments as key
|
||||
|
||||
When used, this identity key will have the effect that when a job should be
|
||||
created and a pending job with the exact same recordset and arguments, the
|
||||
second will not be created.
|
||||
|
||||
It should be used with the ``identity_key`` argument:
|
||||
|
||||
.. python::
|
||||
|
||||
from odoo.addons.queue_job.job import identity_exact
|
||||
|
||||
# [...]
|
||||
delayable = self.with_delay(identity_key=identity_exact)
|
||||
delayable.export_record(force=True)
|
||||
|
||||
Alternative identity keys can be built using the various fields of the job.
|
||||
For example, you could compute a hash using only some arguments of
|
||||
the job.
|
||||
|
||||
.. python::
|
||||
|
||||
def identity_example(job_):
|
||||
hasher = hashlib.sha1()
|
||||
hasher.update(job_.model_name)
|
||||
hasher.update(job_.method_name)
|
||||
hasher.update(str(sorted(job_.recordset.ids)))
|
||||
hasher.update(str(job_.args[1]))
|
||||
hasher.update(str(job_.kwargs.get('foo', '')))
|
||||
return hasher.hexdigest()
|
||||
|
||||
Usually you will probably always want to include at least the name of the
|
||||
model and method.
|
||||
"""
|
||||
hasher = identity_exact_hasher(job_)
|
||||
return hasher.hexdigest()
|
||||
|
||||
|
||||
def identity_exact_hasher(job_):
|
||||
"""Prepare hasher object for identity_exact."""
|
||||
hasher = hashlib.sha1()
|
||||
hasher.update(job_.model_name.encode("utf-8"))
|
||||
hasher.update(job_.method_name.encode("utf-8"))
|
||||
hasher.update(str(sorted(job_.recordset.ids)).encode("utf-8"))
|
||||
hasher.update(str(job_.args).encode("utf-8"))
|
||||
hasher.update(str(sorted(job_.kwargs.items())).encode("utf-8"))
|
||||
return hasher
|
||||
|
||||
|
||||
class Job:
|
||||
"""A Job is a task to execute. It is the in-memory representation of a job.
|
||||
|
||||
Jobs are stored in the ``queue.job`` Odoo Model, but they are handled
|
||||
through this class.
|
||||
|
||||
.. attribute:: uuid
|
||||
|
||||
Id (UUID) of the job.
|
||||
|
||||
.. attribute:: graph_uuid
|
||||
|
||||
Shared UUID of the job's graph. Empty if the job is a single job.
|
||||
|
||||
.. attribute:: state
|
||||
|
||||
State of the job, can pending, enqueued, started, done or failed.
|
||||
The start state is pending and the final state is done.
|
||||
|
||||
.. attribute:: retry
|
||||
|
||||
The current try, starts at 0 and each time the job is executed,
|
||||
it increases by 1.
|
||||
|
||||
.. attribute:: max_retries
|
||||
|
||||
The maximum number of retries allowed before the job is
|
||||
considered as failed.
|
||||
|
||||
.. attribute:: args
|
||||
|
||||
Arguments passed to the function when executed.
|
||||
|
||||
.. attribute:: kwargs
|
||||
|
||||
Keyword arguments passed to the function when executed.
|
||||
|
||||
.. attribute:: description
|
||||
|
||||
Human description of the job.
|
||||
|
||||
.. attribute:: func
|
||||
|
||||
The python function itself.
|
||||
|
||||
.. attribute:: model_name
|
||||
|
||||
Odoo model on which the job will run.
|
||||
|
||||
.. attribute:: priority
|
||||
|
||||
Priority of the job, 0 being the higher priority.
|
||||
|
||||
.. attribute:: date_created
|
||||
|
||||
Date and time when the job was created.
|
||||
|
||||
.. attribute:: date_enqueued
|
||||
|
||||
Date and time when the job was enqueued.
|
||||
|
||||
.. attribute:: date_started
|
||||
|
||||
Date and time when the job was started.
|
||||
|
||||
.. attribute:: date_done
|
||||
|
||||
Date and time when the job was done.
|
||||
|
||||
.. attribute:: result
|
||||
|
||||
A description of the result (for humans).
|
||||
|
||||
.. attribute:: exc_name
|
||||
|
||||
Exception error name when the job failed.
|
||||
|
||||
.. attribute:: exc_message
|
||||
|
||||
Exception error message when the job failed.
|
||||
|
||||
.. attribute:: exc_info
|
||||
|
||||
Exception information (traceback) when the job failed.
|
||||
|
||||
.. attribute:: user_id
|
||||
|
||||
Odoo user id which created the job
|
||||
|
||||
.. attribute:: eta
|
||||
|
||||
Estimated Time of Arrival of the job. It will not be executed
|
||||
before this date/time.
|
||||
|
||||
.. attribute:: recordset
|
||||
|
||||
Model recordset when we are on a delayed Model method
|
||||
|
||||
.. attribute::channel
|
||||
|
||||
The complete name of the channel to use to process the job. If
|
||||
provided it overrides the one defined on the job's function.
|
||||
|
||||
.. attribute::identity_key
|
||||
|
||||
A key referencing the job, multiple job with the same key will not
|
||||
be added to a channel if the existing job with the same key is not yet
|
||||
started or executed.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def load(cls, env, job_uuid):
|
||||
"""Read a single job from the Database
|
||||
|
||||
Raise an error if the job is not found.
|
||||
"""
|
||||
stored = cls.db_records_from_uuids(env, [job_uuid])
|
||||
if not stored:
|
||||
raise NoSuchJobError(
|
||||
"Job %s does no longer exist in the storage." % job_uuid
|
||||
)
|
||||
return cls._load_from_db_record(stored)
|
||||
|
||||
@classmethod
|
||||
def load_many(cls, env, job_uuids):
|
||||
"""Read jobs in batch from the Database
|
||||
|
||||
Jobs not found are ignored.
|
||||
"""
|
||||
recordset = cls.db_records_from_uuids(env, job_uuids)
|
||||
return {cls._load_from_db_record(record) for record in recordset}
|
||||
|
||||
def add_lock_record(self):
|
||||
"""
|
||||
Create row in db to be locked while the job is being performed.
|
||||
"""
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
INSERT INTO
|
||||
queue_job_lock (id, queue_job_id)
|
||||
SELECT
|
||||
id, id
|
||||
FROM
|
||||
queue_job
|
||||
WHERE
|
||||
uuid = %s
|
||||
ON CONFLICT(id)
|
||||
DO NOTHING;
|
||||
""",
|
||||
[self.uuid],
|
||||
)
|
||||
|
||||
def lock(self):
|
||||
"""
|
||||
Lock row of job that is being performed
|
||||
|
||||
If a job cannot be locked,
|
||||
it means that the job wasn't started,
|
||||
a RetryableJobError is thrown.
|
||||
"""
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
queue_job_lock
|
||||
WHERE
|
||||
queue_job_id in (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
queue_job
|
||||
WHERE
|
||||
uuid = %s
|
||||
AND state='started'
|
||||
)
|
||||
FOR UPDATE;
|
||||
""",
|
||||
[self.uuid],
|
||||
)
|
||||
|
||||
# 1 job should be locked
|
||||
if 1 != len(self.env.cr.fetchall()):
|
||||
raise RetryableJobError(
|
||||
f"Trying to lock job that wasn't started, uuid: {self.uuid}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _load_from_db_record(cls, job_db_record):
|
||||
stored = job_db_record
|
||||
|
||||
args = stored.args
|
||||
kwargs = stored.kwargs
|
||||
method_name = stored.method_name
|
||||
|
||||
recordset = stored.records
|
||||
method = getattr(recordset, method_name)
|
||||
|
||||
eta = None
|
||||
if stored.eta:
|
||||
eta = stored.eta
|
||||
|
||||
job_ = cls(
|
||||
method,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
priority=stored.priority,
|
||||
eta=eta,
|
||||
job_uuid=stored.uuid,
|
||||
description=stored.name,
|
||||
channel=stored.channel,
|
||||
identity_key=stored.identity_key,
|
||||
)
|
||||
|
||||
if stored.date_created:
|
||||
job_.date_created = stored.date_created
|
||||
|
||||
if stored.date_enqueued:
|
||||
job_.date_enqueued = stored.date_enqueued
|
||||
|
||||
if stored.date_started:
|
||||
job_.date_started = stored.date_started
|
||||
|
||||
if stored.date_done:
|
||||
job_.date_done = stored.date_done
|
||||
|
||||
if stored.date_cancelled:
|
||||
job_.date_cancelled = stored.date_cancelled
|
||||
|
||||
job_.state = stored.state
|
||||
job_.graph_uuid = stored.graph_uuid if stored.graph_uuid else None
|
||||
job_.result = stored.result if stored.result else None
|
||||
job_.exc_info = stored.exc_info if stored.exc_info else None
|
||||
job_.retry = stored.retry
|
||||
job_.max_retries = stored.max_retries
|
||||
if stored.company_id:
|
||||
job_.company_id = stored.company_id.id
|
||||
job_.identity_key = stored.identity_key
|
||||
job_.worker_pid = stored.worker_pid
|
||||
|
||||
job_.__depends_on_uuids.update(stored.dependencies.get("depends_on", []))
|
||||
job_.__reverse_depends_on_uuids.update(
|
||||
stored.dependencies.get("reverse_depends_on", [])
|
||||
)
|
||||
return job_
|
||||
|
||||
def job_record_with_same_identity_key(self):
|
||||
"""Check if a job to be executed with the same key exists."""
|
||||
existing = (
|
||||
self.env["queue.job"]
|
||||
.sudo()
|
||||
.search(
|
||||
[
|
||||
("identity_key", "=", self.identity_key),
|
||||
("state", "in", [WAIT_DEPENDENCIES, PENDING, ENQUEUED]),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
)
|
||||
return existing
|
||||
|
||||
@staticmethod
|
||||
def db_record_from_uuid(env, job_uuid):
|
||||
# TODO remove in 15.0 or 16.0
|
||||
_logger.debug("deprecated, use 'db_records_from_uuids")
|
||||
return Job.db_records_from_uuids(env, [job_uuid])
|
||||
|
||||
@staticmethod
|
||||
def db_records_from_uuids(env, job_uuids):
|
||||
model = env["queue.job"].sudo()
|
||||
record = model.search([("uuid", "in", tuple(job_uuids))])
|
||||
return record.with_env(env).sudo()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func,
|
||||
args=None,
|
||||
kwargs=None,
|
||||
priority=None,
|
||||
eta=None,
|
||||
job_uuid=None,
|
||||
max_retries=None,
|
||||
description=None,
|
||||
channel=None,
|
||||
identity_key=None,
|
||||
):
|
||||
"""Create a Job
|
||||
|
||||
:param func: function to execute
|
||||
:type func: function
|
||||
:param args: arguments for func
|
||||
:type args: tuple
|
||||
:param kwargs: keyworkd arguments for func
|
||||
:type kwargs: dict
|
||||
:param priority: priority of the job,
|
||||
the smaller is the higher priority
|
||||
:type priority: int
|
||||
:param eta: the job can be executed only after this datetime
|
||||
(or now + timedelta)
|
||||
:type eta: datetime or timedelta
|
||||
:param job_uuid: UUID of the job
|
||||
:param max_retries: maximum number of retries before giving up and set
|
||||
the job state to 'failed'. A value of 0 means infinite retries.
|
||||
:param description: human description of the job. If None, description
|
||||
is computed from the function doc or name
|
||||
:param channel: The complete channel name to use to process the job.
|
||||
:param identity_key: A hash to uniquely identify a job, or a function
|
||||
that returns this hash (the function takes the job
|
||||
as argument)
|
||||
"""
|
||||
if args is None:
|
||||
args = ()
|
||||
if isinstance(args, list):
|
||||
args = tuple(args)
|
||||
assert isinstance(args, tuple), "%s: args are not a tuple" % args
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
assert isinstance(kwargs, dict), "%s: kwargs are not a dict" % kwargs
|
||||
|
||||
if not _is_model_method(func):
|
||||
raise TypeError("Job accepts only methods of Models")
|
||||
|
||||
recordset = func.__self__
|
||||
env = recordset.env
|
||||
self.method_name = func.__name__
|
||||
self.recordset = recordset
|
||||
|
||||
self.env = env
|
||||
self.job_model = self.env["queue.job"]
|
||||
self.job_model_name = "queue.job"
|
||||
|
||||
self.job_config = (
|
||||
self.env["queue.job.function"].sudo().job_config(self.job_function_name)
|
||||
)
|
||||
|
||||
self.state = PENDING
|
||||
|
||||
self.retry = 0
|
||||
if max_retries is None:
|
||||
self.max_retries = DEFAULT_MAX_RETRIES
|
||||
else:
|
||||
self.max_retries = max_retries
|
||||
|
||||
self._uuid = job_uuid
|
||||
self.graph_uuid = None
|
||||
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
self.__depends_on_uuids = set()
|
||||
self.__reverse_depends_on_uuids = set()
|
||||
self._depends_on = set()
|
||||
self._reverse_depends_on = weakref.WeakSet()
|
||||
|
||||
self.priority = priority
|
||||
if self.priority is None:
|
||||
self.priority = DEFAULT_PRIORITY
|
||||
|
||||
self.date_created = datetime.now()
|
||||
self._description = description
|
||||
|
||||
if isinstance(identity_key, str):
|
||||
self._identity_key = identity_key
|
||||
self._identity_key_func = None
|
||||
else:
|
||||
# we'll compute the key on the fly when called
|
||||
# from the function
|
||||
self._identity_key = None
|
||||
self._identity_key_func = identity_key
|
||||
|
||||
self.date_enqueued = None
|
||||
self.date_started = None
|
||||
self.date_done = None
|
||||
self.date_cancelled = None
|
||||
|
||||
self.result = None
|
||||
self.exc_name = None
|
||||
self.exc_message = None
|
||||
self.exc_info = None
|
||||
|
||||
if "company_id" in env.context:
|
||||
company_id = env.context["company_id"]
|
||||
else:
|
||||
company_id = env.company.id
|
||||
self.company_id = company_id
|
||||
self._eta = None
|
||||
self.eta = eta
|
||||
self.channel = channel
|
||||
self.worker_pid = None
|
||||
|
||||
def add_depends(self, jobs):
|
||||
if self in jobs:
|
||||
raise ValueError("job cannot depend on itself")
|
||||
self.__depends_on_uuids |= {j.uuid for j in jobs}
|
||||
self._depends_on.update(jobs)
|
||||
for parent in jobs:
|
||||
parent.__reverse_depends_on_uuids.add(self.uuid)
|
||||
parent._reverse_depends_on.add(self)
|
||||
if any(j.state != DONE for j in jobs):
|
||||
self.state = WAIT_DEPENDENCIES
|
||||
|
||||
def perform(self):
|
||||
"""Execute the job.
|
||||
|
||||
The job is executed with the user which has initiated it.
|
||||
"""
|
||||
self.retry += 1
|
||||
try:
|
||||
self.result = self.func(*tuple(self.args), **self.kwargs)
|
||||
except RetryableJobError as err:
|
||||
if err.ignore_retry:
|
||||
self.retry -= 1
|
||||
raise
|
||||
elif not self.max_retries: # infinite retries
|
||||
raise
|
||||
elif self.retry >= self.max_retries:
|
||||
type_, value, traceback = sys.exc_info()
|
||||
# change the exception type but keep the original
|
||||
# traceback and message:
|
||||
# http://blog.ianbicking.org/2007/09/12/re-raising-exceptions/
|
||||
new_exc = FailedJobError(
|
||||
"Max. retries (%d) reached: %s" % (self.max_retries, value or type_)
|
||||
)
|
||||
raise new_exc from err
|
||||
raise
|
||||
|
||||
return self.result
|
||||
|
||||
def _get_common_dependent_jobs_query(self):
|
||||
return """
|
||||
UPDATE queue_job
|
||||
SET state = %s
|
||||
FROM (
|
||||
SELECT child.id, array_agg(parent.state) as parent_states
|
||||
FROM queue_job job
|
||||
JOIN LATERAL
|
||||
json_array_elements_text(
|
||||
job.dependencies::json->'reverse_depends_on'
|
||||
) child_deps ON true
|
||||
JOIN queue_job child
|
||||
ON child.graph_uuid = job.graph_uuid
|
||||
AND child.uuid = child_deps
|
||||
JOIN LATERAL
|
||||
json_array_elements_text(
|
||||
child.dependencies::json->'depends_on'
|
||||
) parent_deps ON true
|
||||
JOIN queue_job parent
|
||||
ON parent.graph_uuid = job.graph_uuid
|
||||
AND parent.uuid = parent_deps
|
||||
WHERE job.uuid = %s
|
||||
GROUP BY child.id
|
||||
) jobs
|
||||
WHERE
|
||||
queue_job.id = jobs.id
|
||||
AND %s = ALL(jobs.parent_states)
|
||||
AND state = %s;
|
||||
"""
|
||||
|
||||
def enqueue_waiting(self):
|
||||
sql = self._get_common_dependent_jobs_query()
|
||||
self.env.cr.execute(sql, (PENDING, self.uuid, DONE, WAIT_DEPENDENCIES))
|
||||
self.env["queue.job"].invalidate_model(["state"])
|
||||
|
||||
def cancel_dependent_jobs(self):
|
||||
sql = self._get_common_dependent_jobs_query()
|
||||
self.env.cr.execute(sql, (CANCELLED, self.uuid, CANCELLED, WAIT_DEPENDENCIES))
|
||||
self.env["queue.job"].invalidate_model(["state"])
|
||||
|
||||
def store(self):
|
||||
"""Store the Job"""
|
||||
job_model = self.env["queue.job"]
|
||||
# The sentinel is used to prevent edition sensitive fields (such as
|
||||
# method_name) from RPC methods.
|
||||
edit_sentinel = job_model.EDIT_SENTINEL
|
||||
|
||||
db_record = self.db_record()
|
||||
if db_record:
|
||||
db_record.with_context(_job_edit_sentinel=edit_sentinel).write(
|
||||
self._store_values()
|
||||
)
|
||||
else:
|
||||
job_model.with_context(_job_edit_sentinel=edit_sentinel).sudo().create(
|
||||
self._store_values(create=True)
|
||||
)
|
||||
|
||||
def _store_values(self, create=False):
|
||||
vals = {
|
||||
"state": self.state,
|
||||
"priority": self.priority,
|
||||
"retry": self.retry,
|
||||
"max_retries": self.max_retries,
|
||||
"exc_name": self.exc_name,
|
||||
"exc_message": self.exc_message,
|
||||
"exc_info": self.exc_info,
|
||||
"company_id": self.company_id,
|
||||
"result": str(self.result) if self.result else False,
|
||||
"date_enqueued": False,
|
||||
"date_started": False,
|
||||
"date_done": False,
|
||||
"exec_time": False,
|
||||
"date_cancelled": False,
|
||||
"eta": False,
|
||||
"identity_key": False,
|
||||
"worker_pid": self.worker_pid,
|
||||
"graph_uuid": self.graph_uuid,
|
||||
}
|
||||
|
||||
if self.date_enqueued:
|
||||
vals["date_enqueued"] = self.date_enqueued
|
||||
if self.date_started:
|
||||
vals["date_started"] = self.date_started
|
||||
if self.date_done:
|
||||
vals["date_done"] = self.date_done
|
||||
if self.exec_time:
|
||||
vals["exec_time"] = self.exec_time
|
||||
if self.date_cancelled:
|
||||
vals["date_cancelled"] = self.date_cancelled
|
||||
if self.eta:
|
||||
vals["eta"] = self.eta
|
||||
if self.identity_key:
|
||||
vals["identity_key"] = self.identity_key
|
||||
|
||||
dependencies = {
|
||||
"depends_on": [parent.uuid for parent in self.depends_on],
|
||||
"reverse_depends_on": [
|
||||
children.uuid for children in self.reverse_depends_on
|
||||
],
|
||||
}
|
||||
vals["dependencies"] = dependencies
|
||||
|
||||
if create:
|
||||
vals.update(
|
||||
{
|
||||
"user_id": self.env.uid,
|
||||
"channel": self.channel,
|
||||
# The following values must never be modified after the
|
||||
# creation of the job
|
||||
"uuid": self.uuid,
|
||||
"name": self.description,
|
||||
"func_string": self.func_string,
|
||||
"date_created": self.date_created,
|
||||
"model_name": self.recordset._name,
|
||||
"method_name": self.method_name,
|
||||
"job_function_id": self.job_config.job_function_id,
|
||||
"channel_method_name": self.job_function_name,
|
||||
"records": self.recordset,
|
||||
"args": self.args,
|
||||
"kwargs": self.kwargs,
|
||||
}
|
||||
)
|
||||
|
||||
vals_from_model = self._store_values_from_model()
|
||||
# Sanitize values: make sure you cannot screw core values
|
||||
vals_from_model = {k: v for k, v in vals_from_model.items() if k not in vals}
|
||||
vals.update(vals_from_model)
|
||||
return vals
|
||||
|
||||
def _store_values_from_model(self):
|
||||
vals = {}
|
||||
value_handlers_candidates = (
|
||||
"_job_store_values_for_" + self.method_name,
|
||||
"_job_store_values",
|
||||
)
|
||||
for candidate in value_handlers_candidates:
|
||||
handler = getattr(self.recordset, candidate, None)
|
||||
if handler is not None:
|
||||
vals = handler(self)
|
||||
return vals
|
||||
|
||||
@property
|
||||
def func_string(self):
|
||||
model = repr(self.recordset)
|
||||
args = [repr(arg) for arg in self.args]
|
||||
kwargs = ["{}={!r}".format(key, val) for key, val in self.kwargs.items()]
|
||||
all_args = ", ".join(args + kwargs)
|
||||
return "{}.{}({})".format(model, self.method_name, all_args)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.uuid == other.uuid
|
||||
|
||||
def __hash__(self):
|
||||
return self.uuid.__hash__()
|
||||
|
||||
def db_record(self):
|
||||
return self.db_records_from_uuids(self.env, [self.uuid])
|
||||
|
||||
@property
|
||||
def func(self):
|
||||
recordset = self.recordset.with_context(job_uuid=self.uuid)
|
||||
return getattr(recordset, self.method_name)
|
||||
|
||||
@property
|
||||
def job_function_name(self):
|
||||
func_model = self.env["queue.job.function"].sudo()
|
||||
return func_model.job_function_name(self.recordset._name, self.method_name)
|
||||
|
||||
@property
|
||||
def identity_key(self):
|
||||
if self._identity_key is None:
|
||||
if self._identity_key_func:
|
||||
self._identity_key = self._identity_key_func(self)
|
||||
return self._identity_key
|
||||
|
||||
@identity_key.setter
|
||||
def identity_key(self, value):
|
||||
if isinstance(value, str):
|
||||
self._identity_key = value
|
||||
self._identity_key_func = None
|
||||
else:
|
||||
# we'll compute the key on the fly when called
|
||||
# from the function
|
||||
self._identity_key = None
|
||||
self._identity_key_func = value
|
||||
|
||||
@property
|
||||
def depends_on(self):
|
||||
if not self._depends_on:
|
||||
self._depends_on = Job.load_many(self.env, self.__depends_on_uuids)
|
||||
return self._depends_on
|
||||
|
||||
@property
|
||||
def reverse_depends_on(self):
|
||||
if not self._reverse_depends_on:
|
||||
self._reverse_depends_on = Job.load_many(
|
||||
self.env, self.__reverse_depends_on_uuids
|
||||
)
|
||||
return set(self._reverse_depends_on)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
if self._description:
|
||||
return self._description
|
||||
elif self.func.__doc__:
|
||||
return self.func.__doc__.splitlines()[0].strip()
|
||||
else:
|
||||
return "{}.{}".format(self.model_name, self.func.__name__)
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
"""Job ID, this is an UUID"""
|
||||
if self._uuid is None:
|
||||
self._uuid = str(uuid.uuid4())
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def model_name(self):
|
||||
return self.recordset._name
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
return self.recordset.env.uid
|
||||
|
||||
@property
|
||||
def eta(self):
|
||||
return self._eta
|
||||
|
||||
@eta.setter
|
||||
def eta(self, value):
|
||||
if not value:
|
||||
self._eta = None
|
||||
elif isinstance(value, timedelta):
|
||||
self._eta = datetime.now() + value
|
||||
elif isinstance(value, int):
|
||||
self._eta = datetime.now() + timedelta(seconds=value)
|
||||
else:
|
||||
self._eta = value
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
return self._channel or self.job_config.channel
|
||||
|
||||
@channel.setter
|
||||
def channel(self, value):
|
||||
self._channel = value
|
||||
|
||||
@property
|
||||
def exec_time(self):
|
||||
if self.date_done and self.date_started:
|
||||
return (self.date_done - self.date_started).total_seconds()
|
||||
return None
|
||||
|
||||
def set_pending(self, result=None, reset_retry=True):
|
||||
if any(j.state != DONE for j in self.depends_on):
|
||||
self.state = WAIT_DEPENDENCIES
|
||||
else:
|
||||
self.state = PENDING
|
||||
self.date_enqueued = None
|
||||
self.date_started = None
|
||||
self.date_done = None
|
||||
self.worker_pid = None
|
||||
self.date_cancelled = None
|
||||
if reset_retry:
|
||||
self.retry = 0
|
||||
if result is not None:
|
||||
self.result = result
|
||||
|
||||
def set_enqueued(self):
|
||||
self.state = ENQUEUED
|
||||
self.date_enqueued = datetime.now()
|
||||
self.date_started = None
|
||||
self.worker_pid = None
|
||||
|
||||
def set_started(self):
|
||||
self.state = STARTED
|
||||
self.date_started = datetime.now()
|
||||
self.worker_pid = os.getpid()
|
||||
self.add_lock_record()
|
||||
|
||||
def set_done(self, result=None):
|
||||
self.state = DONE
|
||||
self.exc_name = None
|
||||
self.exc_info = None
|
||||
self.date_done = datetime.now()
|
||||
if result is not None:
|
||||
self.result = result
|
||||
|
||||
def set_cancelled(self, result=None):
|
||||
self.state = CANCELLED
|
||||
self.date_cancelled = datetime.now()
|
||||
if result is not None:
|
||||
self.result = result
|
||||
|
||||
def set_failed(self, **kw):
|
||||
self.state = FAILED
|
||||
for k, v in kw.items():
|
||||
if v is not None:
|
||||
setattr(self, k, v)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Job %s, priority:%d>" % (self.uuid, self.priority)
|
||||
|
||||
def _get_retry_seconds(self, seconds=None):
|
||||
retry_pattern = self.job_config.retry_pattern
|
||||
if not seconds and retry_pattern:
|
||||
# ordered from higher to lower count of retries
|
||||
patt = sorted(retry_pattern.items(), key=lambda t: t[0])
|
||||
seconds = RETRY_INTERVAL
|
||||
for retry_count, postpone_seconds in patt:
|
||||
if self.retry >= retry_count:
|
||||
seconds = postpone_seconds
|
||||
else:
|
||||
break
|
||||
elif not seconds:
|
||||
seconds = RETRY_INTERVAL
|
||||
if isinstance(seconds, (list, tuple)):
|
||||
seconds = randint(seconds[0], seconds[1])
|
||||
return seconds
|
||||
|
||||
def postpone(self, result=None, seconds=None):
|
||||
"""Postpone the job
|
||||
|
||||
Write an estimated time arrival to n seconds
|
||||
later than now. Used when an retryable exception
|
||||
want to retry a job later.
|
||||
"""
|
||||
eta_seconds = self._get_retry_seconds(seconds)
|
||||
self.eta = timedelta(seconds=eta_seconds)
|
||||
self.exc_name = None
|
||||
self.exc_info = None
|
||||
if result is not None:
|
||||
self.result = result
|
||||
|
||||
def related_action(self):
|
||||
record = self.db_record()
|
||||
if not self.job_config.related_action_enable:
|
||||
return None
|
||||
|
||||
funcname = self.job_config.related_action_func_name
|
||||
if not funcname:
|
||||
funcname = record._default_related_action
|
||||
if not isinstance(funcname, str):
|
||||
raise ValueError(
|
||||
"related_action must be the name of the "
|
||||
"method on queue.job as string"
|
||||
)
|
||||
action = getattr(record, funcname)
|
||||
action_kwargs = self.job_config.related_action_kwargs
|
||||
return action(**action_kwargs)
|
||||
|
||||
|
||||
def _is_model_method(func):
|
||||
return inspect.ismethod(func) and isinstance(
|
||||
func.__self__.__class__, odoo.models.MetaModel
|
||||
)
|
||||
@@ -1,163 +0,0 @@
|
||||
# Copyright (c) 2015-2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# Copyright 2016 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import logging
|
||||
from threading import Thread
|
||||
import time
|
||||
|
||||
from odoo.service import server
|
||||
from odoo.tools import config
|
||||
|
||||
try:
|
||||
from odoo.addons.server_environment import serv_config
|
||||
|
||||
if serv_config.has_section("queue_job"):
|
||||
queue_job_config = serv_config["queue_job"]
|
||||
else:
|
||||
queue_job_config = {}
|
||||
except ImportError:
|
||||
queue_job_config = config.misc.get("queue_job", {})
|
||||
|
||||
|
||||
from .runner import QueueJobRunner, _channels
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
START_DELAY = 5
|
||||
|
||||
|
||||
# Here we monkey patch the Odoo server to start the job runner thread
|
||||
# in the main server process (and not in forked workers). This is
|
||||
# very easy to deploy as we don't need another startup script.
|
||||
|
||||
|
||||
class QueueJobRunnerThread(Thread):
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.runner = QueueJobRunner.from_environ_or_config()
|
||||
|
||||
def run(self):
|
||||
# sleep a bit to let the workers start at ease
|
||||
time.sleep(START_DELAY)
|
||||
self.runner.run()
|
||||
|
||||
def stop(self):
|
||||
self.runner.stop()
|
||||
|
||||
|
||||
class WorkerJobRunner(server.Worker):
|
||||
"""Jobrunner workers"""
|
||||
|
||||
def __init__(self, multi):
|
||||
super().__init__(multi)
|
||||
self.watchdog_timeout = None
|
||||
self.runner = QueueJobRunner.from_environ_or_config()
|
||||
self._recover = False
|
||||
|
||||
def sleep(self):
|
||||
pass
|
||||
|
||||
def signal_handler(self, sig, frame): # pylint: disable=missing-return
|
||||
_logger.debug("WorkerJobRunner (%s) received signal %s", self.pid, sig)
|
||||
super().signal_handler(sig, frame)
|
||||
self.runner.stop()
|
||||
|
||||
def process_work(self):
|
||||
if self._recover:
|
||||
_logger.info("WorkerJobRunner (%s) runner is reinitialized", self.pid)
|
||||
self.runner = QueueJobRunner.from_environ_or_config()
|
||||
self._recover = False
|
||||
_logger.debug("WorkerJobRunner (%s) starting up", self.pid)
|
||||
time.sleep(START_DELAY)
|
||||
self.runner.run()
|
||||
|
||||
def signal_time_expired_handler(self, n, stack):
|
||||
_logger.info(
|
||||
"Worker (%d) CPU time limit (%s) reached.Stop gracefully and recover",
|
||||
self.pid,
|
||||
config["limit_time_cpu"],
|
||||
)
|
||||
self._recover = True
|
||||
self.runner.stop()
|
||||
|
||||
|
||||
runner_thread = None
|
||||
|
||||
|
||||
def _is_runner_enabled():
|
||||
return not _channels().strip().startswith("root:0")
|
||||
|
||||
|
||||
def _start_runner_thread(server_type):
|
||||
global runner_thread
|
||||
if not config["stop_after_init"]:
|
||||
if _is_runner_enabled():
|
||||
_logger.info("starting jobrunner thread (in %s)", server_type)
|
||||
runner_thread = QueueJobRunnerThread()
|
||||
runner_thread.start()
|
||||
else:
|
||||
_logger.info(
|
||||
"jobrunner thread (in %s) NOT started, "
|
||||
"because the root channel's capacity is set to 0",
|
||||
server_type,
|
||||
)
|
||||
|
||||
|
||||
orig_prefork__init__ = server.PreforkServer.__init__
|
||||
orig_prefork_process_spawn = server.PreforkServer.process_spawn
|
||||
orig_prefork_worker_pop = server.PreforkServer.worker_pop
|
||||
orig_threaded_start = server.ThreadedServer.start
|
||||
orig_threaded_stop = server.ThreadedServer.stop
|
||||
|
||||
|
||||
def prefork__init__(server, app):
|
||||
res = orig_prefork__init__(server, app)
|
||||
server.jobrunner = {}
|
||||
return res
|
||||
|
||||
|
||||
def prefork_process_spawn(server):
|
||||
orig_prefork_process_spawn(server)
|
||||
if not hasattr(server, "jobrunner"):
|
||||
# if 'queue_job' is not in server wide modules, PreforkServer is
|
||||
# not initialized with a 'jobrunner' attribute, skip this
|
||||
return
|
||||
if not server.jobrunner and _is_runner_enabled():
|
||||
server.worker_spawn(WorkerJobRunner, server.jobrunner)
|
||||
|
||||
|
||||
def prefork_worker_pop(server, pid):
|
||||
res = orig_prefork_worker_pop(server, pid)
|
||||
if not hasattr(server, "jobrunner"):
|
||||
# if 'queue_job' is not in server wide modules, PreforkServer is
|
||||
# not initialized with a 'jobrunner' attribute, skip this
|
||||
return res
|
||||
if pid in server.jobrunner:
|
||||
server.jobrunner.pop(pid)
|
||||
return res
|
||||
|
||||
|
||||
def threaded_start(server, *args, **kwargs):
|
||||
res = orig_threaded_start(server, *args, **kwargs)
|
||||
_start_runner_thread("threaded server")
|
||||
return res
|
||||
|
||||
|
||||
def threaded_stop(server):
|
||||
global runner_thread
|
||||
if runner_thread:
|
||||
runner_thread.stop()
|
||||
res = orig_threaded_stop(server)
|
||||
if runner_thread:
|
||||
runner_thread.join()
|
||||
runner_thread = None
|
||||
return res
|
||||
|
||||
|
||||
server.PreforkServer.__init__ = prefork__init__
|
||||
server.PreforkServer.process_spawn = prefork_process_spawn
|
||||
server.PreforkServer.worker_pop = prefork_worker_pop
|
||||
server.ThreadedServer.start = threaded_start
|
||||
server.ThreadedServer.stop = threaded_stop
|
||||
@@ -1,13 +0,0 @@
|
||||
import odoo
|
||||
|
||||
from .runner import QueueJobRunner
|
||||
|
||||
|
||||
def main():
|
||||
odoo.tools.config.parse_config()
|
||||
runner = QueueJobRunner.from_environ_or_config()
|
||||
runner.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,629 +0,0 @@
|
||||
# Copyright (c) 2015-2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# Copyright 2015-2016 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
"""
|
||||
What is the job runner?
|
||||
-----------------------
|
||||
The job runner is the main process managing the dispatch of delayed jobs to
|
||||
available Odoo workers
|
||||
|
||||
How does it work?
|
||||
-----------------
|
||||
|
||||
* It starts as a thread in the Odoo main process or as a new worker
|
||||
* It receives postgres NOTIFY messages each time jobs are
|
||||
added or updated in the queue_job table.
|
||||
* It maintains an in-memory priority queue of jobs that
|
||||
is populated from the queue_job tables in all databases.
|
||||
* It does not run jobs itself, but asks Odoo to run them through an
|
||||
anonymous ``/queue_job/runjob`` HTTP request. [1]_
|
||||
|
||||
How to use it?
|
||||
--------------
|
||||
|
||||
* Optionally adjust your configuration through environment variables:
|
||||
|
||||
- ``ODOO_QUEUE_JOB_CHANNELS=root:4`` (or any other channels
|
||||
configuration), default ``root:1``.
|
||||
- ``ODOO_QUEUE_JOB_SCHEME=https``, default ``http``.
|
||||
- ``ODOO_QUEUE_JOB_HOST=load-balancer``, default ``http_interface``
|
||||
or ``localhost`` if unset.
|
||||
- ``ODOO_QUEUE_JOB_PORT=443``, default ``http_port`` or 8069 if unset.
|
||||
- ``ODOO_QUEUE_JOB_HTTP_AUTH_USER=jobrunner``, default empty.
|
||||
- ``ODOO_QUEUE_JOB_HTTP_AUTH_PASSWORD=s3cr3t``, default empty.
|
||||
- ``ODOO_QUEUE_JOB_JOBRUNNER_DB_HOST=master-db``, default ``db_host``
|
||||
or ``False`` if unset.
|
||||
- ``ODOO_QUEUE_JOB_JOBRUNNER_DB_PORT=5432``, default ``db_port``
|
||||
or ``False`` if unset.
|
||||
- ``ODOO_QUEUE_JOB_JOBRUNNER_DB_USER=userdb``, default ``db_user``
|
||||
or ``False`` if unset.
|
||||
- ``ODOO_QUEUE_JOB_JOBRUNNER_DB_PASSWORD=passdb``, default ``db_password``
|
||||
or ``False`` if unset.
|
||||
|
||||
* Alternatively, configure the channels through the Odoo configuration
|
||||
file, like:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[queue_job]
|
||||
channels = root:4
|
||||
scheme = https
|
||||
host = load-balancer
|
||||
port = 443
|
||||
http_auth_user = jobrunner
|
||||
http_auth_password = s3cr3t
|
||||
jobrunner_db_host = master-db
|
||||
jobrunner_db_port = 5432
|
||||
jobrunner_db_user = userdb
|
||||
jobrunner_db_password = passdb
|
||||
|
||||
* Or, if using ``anybox.recipe.odoo``, add this to your buildout configuration:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[odoo]
|
||||
recipe = anybox.recipe.odoo
|
||||
(...)
|
||||
queue_job.channels = root:4
|
||||
queue_job.scheme = https
|
||||
queue_job.host = load-balancer
|
||||
queue_job.port = 443
|
||||
queue_job.http_auth_user = jobrunner
|
||||
queue_job.http_auth_password = s3cr3t
|
||||
|
||||
* Start Odoo with ``--load=web,web_kanban,queue_job``
|
||||
and ``--workers`` greater than 1 [2]_, or set the ``server_wide_modules``
|
||||
option in The Odoo configuration file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[options]
|
||||
(...)
|
||||
workers = 4
|
||||
server_wide_modules = web,web_kanban,queue_job
|
||||
(...)
|
||||
|
||||
* Or, if using ``anybox.recipe.odoo``:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[odoo]
|
||||
recipe = anybox.recipe.odoo
|
||||
(...)
|
||||
options.workers = 4
|
||||
options.server_wide_modules = web,web_kanban,queue_job
|
||||
|
||||
* Confirm the runner is starting correctly by checking the odoo log file:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
...INFO...queue_job.jobrunner.runner: starting
|
||||
...INFO...queue_job.jobrunner.runner: initializing database connections
|
||||
...INFO...queue_job.jobrunner.runner: queue job runner ready for db <dbname>
|
||||
...INFO...queue_job.jobrunner.runner: database connections ready
|
||||
|
||||
* Create jobs (eg using base_import_async) and observe they
|
||||
start immediately and in parallel.
|
||||
|
||||
* Tip: to enable debug logging for the queue job, use
|
||||
``--log-handler=odoo.addons.queue_job:DEBUG``
|
||||
|
||||
Caveat
|
||||
------
|
||||
|
||||
* After creating a new database or installing queue_job on an
|
||||
existing database, Odoo must be restarted for the runner to detect it.
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [1] From a security standpoint, it is safe to have an anonymous HTTP
|
||||
request because this request only accepts to run jobs that are
|
||||
enqueued.
|
||||
.. [2] It works with the threaded Odoo server too, although this way
|
||||
of running Odoo is obviously not for production purposes.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import selectors
|
||||
import threading
|
||||
import time
|
||||
from contextlib import closing, contextmanager
|
||||
|
||||
import psycopg2
|
||||
import requests
|
||||
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
|
||||
|
||||
import odoo
|
||||
from odoo.tools import config
|
||||
|
||||
from . import queue_job_config
|
||||
from .channels import ENQUEUED, NOT_DONE, ChannelManager
|
||||
|
||||
SELECT_TIMEOUT = 60
|
||||
ERROR_RECOVERY_DELAY = 5
|
||||
PG_ADVISORY_LOCK_ID = 2293787760715711918
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
select = selectors.DefaultSelector
|
||||
|
||||
|
||||
class MasterElectionLost(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Unfortunately, it is not possible to extend the Odoo
|
||||
# server command line arguments, so we resort to environment variables
|
||||
# to configure the runner (channels mostly).
|
||||
#
|
||||
# On the other hand, the odoo configuration file can be extended at will,
|
||||
# so we check it in addition to the environment variables.
|
||||
|
||||
|
||||
def _channels():
|
||||
return (
|
||||
os.environ.get("ODOO_QUEUE_JOB_CHANNELS")
|
||||
or queue_job_config.get("channels")
|
||||
or "root:1"
|
||||
)
|
||||
|
||||
|
||||
def _datetime_to_epoch(dt):
|
||||
# important: this must return the same as postgresql
|
||||
# EXTRACT(EPOCH FROM TIMESTAMP dt)
|
||||
return (dt - datetime.datetime(1970, 1, 1)).total_seconds()
|
||||
|
||||
|
||||
def _odoo_now():
|
||||
dt = datetime.datetime.utcnow()
|
||||
return _datetime_to_epoch(dt)
|
||||
|
||||
|
||||
def _connection_info_for(db_name):
|
||||
db_or_uri, connection_info = odoo.sql_db.connection_info_for(db_name)
|
||||
|
||||
for p in ("host", "port", "user", "password"):
|
||||
cfg = os.environ.get(
|
||||
"ODOO_QUEUE_JOB_JOBRUNNER_DB_%s" % p.upper()
|
||||
) or queue_job_config.get("jobrunner_db_" + p)
|
||||
|
||||
if cfg:
|
||||
connection_info[p] = cfg
|
||||
|
||||
return connection_info
|
||||
|
||||
|
||||
def _async_http_get(scheme, host, port, user, password, db_name, job_uuid):
|
||||
# TODO: better way to HTTP GET asynchronously (grequest, ...)?
|
||||
# if this was python3 I would be doing this with
|
||||
# asyncio, aiohttp and aiopg
|
||||
def urlopen():
|
||||
url = "{}://{}:{}/queue_job/runjob?db={}&job_uuid={}".format(
|
||||
scheme, host, port, db_name, job_uuid
|
||||
)
|
||||
# pylint: disable=except-pass
|
||||
try:
|
||||
auth = None
|
||||
if user:
|
||||
auth = (user, password)
|
||||
# we are not interested in the result, so we set a short timeout
|
||||
# but not too short so we trap and log hard configuration errors
|
||||
response = requests.get(url, timeout=1, auth=auth)
|
||||
|
||||
# raise_for_status will result in either nothing, a Client Error
|
||||
# for HTTP Response codes between 400 and 500 or a Server Error
|
||||
# for codes between 500 and 600
|
||||
response.raise_for_status()
|
||||
except requests.Timeout:
|
||||
# A timeout is a normal behaviour, it shouldn't be logged as an exception
|
||||
pass
|
||||
except Exception:
|
||||
_logger.exception("exception in GET %s", url)
|
||||
|
||||
thread = threading.Thread(target=urlopen)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, db_name):
|
||||
self.db_name = db_name
|
||||
connection_info = _connection_info_for(db_name)
|
||||
self.conn = psycopg2.connect(**connection_info)
|
||||
try:
|
||||
self.conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
self.has_queue_job = self._has_queue_job()
|
||||
if self.has_queue_job:
|
||||
self._acquire_master_lock()
|
||||
self._initialize()
|
||||
except BaseException:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
# pylint: disable=except-pass
|
||||
# if close fail for any reason, it's either because it's already closed
|
||||
# and we don't care, or for any reason but anyway it will be closed on
|
||||
# del
|
||||
try:
|
||||
self.conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.conn = None
|
||||
|
||||
def _acquire_master_lock(self):
|
||||
"""Acquire the master runner lock or raise MasterElectionLost"""
|
||||
with closing(self.conn.cursor()) as cr:
|
||||
cr.execute("SELECT pg_try_advisory_lock(%s)", (PG_ADVISORY_LOCK_ID,))
|
||||
if not cr.fetchone()[0]:
|
||||
msg = f"could not acquire master runner lock on {self.db_name}"
|
||||
raise MasterElectionLost(msg)
|
||||
|
||||
def _has_queue_job(self):
|
||||
with closing(self.conn.cursor()) as cr:
|
||||
cr.execute(
|
||||
"SELECT 1 FROM pg_tables WHERE tablename=%s", ("ir_module_module",)
|
||||
)
|
||||
if not cr.fetchone():
|
||||
_logger.debug("%s doesn't seem to be an odoo db", self.db_name)
|
||||
return False
|
||||
cr.execute(
|
||||
"SELECT 1 FROM ir_module_module WHERE name=%s AND state=%s",
|
||||
("queue_job", "installed"),
|
||||
)
|
||||
if not cr.fetchone():
|
||||
_logger.debug("queue_job is not installed for db %s", self.db_name)
|
||||
return False
|
||||
cr.execute(
|
||||
"""SELECT COUNT(1)
|
||||
FROM information_schema.triggers
|
||||
WHERE event_object_table = %s
|
||||
AND trigger_name = %s""",
|
||||
("queue_job", "queue_job_notify"),
|
||||
)
|
||||
if cr.fetchone()[0] != 3: # INSERT, DELETE, UPDATE
|
||||
_logger.error(
|
||||
"queue_job_notify trigger is missing in db %s", self.db_name
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _initialize(self):
|
||||
with closing(self.conn.cursor()) as cr:
|
||||
cr.execute("LISTEN queue_job")
|
||||
|
||||
@contextmanager
|
||||
def select_jobs(self, where, args):
|
||||
# pylint: disable=sql-injection
|
||||
# the checker thinks we are injecting values but we are not, we are
|
||||
# adding the where conditions, values are added later properly with
|
||||
# parameters
|
||||
query = (
|
||||
"SELECT channel, uuid, id as seq, date_created, "
|
||||
"priority, EXTRACT(EPOCH FROM eta), state "
|
||||
"FROM queue_job WHERE %s" % (where,)
|
||||
)
|
||||
with closing(self.conn.cursor("select_jobs", withhold=True)) as cr:
|
||||
cr.execute(query, args)
|
||||
yield cr
|
||||
|
||||
def keep_alive(self):
|
||||
query = "SELECT 1"
|
||||
with closing(self.conn.cursor()) as cr:
|
||||
cr.execute(query)
|
||||
|
||||
def set_job_enqueued(self, uuid):
|
||||
with closing(self.conn.cursor()) as cr:
|
||||
cr.execute(
|
||||
"UPDATE queue_job SET state=%s, "
|
||||
"date_enqueued=date_trunc('seconds', "
|
||||
" now() at time zone 'utc') "
|
||||
"WHERE uuid=%s",
|
||||
(ENQUEUED, uuid),
|
||||
)
|
||||
|
||||
def _query_requeue_dead_jobs(self):
|
||||
return """
|
||||
UPDATE
|
||||
queue_job
|
||||
SET
|
||||
state=(
|
||||
CASE
|
||||
WHEN
|
||||
max_retries IS NOT NULL AND
|
||||
max_retries != 0 AND -- infinite retries if max_retries is 0
|
||||
retry IS NOT NULL AND
|
||||
retry>max_retries
|
||||
THEN 'failed'
|
||||
ELSE 'pending'
|
||||
END),
|
||||
retry=(CASE WHEN state='started' THEN COALESCE(retry,0)+1 ELSE retry END),
|
||||
exc_name=(
|
||||
CASE
|
||||
WHEN
|
||||
max_retries IS NOT NULL AND
|
||||
max_retries != 0 AND -- infinite retries if max_retries is 0
|
||||
retry IS NOT NULL AND
|
||||
retry>max_retries
|
||||
THEN 'JobFoundDead'
|
||||
ELSE exc_name
|
||||
END),
|
||||
exc_info=(
|
||||
CASE
|
||||
WHEN
|
||||
max_retries IS NOT NULL AND
|
||||
max_retries != 0 AND -- infinite retries if max_retries is 0
|
||||
retry IS NOT NULL AND
|
||||
retry>max_retries
|
||||
THEN 'Job found dead after too many retries'
|
||||
ELSE exc_info
|
||||
END)
|
||||
WHERE
|
||||
id in (
|
||||
SELECT
|
||||
queue_job_id
|
||||
FROM
|
||||
queue_job_lock
|
||||
WHERE
|
||||
queue_job_id in (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
queue_job
|
||||
WHERE
|
||||
state IN ('enqueued','started')
|
||||
AND date_enqueued <
|
||||
(now() AT TIME ZONE 'utc' - INTERVAL '10 sec')
|
||||
)
|
||||
FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
RETURNING uuid
|
||||
"""
|
||||
|
||||
def requeue_dead_jobs(self):
|
||||
"""
|
||||
Set started and enqueued jobs but not locked to pending
|
||||
|
||||
A job is locked when it's being executed
|
||||
When a job is killed, it releases the lock
|
||||
|
||||
If the number of retries exceeds the number of max retries,
|
||||
the job is set as 'failed' with the error 'JobFoundDead'.
|
||||
|
||||
Adding a buffer on 'date_enqueued' to check
|
||||
that it has been enqueued for more than 10sec.
|
||||
This prevents from requeuing jobs before they are actually started.
|
||||
|
||||
When Odoo shuts down normally, it waits for running jobs to finish.
|
||||
However, when the Odoo server crashes or is otherwise force-stopped,
|
||||
running jobs are interrupted while the runner has no chance to know
|
||||
they have been aborted.
|
||||
"""
|
||||
|
||||
with closing(self.conn.cursor()) as cr:
|
||||
query = self._query_requeue_dead_jobs()
|
||||
|
||||
cr.execute(query)
|
||||
|
||||
for (uuid,) in cr.fetchall():
|
||||
_logger.warning("Re-queued dead job with uuid: %s", uuid)
|
||||
|
||||
|
||||
class QueueJobRunner:
|
||||
def __init__(
|
||||
self,
|
||||
scheme="http",
|
||||
host="localhost",
|
||||
port=8069,
|
||||
user=None,
|
||||
password=None,
|
||||
channel_config_string=None,
|
||||
):
|
||||
self.scheme = scheme
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.channel_manager = ChannelManager()
|
||||
if channel_config_string is None:
|
||||
channel_config_string = _channels()
|
||||
self.channel_manager.simple_configure(channel_config_string)
|
||||
self.db_by_name = {}
|
||||
self._stop = False
|
||||
self._stop_pipe = os.pipe()
|
||||
|
||||
def __del__(self):
|
||||
# pylint: disable=except-pass
|
||||
try:
|
||||
os.close(self._stop_pipe[0])
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.close(self._stop_pipe[1])
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_environ_or_config(cls):
|
||||
scheme = os.environ.get("ODOO_QUEUE_JOB_SCHEME") or queue_job_config.get(
|
||||
"scheme"
|
||||
)
|
||||
host = (
|
||||
os.environ.get("ODOO_QUEUE_JOB_HOST")
|
||||
or queue_job_config.get("host")
|
||||
or config["http_interface"]
|
||||
)
|
||||
port = (
|
||||
os.environ.get("ODOO_QUEUE_JOB_PORT")
|
||||
or queue_job_config.get("port")
|
||||
or config["http_port"]
|
||||
)
|
||||
user = os.environ.get("ODOO_QUEUE_JOB_HTTP_AUTH_USER") or queue_job_config.get(
|
||||
"http_auth_user"
|
||||
)
|
||||
password = os.environ.get(
|
||||
"ODOO_QUEUE_JOB_HTTP_AUTH_PASSWORD"
|
||||
) or queue_job_config.get("http_auth_password")
|
||||
runner = cls(
|
||||
scheme=scheme or "http",
|
||||
host=host or "localhost",
|
||||
port=port or 8069,
|
||||
user=user,
|
||||
password=password,
|
||||
)
|
||||
return runner
|
||||
|
||||
def get_db_names(self):
|
||||
if config["db_name"]:
|
||||
db_names = config["db_name"].split(",")
|
||||
else:
|
||||
db_names = odoo.service.db.list_dbs(True)
|
||||
return db_names
|
||||
|
||||
def close_databases(self, remove_jobs=True):
|
||||
for db_name, db in self.db_by_name.items():
|
||||
try:
|
||||
if remove_jobs:
|
||||
self.channel_manager.remove_db(db_name)
|
||||
db.close()
|
||||
except Exception:
|
||||
_logger.warning("error closing database %s", db_name, exc_info=True)
|
||||
self.db_by_name = {}
|
||||
|
||||
def initialize_databases(self):
|
||||
for db_name in sorted(self.get_db_names()):
|
||||
# sorting is important to avoid deadlocks in acquiring the master lock
|
||||
db = Database(db_name)
|
||||
if db.has_queue_job:
|
||||
self.db_by_name[db_name] = db
|
||||
with db.select_jobs("state in %s", (NOT_DONE,)) as cr:
|
||||
for job_data in cr:
|
||||
self.channel_manager.notify(db_name, *job_data)
|
||||
_logger.info("queue job runner ready for db %s", db_name)
|
||||
else:
|
||||
db.close()
|
||||
|
||||
def requeue_dead_jobs(self):
|
||||
for db in self.db_by_name.values():
|
||||
if db.has_queue_job:
|
||||
db.requeue_dead_jobs()
|
||||
|
||||
def run_jobs(self):
|
||||
now = _odoo_now()
|
||||
for job in self.channel_manager.get_jobs_to_run(now):
|
||||
if self._stop:
|
||||
break
|
||||
_logger.info("asking Odoo to run job %s on db %s", job.uuid, job.db_name)
|
||||
self.db_by_name[job.db_name].set_job_enqueued(job.uuid)
|
||||
_async_http_get(
|
||||
self.scheme,
|
||||
self.host,
|
||||
self.port,
|
||||
self.user,
|
||||
self.password,
|
||||
job.db_name,
|
||||
job.uuid,
|
||||
)
|
||||
|
||||
def process_notifications(self):
|
||||
for db in self.db_by_name.values():
|
||||
if not db.conn.notifies:
|
||||
# If there are no activity in the queue_job table it seems that
|
||||
# tcp keepalives are not sent (in that very specific scenario),
|
||||
# causing some intermediaries (such as haproxy) to close the
|
||||
# connection, making the jobrunner to restart on a socket error
|
||||
db.keep_alive()
|
||||
while db.conn.notifies:
|
||||
if self._stop:
|
||||
break
|
||||
notification = db.conn.notifies.pop()
|
||||
uuid = notification.payload
|
||||
with db.select_jobs("uuid = %s", (uuid,)) as cr:
|
||||
job_datas = cr.fetchone()
|
||||
if job_datas:
|
||||
self.channel_manager.notify(db.db_name, *job_datas)
|
||||
else:
|
||||
self.channel_manager.remove_job(uuid)
|
||||
|
||||
def wait_notification(self):
|
||||
for db in self.db_by_name.values():
|
||||
if db.conn.notifies:
|
||||
# something is going on in the queue, no need to wait
|
||||
return
|
||||
# wait for something to happen in the queue_job tables
|
||||
# we'll select() on database connections and the stop pipe
|
||||
conns = [db.conn for db in self.db_by_name.values()]
|
||||
conns.append(self._stop_pipe[0])
|
||||
# look if the channels specify a wakeup time
|
||||
wakeup_time = self.channel_manager.get_wakeup_time()
|
||||
if not wakeup_time:
|
||||
# this could very well be no timeout at all, because
|
||||
# any activity in the job queue will wake us up, but
|
||||
# let's have a timeout anyway, just to be safe
|
||||
timeout = SELECT_TIMEOUT
|
||||
else:
|
||||
timeout = wakeup_time - _odoo_now()
|
||||
# wait for a notification or a timeout;
|
||||
# if timeout is negative (ie wakeup time in the past),
|
||||
# do not wait; this should rarely happen
|
||||
# because of how get_wakeup_time is designed; actually
|
||||
# if timeout remains a large negative number, it is most
|
||||
# probably a bug
|
||||
_logger.debug("select() timeout: %.2f sec", timeout)
|
||||
if timeout > 0:
|
||||
if conns and not self._stop:
|
||||
with select() as sel:
|
||||
for conn in conns:
|
||||
sel.register(conn, selectors.EVENT_READ)
|
||||
events = sel.select(timeout=timeout)
|
||||
for key, _mask in events:
|
||||
if key.fileobj == self._stop_pipe[0]:
|
||||
# stop-pipe is not a conn so doesn't need poll()
|
||||
continue
|
||||
key.fileobj.poll()
|
||||
|
||||
def stop(self):
|
||||
_logger.info("graceful stop requested")
|
||||
self._stop = True
|
||||
# wakeup the select() in wait_notification
|
||||
os.write(self._stop_pipe[1], b".")
|
||||
|
||||
def run(self):
|
||||
_logger.info("starting")
|
||||
while not self._stop:
|
||||
# outer loop does exception recovery
|
||||
try:
|
||||
_logger.debug("initializing database connections")
|
||||
# TODO: how to detect new databases or databases
|
||||
# on which queue_job is installed after server start?
|
||||
self.initialize_databases()
|
||||
_logger.info("database connections ready")
|
||||
# inner loop does the normal processing
|
||||
while not self._stop:
|
||||
self.requeue_dead_jobs()
|
||||
self.process_notifications()
|
||||
self.run_jobs()
|
||||
self.wait_notification()
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
except InterruptedError:
|
||||
# Interrupted system call, i.e. KeyboardInterrupt during select
|
||||
self.stop()
|
||||
except MasterElectionLost as e:
|
||||
_logger.debug(
|
||||
"master election lost: %s, sleeping %ds and retrying",
|
||||
e,
|
||||
ERROR_RECOVERY_DELAY,
|
||||
)
|
||||
self.close_databases()
|
||||
time.sleep(ERROR_RECOVERY_DELAY)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"exception: sleeping %ds and retrying", ERROR_RECOVERY_DELAY
|
||||
)
|
||||
self.close_databases()
|
||||
time.sleep(ERROR_RECOVERY_DELAY)
|
||||
self.close_databases(remove_jobs=False)
|
||||
_logger.info("stopped")
|
||||
@@ -1,47 +0,0 @@
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
with api.Environment.manage():
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
_logger.info("Computing exception name for failed jobs")
|
||||
_compute_jobs_new_values(env)
|
||||
|
||||
|
||||
def _compute_jobs_new_values(env):
|
||||
for job in env["queue.job"].search(
|
||||
[("state", "=", "failed"), ("exc_info", "!=", False)]
|
||||
):
|
||||
exception_details = _get_exception_details(job)
|
||||
if exception_details:
|
||||
job.update(exception_details)
|
||||
|
||||
|
||||
def _get_exception_details(job):
|
||||
for line in reversed(job.exc_info.splitlines()):
|
||||
if _find_exception(line):
|
||||
name, msg = line.split(":", 1)
|
||||
return {
|
||||
"exc_name": name.strip(),
|
||||
"exc_message": msg.strip("()', \""),
|
||||
}
|
||||
|
||||
|
||||
def _find_exception(line):
|
||||
# Just a list of common errors.
|
||||
# If you want to target others, add your own migration step for your db.
|
||||
exceptions = (
|
||||
"Error:", # catch all well named exceptions
|
||||
# other live instance errors found
|
||||
"requests.exceptions.MissingSchema",
|
||||
"botocore.errorfactory.NoSuchKey",
|
||||
)
|
||||
for exc in exceptions:
|
||||
if exc in line:
|
||||
return exc
|
||||
@@ -1,33 +0,0 @@
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
from odoo.tools.sql import column_exists, table_exists
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
if table_exists(cr, "queue_job") and not column_exists(
|
||||
cr, "queue_job", "exec_time"
|
||||
):
|
||||
# Disable trigger otherwise the update takes ages.
|
||||
cr.execute(
|
||||
"""
|
||||
ALTER TABLE queue_job DISABLE TRIGGER queue_job_notify;
|
||||
"""
|
||||
)
|
||||
cr.execute(
|
||||
"""
|
||||
ALTER TABLE queue_job ADD COLUMN exec_time double precision DEFAULT 0;
|
||||
"""
|
||||
)
|
||||
cr.execute(
|
||||
"""
|
||||
UPDATE
|
||||
queue_job
|
||||
SET
|
||||
exec_time = EXTRACT(EPOCH FROM (date_done - date_started));
|
||||
"""
|
||||
)
|
||||
cr.execute(
|
||||
"""
|
||||
ALTER TABLE queue_job ENABLE TRIGGER queue_job_notify;
|
||||
"""
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
from openupgradelib import openupgrade
|
||||
|
||||
|
||||
@openupgrade.migrate()
|
||||
def migrate(env, version):
|
||||
# Remove cron garbage collector
|
||||
openupgrade.delete_records_safely_by_xml_id(
|
||||
env,
|
||||
["queue_job.ir_cron_queue_job_garbage_collector"],
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
from odoo.tools.sql import table_exists
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
if table_exists(cr, "queue_job"):
|
||||
# Drop index 'queue_job_identity_key_state_partial_index',
|
||||
# it will be recreated during the update
|
||||
cr.execute("DROP INDEX IF EXISTS queue_job_identity_key_state_partial_index;")
|
||||
@@ -1,6 +0,0 @@
|
||||
from . import base
|
||||
from . import ir_model_fields
|
||||
from . import queue_job
|
||||
from . import queue_job_channel
|
||||
from . import queue_job_function
|
||||
from . import queue_job_lock
|
||||
@@ -1,266 +0,0 @@
|
||||
# Copyright 2016 Camptocamp
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import functools
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
from ..delay import Delayable, DelayableRecordset
|
||||
from ..utils import must_run_without_delay
|
||||
|
||||
|
||||
class Base(models.AbstractModel):
|
||||
"""The base model, which is implicitly inherited by all models.
|
||||
|
||||
A new :meth:`~with_delay` method is added on all Odoo Models, allowing to
|
||||
postpone the execution of a job method in an asynchronous process.
|
||||
"""
|
||||
|
||||
_inherit = "base"
|
||||
|
||||
def with_delay(
|
||||
self,
|
||||
priority=None,
|
||||
eta=None,
|
||||
max_retries=None,
|
||||
description=None,
|
||||
channel=None,
|
||||
identity_key=None,
|
||||
):
|
||||
"""Return a ``DelayableRecordset``
|
||||
|
||||
It is a shortcut for the longer form as shown below::
|
||||
|
||||
self.with_delay(priority=20).action_done()
|
||||
# is equivalent to:
|
||||
self.delayable().set(priority=20).action_done().delay()
|
||||
|
||||
``with_delay()`` accepts job properties which specify how the job will
|
||||
be executed.
|
||||
|
||||
Usage with job properties::
|
||||
|
||||
env['a.model'].with_delay(priority=30, eta=60*60*5).action_done()
|
||||
delayable.export_one_thing(the_thing_to_export)
|
||||
# => the job will be executed with a low priority and not before a
|
||||
# delay of 5 hours from now
|
||||
|
||||
When using :meth:``with_delay``, the final ``delay()`` is implicit.
|
||||
See the documentation of :meth:``delayable`` for more details.
|
||||
|
||||
:return: instance of a DelayableRecordset
|
||||
:rtype: :class:`odoo.addons.queue_job.job.DelayableRecordset`
|
||||
"""
|
||||
return DelayableRecordset(
|
||||
self,
|
||||
priority=priority,
|
||||
eta=eta,
|
||||
max_retries=max_retries,
|
||||
description=description,
|
||||
channel=channel,
|
||||
identity_key=identity_key,
|
||||
)
|
||||
|
||||
def delayable(
|
||||
self,
|
||||
priority=None,
|
||||
eta=None,
|
||||
max_retries=None,
|
||||
description=None,
|
||||
channel=None,
|
||||
identity_key=None,
|
||||
):
|
||||
"""Return a ``Delayable``
|
||||
|
||||
The returned instance allows to enqueue any method of the recordset's
|
||||
Model.
|
||||
|
||||
Usage::
|
||||
|
||||
delayable = self.env["res.users"].browse(10).delayable(priority=20)
|
||||
delayable.do_work(name="test"}).delay()
|
||||
|
||||
In this example, the ``do_work`` method will not be executed directly.
|
||||
It will be executed in an asynchronous job.
|
||||
|
||||
Method calls on a Delayable generally return themselves, so calls can
|
||||
be chained together::
|
||||
|
||||
delayable.set(priority=15).do_work(name="test"}).delay()
|
||||
|
||||
The order of the calls that build the job is not relevant, beside
|
||||
the call to ``delay()`` that must happen at the very end. This is
|
||||
equivalent to the example above::
|
||||
|
||||
delayable.do_work(name="test"}).set(priority=15).delay()
|
||||
|
||||
Very importantly, ``delay()`` must be called on the top-most parent
|
||||
of a chain of jobs, so if you have this::
|
||||
|
||||
job1 = record1.delayable().do_work()
|
||||
job2 = record2.delayable().do_work()
|
||||
job1.on_done(job2)
|
||||
|
||||
The ``delay()`` call must be made on ``job1``, otherwise ``job2`` will
|
||||
be delayed, but ``job1`` will never be. When done on ``job1``, the
|
||||
``delay()`` call will traverse the graph of jobs and delay all of
|
||||
them::
|
||||
|
||||
job1.delay()
|
||||
|
||||
For more details on the graph dependencies, read the documentation of
|
||||
:module:`~odoo.addons.queue_job.delay`.
|
||||
|
||||
:param priority: Priority of the job, 0 being the higher priority.
|
||||
Default is 10.
|
||||
:param eta: Estimated Time of Arrival of the job. It will not be
|
||||
executed before this date/time.
|
||||
:param max_retries: maximum number of retries before giving up and set
|
||||
the job state to 'failed'. A value of 0 means
|
||||
infinite retries. Default is 5.
|
||||
:param description: human description of the job. If None, description
|
||||
is computed from the function doc or name
|
||||
:param channel: the complete name of the channel to use to process
|
||||
the function. If specified it overrides the one
|
||||
defined on the function
|
||||
:param identity_key: key uniquely identifying the job, if specified
|
||||
and a job with the same key has not yet been run,
|
||||
the new job will not be added. It is either a
|
||||
string, either a function that takes the job as
|
||||
argument (see :py:func:`..job.identity_exact`).
|
||||
the new job will not be added.
|
||||
:return: instance of a Delayable
|
||||
:rtype: :class:`odoo.addons.queue_job.job.Delayable`
|
||||
"""
|
||||
return Delayable(
|
||||
self,
|
||||
priority=priority,
|
||||
eta=eta,
|
||||
max_retries=max_retries,
|
||||
description=description,
|
||||
channel=channel,
|
||||
identity_key=identity_key,
|
||||
)
|
||||
|
||||
def _patch_job_auto_delay(self, method_name, context_key=None):
|
||||
"""Patch a method to be automatically delayed as job method when called
|
||||
|
||||
This patch method has to be called in ``_register_hook`` (example
|
||||
below).
|
||||
|
||||
When a method is patched, any call to the method will not directly
|
||||
execute the method's body, but will instead enqueue a job.
|
||||
|
||||
When a ``context_key`` is set when calling ``_patch_job_auto_delay``,
|
||||
the patched method is automatically delayed only when this key is
|
||||
``True`` in the caller's context. It is advised to patch the method
|
||||
with a ``context_key``, because making the automatic delay *in any
|
||||
case* can produce nasty and unexpected side effects (e.g. another
|
||||
module calls the method and expects it to be computed before doing
|
||||
something else, expecting a result, ...).
|
||||
|
||||
A typical use case is when a method in a module we don't control is
|
||||
called synchronously in the middle of another method, and we'd like all
|
||||
the calls to this method become asynchronous.
|
||||
|
||||
The options of the job usually passed to ``with_delay()`` (priority,
|
||||
description, identity_key, ...) can be returned in a dictionary by a
|
||||
method named after the name of the method suffixed by ``_job_options``
|
||||
which takes the same parameters as the initial method.
|
||||
|
||||
It is still possible to force synchronous execution of the method by
|
||||
setting a key ``_job_force_sync`` to True in the environment context.
|
||||
|
||||
Example patching the "foo" method to be automatically delayed as job
|
||||
(the job options method is optional):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# original method:
|
||||
def foo(self, arg1):
|
||||
print("hello", arg1)
|
||||
|
||||
def large_method(self):
|
||||
# doing a lot of things
|
||||
self.foo("world)
|
||||
# doing a lot of other things
|
||||
|
||||
def button_x(self):
|
||||
self.with_context(auto_delay_foo=True).large_method()
|
||||
|
||||
# auto delay patch:
|
||||
def foo_job_options(self, arg1):
|
||||
return {
|
||||
"priority": 100,
|
||||
"description": "Saying hello to {}".format(arg1)
|
||||
}
|
||||
|
||||
def _register_hook(self):
|
||||
self._patch_method(
|
||||
"foo",
|
||||
self._patch_job_auto_delay("foo", context_key="auto_delay_foo")
|
||||
)
|
||||
return super()._register_hook()
|
||||
|
||||
The result when ``button_x`` is called, is that a new job for ``foo``
|
||||
is delayed.
|
||||
"""
|
||||
|
||||
def auto_delay_wrapper(self, *args, **kwargs):
|
||||
# when no context_key is set, we delay in any case (warning, can be
|
||||
# dangerous)
|
||||
context_delay = self.env.context.get(context_key) if context_key else True
|
||||
if (
|
||||
self.env.context.get("job_uuid")
|
||||
or not context_delay
|
||||
or must_run_without_delay(self.env)
|
||||
):
|
||||
# we are in the job execution
|
||||
return auto_delay_wrapper.origin(self, *args, **kwargs)
|
||||
else:
|
||||
# replace the synchronous call by a job on itself
|
||||
method_name = auto_delay_wrapper.origin.__name__
|
||||
job_options_method = getattr(
|
||||
self, "{}_job_options".format(method_name), None
|
||||
)
|
||||
job_options = {}
|
||||
if job_options_method:
|
||||
job_options.update(job_options_method(*args, **kwargs))
|
||||
delayed = self.with_delay(**job_options)
|
||||
return getattr(delayed, method_name)(*args, **kwargs)
|
||||
|
||||
origin = getattr(self, method_name)
|
||||
return functools.update_wrapper(auto_delay_wrapper, origin)
|
||||
|
||||
@api.model
|
||||
def _job_store_values(self, job):
|
||||
"""Hook for manipulating job stored values.
|
||||
|
||||
You can define a more specific hook for a job function
|
||||
by defining a method name with this pattern:
|
||||
|
||||
`_queue_job_store_values_${func_name}`
|
||||
|
||||
NOTE: values will be stored only if they match stored fields on `queue.job`.
|
||||
|
||||
:param job: current queue_job.job.Job instance.
|
||||
:return: dictionary for setting job values.
|
||||
"""
|
||||
return {}
|
||||
|
||||
@api.model
|
||||
def _job_prepare_context_before_enqueue_keys(self):
|
||||
"""Keys to keep in context of stored jobs
|
||||
Empty by default for backward compatibility.
|
||||
"""
|
||||
return ("tz", "lang", "allowed_company_ids", "force_company", "active_test")
|
||||
|
||||
def _job_prepare_context_before_enqueue(self):
|
||||
"""Return the context to store in the jobs
|
||||
Can be used to keep only safe keys.
|
||||
"""
|
||||
return {
|
||||
key: value
|
||||
for key, value in self.env.context.items()
|
||||
if key in self._job_prepare_context_before_enqueue_keys()
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
# Copyright 2020 Camptocamp
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class IrModelFields(models.Model):
|
||||
_inherit = "ir.model.fields"
|
||||
|
||||
ttype = fields.Selection(
|
||||
selection_add=[("job_serialized", "Job Serialized")],
|
||||
ondelete={"job_serialized": "cascade"},
|
||||
)
|
||||
@@ -1,463 +0,0 @@
|
||||
# Copyright 2013-2020 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
from odoo.tools import config, html_escape, index_exists
|
||||
|
||||
from odoo.addons.base_sparse_field.models.fields import Serialized
|
||||
|
||||
from ..delay import Graph
|
||||
from ..exception import JobError
|
||||
from ..fields import JobSerialized
|
||||
from ..job import (
|
||||
CANCELLED,
|
||||
DONE,
|
||||
FAILED,
|
||||
PENDING,
|
||||
STARTED,
|
||||
STATES,
|
||||
WAIT_DEPENDENCIES,
|
||||
Job,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QueueJob(models.Model):
|
||||
"""Model storing the jobs to be executed."""
|
||||
|
||||
_name = "queue.job"
|
||||
_description = "Queue Job"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
_log_access = False
|
||||
|
||||
_order = "date_created DESC, date_done DESC"
|
||||
|
||||
_removal_interval = 30 # days
|
||||
_default_related_action = "related_action_open_record"
|
||||
|
||||
# This must be passed in a context key "_job_edit_sentinel" to write on
|
||||
# protected fields. It protects against crafting "queue.job" records from
|
||||
# RPC (e.g. on internal methods). When ``with_delay`` is used, the sentinel
|
||||
# is set.
|
||||
EDIT_SENTINEL = object()
|
||||
_protected_fields = (
|
||||
"uuid",
|
||||
"name",
|
||||
"date_created",
|
||||
"model_name",
|
||||
"method_name",
|
||||
"func_string",
|
||||
"channel_method_name",
|
||||
"job_function_id",
|
||||
"records",
|
||||
"args",
|
||||
"kwargs",
|
||||
)
|
||||
|
||||
uuid = fields.Char(string="UUID", readonly=True, index=True, required=True)
|
||||
graph_uuid = fields.Char(
|
||||
string="Graph UUID",
|
||||
readonly=True,
|
||||
index=True,
|
||||
help="Single shared identifier of a Graph. Empty for a single job.",
|
||||
)
|
||||
user_id = fields.Many2one(comodel_name="res.users", string="User ID")
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company", string="Company", index=True
|
||||
)
|
||||
name = fields.Char(string="Description", readonly=True)
|
||||
|
||||
model_name = fields.Char(string="Model", readonly=True)
|
||||
method_name = fields.Char(readonly=True)
|
||||
# record_ids field is only for backward compatibility (e.g. used in related
|
||||
# actions), can be removed (replaced by "records") in 14.0
|
||||
record_ids = JobSerialized(compute="_compute_record_ids", base_type=list)
|
||||
records = JobSerialized(
|
||||
string="Record(s)",
|
||||
readonly=True,
|
||||
base_type=models.BaseModel,
|
||||
)
|
||||
dependencies = Serialized(readonly=True)
|
||||
# dependency graph as expected by the field widget
|
||||
dependency_graph = Serialized(compute="_compute_dependency_graph")
|
||||
graph_jobs_count = fields.Integer(compute="_compute_graph_jobs_count")
|
||||
args = JobSerialized(readonly=True, base_type=tuple)
|
||||
kwargs = JobSerialized(readonly=True, base_type=dict)
|
||||
func_string = fields.Char(string="Task", readonly=True)
|
||||
|
||||
state = fields.Selection(STATES, readonly=True, required=True, index=True)
|
||||
priority = fields.Integer(group_operator=False)
|
||||
exc_name = fields.Char(string="Exception", readonly=True)
|
||||
exc_message = fields.Char(string="Exception Message", readonly=True, tracking=True)
|
||||
exc_info = fields.Text(string="Exception Info", readonly=True)
|
||||
result = fields.Text(readonly=True)
|
||||
|
||||
date_created = fields.Datetime(string="Created Date", readonly=True)
|
||||
date_started = fields.Datetime(string="Start Date", readonly=True)
|
||||
date_enqueued = fields.Datetime(string="Enqueue Time", readonly=True)
|
||||
date_done = fields.Datetime(readonly=True)
|
||||
exec_time = fields.Float(
|
||||
string="Execution Time (avg)",
|
||||
group_operator="avg",
|
||||
help="Time required to execute this job in seconds. Average when grouped.",
|
||||
)
|
||||
date_cancelled = fields.Datetime(readonly=True)
|
||||
|
||||
eta = fields.Datetime(string="Execute only after")
|
||||
retry = fields.Integer(string="Current try")
|
||||
max_retries = fields.Integer(
|
||||
string="Max. retries",
|
||||
help="The job will fail if the number of tries reach the "
|
||||
"max. retries.\n"
|
||||
"Retries are infinite when empty.",
|
||||
)
|
||||
# FIXME the name of this field is very confusing
|
||||
channel_method_name = fields.Char(string="Complete Method Name", readonly=True)
|
||||
job_function_id = fields.Many2one(
|
||||
comodel_name="queue.job.function",
|
||||
string="Job Function",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
channel = fields.Char(index=True)
|
||||
|
||||
identity_key = fields.Char(readonly=True)
|
||||
worker_pid = fields.Integer(readonly=True)
|
||||
|
||||
def init(self):
|
||||
index_1 = "queue_job_identity_key_state_partial_index"
|
||||
index_2 = "queue_job_channel_date_done_date_created_index"
|
||||
if not index_exists(self._cr, index_1):
|
||||
# Used by Job.job_record_with_same_identity_key
|
||||
self._cr.execute(
|
||||
"CREATE INDEX queue_job_identity_key_state_partial_index "
|
||||
"ON queue_job (identity_key) WHERE state in ('pending', "
|
||||
"'enqueued', 'wait_dependencies') AND identity_key IS NOT NULL;"
|
||||
)
|
||||
if not index_exists(self._cr, index_2):
|
||||
# Used by <queue.job>.autovacuum
|
||||
self._cr.execute(
|
||||
"CREATE INDEX queue_job_channel_date_done_date_created_index "
|
||||
"ON queue_job (channel, date_done, date_created);"
|
||||
)
|
||||
|
||||
@api.depends("records")
|
||||
def _compute_record_ids(self):
|
||||
for record in self:
|
||||
record.record_ids = record.records.ids
|
||||
|
||||
@api.depends("dependencies")
|
||||
def _compute_dependency_graph(self):
|
||||
jobs_groups = self.env["queue.job"].read_group(
|
||||
[
|
||||
(
|
||||
"graph_uuid",
|
||||
"in",
|
||||
[uuid for uuid in self.mapped("graph_uuid") if uuid],
|
||||
)
|
||||
],
|
||||
["graph_uuid", "ids:array_agg(id)"],
|
||||
["graph_uuid"],
|
||||
)
|
||||
ids_per_graph_uuid = {
|
||||
group["graph_uuid"]: group["ids"] for group in jobs_groups
|
||||
}
|
||||
for record in self:
|
||||
if not record.graph_uuid:
|
||||
record.dependency_graph = {}
|
||||
continue
|
||||
|
||||
graph_jobs = self.browse(ids_per_graph_uuid.get(record.graph_uuid) or [])
|
||||
if not graph_jobs:
|
||||
record.dependency_graph = {}
|
||||
continue
|
||||
|
||||
graph_ids = {graph_job.uuid: graph_job.id for graph_job in graph_jobs}
|
||||
graph_jobs_by_ids = {graph_job.id: graph_job for graph_job in graph_jobs}
|
||||
|
||||
graph = Graph()
|
||||
for graph_job in graph_jobs:
|
||||
graph.add_vertex(graph_job.id)
|
||||
for parent_uuid in graph_job.dependencies["depends_on"]:
|
||||
parent_id = graph_ids.get(parent_uuid)
|
||||
if not parent_id:
|
||||
continue
|
||||
graph.add_edge(parent_id, graph_job.id)
|
||||
for child_uuid in graph_job.dependencies["reverse_depends_on"]:
|
||||
child_id = graph_ids.get(child_uuid)
|
||||
if not child_id:
|
||||
continue
|
||||
graph.add_edge(graph_job.id, child_id)
|
||||
|
||||
record.dependency_graph = {
|
||||
# list of ids
|
||||
"nodes": [
|
||||
graph_jobs_by_ids[graph_id]._dependency_graph_vis_node()
|
||||
for graph_id in graph.vertices()
|
||||
],
|
||||
# list of tuples (from, to)
|
||||
"edges": graph.edges(),
|
||||
}
|
||||
|
||||
def _dependency_graph_vis_node(self):
|
||||
"""Return the node as expected by the JobDirectedGraph widget"""
|
||||
default = ("#D2E5FF", "#2B7CE9")
|
||||
colors = {
|
||||
DONE: ("#C2FABC", "#4AD63A"),
|
||||
FAILED: ("#FB7E81", "#FA0A10"),
|
||||
STARTED: ("#FFFF00", "#FFA500"),
|
||||
}
|
||||
return {
|
||||
"id": self.id,
|
||||
"title": "<strong>%s</strong><br/>%s"
|
||||
% (
|
||||
html_escape(self.display_name),
|
||||
html_escape(self.func_string),
|
||||
),
|
||||
"color": colors.get(self.state, default)[0],
|
||||
"border": colors.get(self.state, default)[1],
|
||||
"shadow": True,
|
||||
}
|
||||
|
||||
def _compute_graph_jobs_count(self):
|
||||
jobs_groups = self.env["queue.job"].read_group(
|
||||
[
|
||||
(
|
||||
"graph_uuid",
|
||||
"in",
|
||||
[uuid for uuid in self.mapped("graph_uuid") if uuid],
|
||||
)
|
||||
],
|
||||
["graph_uuid"],
|
||||
["graph_uuid"],
|
||||
)
|
||||
count_per_graph_uuid = {
|
||||
group["graph_uuid"]: group["graph_uuid_count"] for group in jobs_groups
|
||||
}
|
||||
for record in self:
|
||||
record.graph_jobs_count = count_per_graph_uuid.get(record.graph_uuid) or 0
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if self.env.context.get("_job_edit_sentinel") is not self.EDIT_SENTINEL:
|
||||
# Prevent to create a queue.job record "raw" from RPC.
|
||||
# ``with_delay()`` must be used.
|
||||
raise exceptions.AccessError(
|
||||
_("Queue jobs must be created by calling 'with_delay()'.")
|
||||
)
|
||||
return super(
|
||||
QueueJob,
|
||||
self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True),
|
||||
).create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
if self.env.context.get("_job_edit_sentinel") is not self.EDIT_SENTINEL:
|
||||
write_on_protected_fields = [
|
||||
fieldname for fieldname in vals if fieldname in self._protected_fields
|
||||
]
|
||||
if write_on_protected_fields:
|
||||
raise exceptions.AccessError(
|
||||
_("Not allowed to change field(s): {}").format(
|
||||
write_on_protected_fields
|
||||
)
|
||||
)
|
||||
|
||||
different_user_jobs = self.browse()
|
||||
if vals.get("user_id"):
|
||||
different_user_jobs = self.filtered(
|
||||
lambda records: records.env.user.id != vals["user_id"]
|
||||
)
|
||||
|
||||
if vals.get("state") == "failed":
|
||||
self._message_post_on_failure()
|
||||
|
||||
result = super().write(vals)
|
||||
|
||||
for record in different_user_jobs:
|
||||
# the user is stored in the env of the record, but we still want to
|
||||
# have a stored user_id field to be able to search/groupby, so
|
||||
# synchronize the env of records with user_id
|
||||
super(QueueJob, record).write(
|
||||
{"records": record.records.with_user(vals["user_id"])}
|
||||
)
|
||||
return result
|
||||
|
||||
def open_related_action(self):
|
||||
"""Open the related action associated to the job"""
|
||||
self.ensure_one()
|
||||
job = Job.load(self.env, self.uuid)
|
||||
action = job.related_action()
|
||||
if action is None:
|
||||
raise exceptions.UserError(_("No action available for this job"))
|
||||
return action
|
||||
|
||||
def open_graph_jobs(self):
|
||||
"""Return action that opens all jobs of the same graph"""
|
||||
self.ensure_one()
|
||||
jobs = self.env["queue.job"].search([("graph_uuid", "=", self.graph_uuid)])
|
||||
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"queue_job.action_queue_job"
|
||||
)
|
||||
action.update(
|
||||
{
|
||||
"name": _("Jobs for graph %s") % (self.graph_uuid),
|
||||
"context": {},
|
||||
"domain": [("id", "in", jobs.ids)],
|
||||
}
|
||||
)
|
||||
return action
|
||||
|
||||
def _change_job_state(self, state, result=None):
|
||||
"""Change the state of the `Job` object
|
||||
|
||||
Changing the state of the Job will automatically change some fields
|
||||
(date, result, ...).
|
||||
"""
|
||||
for record in self:
|
||||
job_ = Job.load(record.env, record.uuid)
|
||||
if state == DONE:
|
||||
job_.set_done(result=result)
|
||||
job_.store()
|
||||
record.env["queue.job"].flush_model()
|
||||
job_.enqueue_waiting()
|
||||
elif state == PENDING:
|
||||
job_.set_pending(result=result)
|
||||
job_.store()
|
||||
elif state == CANCELLED:
|
||||
job_.set_cancelled(result=result)
|
||||
job_.store()
|
||||
record.env["queue.job"].flush_model()
|
||||
job_.cancel_dependent_jobs()
|
||||
else:
|
||||
raise ValueError("State not supported: %s" % state)
|
||||
|
||||
def button_done(self):
|
||||
result = _("Manually set to done by %s") % self.env.user.name
|
||||
self._change_job_state(DONE, result=result)
|
||||
return True
|
||||
|
||||
def button_cancelled(self):
|
||||
result = _("Cancelled by %s") % self.env.user.name
|
||||
self._change_job_state(CANCELLED, result=result)
|
||||
return True
|
||||
|
||||
def requeue(self):
|
||||
jobs_to_requeue = self.filtered(lambda job_: job_.state != WAIT_DEPENDENCIES)
|
||||
jobs_to_requeue._change_job_state(PENDING)
|
||||
return jobs_to_requeue
|
||||
|
||||
def _message_post_on_failure(self):
|
||||
# subscribe the users now to avoid to subscribe them
|
||||
# at every job creation
|
||||
domain = self._subscribe_users_domain()
|
||||
base_users = self.env["res.users"].search(domain)
|
||||
for record in self:
|
||||
users = base_users | record.user_id
|
||||
record.message_subscribe(partner_ids=users.mapped("partner_id").ids)
|
||||
msg = record._message_failed_job()
|
||||
if msg:
|
||||
record.message_post(body=msg, subtype_xmlid="queue_job.mt_job_failed")
|
||||
|
||||
def _subscribe_users_domain(self):
|
||||
"""Subscribe all users having the 'Queue Job Manager' group"""
|
||||
group = self.env.ref("queue_job.group_queue_job_manager")
|
||||
if not group:
|
||||
return None
|
||||
companies = self.mapped("company_id")
|
||||
domain = [("groups_id", "=", group.id)]
|
||||
if companies:
|
||||
domain.append(("company_id", "in", companies.ids))
|
||||
return domain
|
||||
|
||||
def _message_failed_job(self):
|
||||
"""Return a message which will be posted on the job when it is failed.
|
||||
|
||||
It can be inherited to allow more precise messages based on the
|
||||
exception informations.
|
||||
|
||||
If nothing is returned, no message will be posted.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return _(
|
||||
"Something bad happened during the execution of job %s. "
|
||||
"More details in the 'Exception Information' section.",
|
||||
self.uuid,
|
||||
)
|
||||
|
||||
def _needaction_domain_get(self):
|
||||
"""Returns the domain to filter records that require an action
|
||||
|
||||
:return: domain or False is no action
|
||||
"""
|
||||
return [("state", "=", "failed")]
|
||||
|
||||
def autovacuum(self):
|
||||
"""Delete all jobs done based on the removal interval defined on the
|
||||
channel
|
||||
|
||||
Called from a cron.
|
||||
"""
|
||||
for channel in self.env["queue.job.channel"].search([]):
|
||||
deadline = datetime.now() - timedelta(days=int(channel.removal_interval))
|
||||
while True:
|
||||
jobs = self.search(
|
||||
[
|
||||
"|",
|
||||
("date_done", "<=", deadline),
|
||||
("date_cancelled", "<=", deadline),
|
||||
("channel", "=", channel.complete_name),
|
||||
],
|
||||
order="date_done, date_created",
|
||||
limit=1000,
|
||||
)
|
||||
if jobs:
|
||||
jobs.unlink()
|
||||
if not config["test_enable"]:
|
||||
self.env.cr.commit() # pylint: disable=E8102
|
||||
else:
|
||||
break
|
||||
return True
|
||||
|
||||
def related_action_open_record(self):
|
||||
"""Open a form view with the record(s) of the job.
|
||||
|
||||
For instance, for a job on a ``product.product``, it will open a
|
||||
``product.product`` form view with the product record(s) concerned by
|
||||
the job. If the job concerns more than one record, it opens them in a
|
||||
list.
|
||||
|
||||
This is the default related action.
|
||||
|
||||
"""
|
||||
self.ensure_one()
|
||||
records = self.records.exists()
|
||||
if not records:
|
||||
return None
|
||||
action = {
|
||||
"name": _("Related Record"),
|
||||
"type": "ir.actions.act_window",
|
||||
"view_mode": "form",
|
||||
"res_model": records._name,
|
||||
}
|
||||
if len(records) == 1:
|
||||
action["res_id"] = records.id
|
||||
else:
|
||||
action.update(
|
||||
{
|
||||
"name": _("Related Records"),
|
||||
"view_mode": "tree,form",
|
||||
"domain": [("id", "in", records.ids)],
|
||||
}
|
||||
)
|
||||
return action
|
||||
|
||||
def _test_job(self, failure_rate=0):
|
||||
_logger.info("Running test job.")
|
||||
if random.random() <= failure_rate:
|
||||
raise JobError("Job failed")
|
||||
@@ -1,94 +0,0 @@
|
||||
# Copyright 2013-2020 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
|
||||
class QueueJobChannel(models.Model):
|
||||
_name = "queue.job.channel"
|
||||
_description = "Job Channels"
|
||||
|
||||
name = fields.Char()
|
||||
complete_name = fields.Char(
|
||||
compute="_compute_complete_name", store=True, readonly=True, recursive=True
|
||||
)
|
||||
parent_id = fields.Many2one(
|
||||
comodel_name="queue.job.channel", string="Parent Channel", ondelete="restrict"
|
||||
)
|
||||
job_function_ids = fields.One2many(
|
||||
comodel_name="queue.job.function",
|
||||
inverse_name="channel_id",
|
||||
string="Job Functions",
|
||||
)
|
||||
removal_interval = fields.Integer(
|
||||
default=lambda self: self.env["queue.job"]._removal_interval, required=True
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
("name_uniq", "unique(complete_name)", "Channel complete name must be unique")
|
||||
]
|
||||
|
||||
@api.depends("name", "parent_id.complete_name")
|
||||
def _compute_complete_name(self):
|
||||
for record in self:
|
||||
if not record.name:
|
||||
complete_name = "" # new record
|
||||
elif record.parent_id:
|
||||
complete_name = ".".join([record.parent_id.complete_name, record.name])
|
||||
else:
|
||||
complete_name = record.name
|
||||
record.complete_name = complete_name
|
||||
|
||||
@api.constrains("parent_id", "name")
|
||||
def parent_required(self):
|
||||
for record in self:
|
||||
if record.name != "root" and not record.parent_id:
|
||||
raise exceptions.ValidationError(_("Parent channel required."))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = self.browse()
|
||||
if self.env.context.get("install_mode"):
|
||||
# installing a module that creates a channel: rebinds the channel
|
||||
# to an existing one (likely we already had the channel created by
|
||||
# the @job decorator previously)
|
||||
new_vals_list = []
|
||||
for vals in vals_list:
|
||||
name = vals.get("name")
|
||||
parent_id = vals.get("parent_id")
|
||||
if name and parent_id:
|
||||
existing = self.search(
|
||||
[("name", "=", name), ("parent_id", "=", parent_id)]
|
||||
)
|
||||
if existing:
|
||||
if not existing.get_metadata()[0].get("noupdate"):
|
||||
existing.write(vals)
|
||||
records |= existing
|
||||
continue
|
||||
new_vals_list.append(vals)
|
||||
vals_list = new_vals_list
|
||||
records |= super().create(vals_list)
|
||||
return records
|
||||
|
||||
def write(self, values):
|
||||
for channel in self:
|
||||
if (
|
||||
not self.env.context.get("install_mode")
|
||||
and channel.name == "root"
|
||||
and ("name" in values or "parent_id" in values)
|
||||
):
|
||||
raise exceptions.UserError(_("Cannot change the root channel"))
|
||||
return super().write(values)
|
||||
|
||||
def unlink(self):
|
||||
for channel in self:
|
||||
if channel.name == "root":
|
||||
raise exceptions.UserError(_("Cannot remove the root channel"))
|
||||
return super().unlink()
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for record in self:
|
||||
result.append((record.id, record.complete_name))
|
||||
return result
|
||||
@@ -1,273 +0,0 @@
|
||||
# Copyright 2013-2020 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
from odoo import _, api, exceptions, fields, models, tools
|
||||
|
||||
from ..fields import JobSerialized
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
regex_job_function_name = re.compile(r"^<([0-9a-z_\.]+)>\.([0-9a-zA-Z_]+)$")
|
||||
|
||||
|
||||
class QueueJobFunction(models.Model):
|
||||
_name = "queue.job.function"
|
||||
_description = "Job Functions"
|
||||
_log_access = False
|
||||
|
||||
JobConfig = namedtuple(
|
||||
"JobConfig",
|
||||
"channel "
|
||||
"retry_pattern "
|
||||
"related_action_enable "
|
||||
"related_action_func_name "
|
||||
"related_action_kwargs "
|
||||
"job_function_id ",
|
||||
)
|
||||
|
||||
def _default_channel(self):
|
||||
return self.env.ref("queue_job.channel_root")
|
||||
|
||||
name = fields.Char(
|
||||
compute="_compute_name",
|
||||
inverse="_inverse_name",
|
||||
index=True,
|
||||
store=True,
|
||||
)
|
||||
|
||||
# model and method should be required, but the required flag doesn't
|
||||
# let a chance to _inverse_name to be executed
|
||||
model_id = fields.Many2one(
|
||||
comodel_name="ir.model", string="Model", ondelete="cascade"
|
||||
)
|
||||
method = fields.Char()
|
||||
|
||||
channel_id = fields.Many2one(
|
||||
comodel_name="queue.job.channel",
|
||||
string="Channel",
|
||||
required=True,
|
||||
default=lambda r: r._default_channel(),
|
||||
)
|
||||
channel = fields.Char(related="channel_id.complete_name", store=True, readonly=True)
|
||||
retry_pattern = JobSerialized(string="Retry Pattern (serialized)", base_type=dict)
|
||||
edit_retry_pattern = fields.Text(
|
||||
string="Retry Pattern",
|
||||
compute="_compute_edit_retry_pattern",
|
||||
inverse="_inverse_edit_retry_pattern",
|
||||
help="Pattern expressing from the count of retries on retryable errors,"
|
||||
" the number of of seconds to postpone the next execution. Setting the "
|
||||
"number of seconds to a 2-element tuple or list will randomize the "
|
||||
"retry interval between the 2 values.\n"
|
||||
"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
|
||||
"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
|
||||
"See the module description for details.",
|
||||
)
|
||||
related_action = JobSerialized(string="Related Action (serialized)", base_type=dict)
|
||||
edit_related_action = fields.Text(
|
||||
string="Related Action",
|
||||
compute="_compute_edit_related_action",
|
||||
inverse="_inverse_edit_related_action",
|
||||
help="The action when the button *Related Action* is used on a job. "
|
||||
"The default action is to open the view of the record related "
|
||||
"to the job. Configured as a dictionary with optional keys: "
|
||||
"enable, func_name, kwargs.\n"
|
||||
"See the module description for details.",
|
||||
)
|
||||
|
||||
@api.depends("model_id.model", "method")
|
||||
def _compute_name(self):
|
||||
for record in self:
|
||||
if not (record.model_id and record.method):
|
||||
record.name = ""
|
||||
continue
|
||||
record.name = self.job_function_name(record.model_id.model, record.method)
|
||||
|
||||
def _inverse_name(self):
|
||||
groups = regex_job_function_name.match(self.name)
|
||||
if not groups:
|
||||
raise exceptions.UserError(_("Invalid job function: {}").format(self.name))
|
||||
model_name = groups[1]
|
||||
method = groups[2]
|
||||
model = (
|
||||
self.env["ir.model"].sudo().search([("model", "=", model_name)], limit=1)
|
||||
)
|
||||
if not model:
|
||||
raise exceptions.UserError(_("Model {} not found").format(model_name))
|
||||
self.model_id = model.id
|
||||
self.method = method
|
||||
|
||||
@api.depends("retry_pattern")
|
||||
def _compute_edit_retry_pattern(self):
|
||||
for record in self:
|
||||
retry_pattern = record._parse_retry_pattern()
|
||||
record.edit_retry_pattern = str(retry_pattern)
|
||||
|
||||
def _inverse_edit_retry_pattern(self):
|
||||
try:
|
||||
edited = (self.edit_retry_pattern or "").strip()
|
||||
if edited:
|
||||
self.retry_pattern = ast.literal_eval(edited)
|
||||
else:
|
||||
self.retry_pattern = {}
|
||||
except (ValueError, TypeError, SyntaxError) as ex:
|
||||
raise exceptions.UserError(
|
||||
self._retry_pattern_format_error_message()
|
||||
) from ex
|
||||
|
||||
@api.depends("related_action")
|
||||
def _compute_edit_related_action(self):
|
||||
for record in self:
|
||||
record.edit_related_action = str(record.related_action)
|
||||
|
||||
def _inverse_edit_related_action(self):
|
||||
try:
|
||||
edited = (self.edit_related_action or "").strip()
|
||||
if edited:
|
||||
self.related_action = ast.literal_eval(edited)
|
||||
else:
|
||||
self.related_action = {}
|
||||
except (ValueError, TypeError, SyntaxError) as ex:
|
||||
raise exceptions.UserError(
|
||||
self._related_action_format_error_message()
|
||||
) from ex
|
||||
|
||||
@staticmethod
|
||||
def job_function_name(model_name, method_name):
|
||||
return "<{}>.{}".format(model_name, method_name)
|
||||
|
||||
def job_default_config(self):
|
||||
return self.JobConfig(
|
||||
channel="root",
|
||||
retry_pattern={},
|
||||
related_action_enable=True,
|
||||
related_action_func_name=None,
|
||||
related_action_kwargs={},
|
||||
job_function_id=None,
|
||||
)
|
||||
|
||||
def _parse_retry_pattern(self):
|
||||
try:
|
||||
# as json can't have integers as keys and the field is stored
|
||||
# as json, convert back to int
|
||||
retry_pattern = {}
|
||||
for try_count, postpone_value in self.retry_pattern.items():
|
||||
if isinstance(postpone_value, int):
|
||||
retry_pattern[int(try_count)] = postpone_value
|
||||
else:
|
||||
retry_pattern[int(try_count)] = tuple(postpone_value)
|
||||
except ValueError:
|
||||
_logger.error(
|
||||
"Invalid retry pattern for job function %s,"
|
||||
" keys could not be parsed as integers, fallback"
|
||||
" to the default retry pattern.",
|
||||
self.name,
|
||||
)
|
||||
retry_pattern = {}
|
||||
return retry_pattern
|
||||
|
||||
@tools.ormcache("name")
|
||||
def job_config(self, name):
|
||||
config = self.search([("name", "=", name)], limit=1)
|
||||
if not config:
|
||||
return self.job_default_config()
|
||||
retry_pattern = config._parse_retry_pattern()
|
||||
return self.JobConfig(
|
||||
channel=config.channel,
|
||||
retry_pattern=retry_pattern,
|
||||
related_action_enable=config.related_action.get("enable", True),
|
||||
related_action_func_name=config.related_action.get("func_name"),
|
||||
related_action_kwargs=config.related_action.get("kwargs", {}),
|
||||
job_function_id=config.id,
|
||||
)
|
||||
|
||||
def _retry_pattern_format_error_message(self):
|
||||
return _(
|
||||
"Unexpected format of Retry Pattern for {}.\n"
|
||||
"Example of valid formats:\n"
|
||||
"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
|
||||
"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
|
||||
).format(self.name)
|
||||
|
||||
@api.constrains("retry_pattern")
|
||||
def _check_retry_pattern(self):
|
||||
for record in self:
|
||||
retry_pattern = record.retry_pattern
|
||||
if not retry_pattern:
|
||||
continue
|
||||
|
||||
all_values = list(retry_pattern) + list(retry_pattern.values())
|
||||
for value in all_values:
|
||||
try:
|
||||
self._retry_value_type_check(value)
|
||||
except ValueError as ex:
|
||||
raise exceptions.UserError(
|
||||
record._retry_pattern_format_error_message()
|
||||
) from ex
|
||||
|
||||
def _retry_value_type_check(self, value):
|
||||
if isinstance(value, (tuple, list)):
|
||||
if len(value) != 2:
|
||||
raise ValueError
|
||||
[self._retry_value_type_check(element) for element in value]
|
||||
return
|
||||
int(value)
|
||||
|
||||
def _related_action_format_error_message(self):
|
||||
return _(
|
||||
"Unexpected format of Related Action for {}.\n"
|
||||
"Example of valid format:\n"
|
||||
'{{"enable": True, "func_name": "related_action_foo",'
|
||||
' "kwargs" {{"limit": 10}}}}'
|
||||
).format(self.name)
|
||||
|
||||
@api.constrains("related_action")
|
||||
def _check_related_action(self):
|
||||
valid_keys = ("enable", "func_name", "kwargs")
|
||||
for record in self:
|
||||
related_action = record.related_action
|
||||
if not related_action:
|
||||
continue
|
||||
|
||||
if any(key not in valid_keys for key in related_action):
|
||||
raise exceptions.UserError(
|
||||
record._related_action_format_error_message()
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = self.browse()
|
||||
if self.env.context.get("install_mode"):
|
||||
# installing a module that creates a job function: rebinds the record
|
||||
# to an existing one (likely we already had the job function created by
|
||||
# the @job decorator previously)
|
||||
new_vals_list = []
|
||||
for vals in vals_list:
|
||||
name = vals.get("name")
|
||||
if name:
|
||||
existing = self.search([("name", "=", name)], limit=1)
|
||||
if existing:
|
||||
if not existing.get_metadata()[0].get("noupdate"):
|
||||
existing.write(vals)
|
||||
records |= existing
|
||||
continue
|
||||
new_vals_list.append(vals)
|
||||
vals_list = new_vals_list
|
||||
records |= super().create(vals_list)
|
||||
self.clear_caches()
|
||||
return records
|
||||
|
||||
def write(self, values):
|
||||
res = super().write(values)
|
||||
self.clear_caches()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
res = super().unlink()
|
||||
self.clear_caches()
|
||||
return res
|
||||
@@ -1,16 +0,0 @@
|
||||
# Copyright 2025 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class QueueJobLock(models.Model):
|
||||
_name = "queue.job.lock"
|
||||
_description = "Queue Job Lock"
|
||||
|
||||
queue_job_id = fields.Many2one(
|
||||
comodel_name="queue.job",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
)
|
||||
@@ -1,33 +0,0 @@
|
||||
# Copyright 2020 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def post_init_hook(cr, registry):
|
||||
# this is the trigger that sends notifications when jobs change
|
||||
logger.info("Create queue_job_notify trigger")
|
||||
cr.execute(
|
||||
"""
|
||||
DROP TRIGGER IF EXISTS queue_job_notify ON queue_job;
|
||||
CREATE OR REPLACE
|
||||
FUNCTION queue_job_notify() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
IF OLD.state != 'done' THEN
|
||||
PERFORM pg_notify('queue_job', OLD.uuid);
|
||||
END IF;
|
||||
ELSE
|
||||
PERFORM pg_notify('queue_job', NEW.uuid);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER queue_job_notify
|
||||
AFTER INSERT OR UPDATE OR DELETE
|
||||
ON queue_job
|
||||
FOR EACH ROW EXECUTE PROCEDURE queue_job_notify();
|
||||
"""
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
import logging
|
||||
|
||||
from odoo import http
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def post_load():
|
||||
_logger.info(
|
||||
"Apply Request._get_session_and_dbname monkey patch to capture db"
|
||||
" from request with multiple databases"
|
||||
)
|
||||
_get_session_and_dbname_orig = http.Request._get_session_and_dbname
|
||||
|
||||
def _get_session_and_dbname(self):
|
||||
session, dbname = _get_session_and_dbname_orig(self)
|
||||
if (
|
||||
not dbname
|
||||
and self.httprequest.path == "/queue_job/runjob"
|
||||
and self.httprequest.args.get("db")
|
||||
):
|
||||
dbname = self.httprequest.args["db"]
|
||||
return session, dbname
|
||||
|
||||
http.Request._get_session_and_dbname = _get_session_and_dbname
|
||||
@@ -1,50 +0,0 @@
|
||||
* Using environment variables and command line:
|
||||
|
||||
* Adjust environment variables (optional):
|
||||
|
||||
- ``ODOO_QUEUE_JOB_CHANNELS=root:4`` or any other channels configuration.
|
||||
The default is ``root:1``
|
||||
|
||||
- if ``xmlrpc_port`` is not set: ``ODOO_QUEUE_JOB_PORT=8069``
|
||||
|
||||
* Start Odoo with ``--load=web,queue_job``
|
||||
and ``--workers`` greater than 1. [1]_
|
||||
|
||||
* Keep in mind that the number of workers should be greater than the number of
|
||||
channels. ``queue_job`` will reuse normal Odoo workers to process jobs. It
|
||||
will not spawn its own workers.
|
||||
|
||||
* Using the Odoo configuration file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[options]
|
||||
(...)
|
||||
workers = 6
|
||||
server_wide_modules = web,queue_job
|
||||
|
||||
(...)
|
||||
[queue_job]
|
||||
channels = root:2
|
||||
|
||||
* Environment variables have priority over the configuration file.
|
||||
|
||||
* Confirm the runner is starting correctly by checking the odoo log file:
|
||||
|
||||
.. code-block::
|
||||
|
||||
...INFO...queue_job.jobrunner.runner: starting
|
||||
...INFO...queue_job.jobrunner.runner: initializing database connections
|
||||
...INFO...queue_job.jobrunner.runner: queue job runner ready for db <dbname>
|
||||
...INFO...queue_job.jobrunner.runner: database connections ready
|
||||
|
||||
* Create jobs (eg using ``base_import_async``) and observe they
|
||||
start immediately and in parallel.
|
||||
|
||||
* Tip: to enable debug logging for the queue job, use
|
||||
``--log-handler=odoo.addons.queue_job:DEBUG``
|
||||
|
||||
.. [1] It works with the threaded Odoo server too, although this way
|
||||
of running Odoo is obviously not for production purposes.
|
||||
|
||||
* Jobs that remain in `enqueued` or `started` state (because, for instance, their worker has been killed) will be automatically re-queued.
|
||||
@@ -1,12 +0,0 @@
|
||||
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
||||
* Stéphane Bidoul <stephane.bidoul@acsone.eu>
|
||||
* Matthieu Dietrich <matthieu.dietrich@camptocamp.com>
|
||||
* Jos De Graeve <Jos.DeGraeve@apertoso.be>
|
||||
* David Lefever <dl@taktik.be>
|
||||
* Laurent Mignon <laurent.mignon@acsone.eu>
|
||||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu>
|
||||
* Cédric Pigeon <cedric.pigeon@acsone.eu>
|
||||
* Tatiana Deribina <tatiana.deribina@avoin.systems>
|
||||
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
|
||||
* Eric Antones <eantones@nuobit.com>
|
||||
* Simone Orsi <simone.orsi@camptocamp.com>
|
||||
@@ -1,46 +0,0 @@
|
||||
This addon adds an integrated Job Queue to Odoo.
|
||||
|
||||
It allows to postpone method calls executed asynchronously.
|
||||
|
||||
Jobs are executed in the background by a ``Jobrunner``, in their own transaction.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
class MyModel(models.Model):
|
||||
_name = 'my.model'
|
||||
|
||||
def my_method(self, a, k=None):
|
||||
_logger.info('executed with a: %s and k: %s', a, k)
|
||||
|
||||
|
||||
class MyOtherModel(models.Model):
|
||||
_name = 'my.other.model'
|
||||
|
||||
def button_do_stuff(self):
|
||||
self.env['my.model'].with_delay().my_method('a', k=2)
|
||||
|
||||
|
||||
In the snippet of code above, when we call ``button_do_stuff``, a job **capturing
|
||||
the method and arguments** will be postponed. It will be executed as soon as the
|
||||
Jobrunner has a free bucket, which can be instantaneous if no other job is
|
||||
running.
|
||||
|
||||
|
||||
Features:
|
||||
|
||||
* Views for jobs, jobs are stored in PostgreSQL
|
||||
* Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL's NOTIFY
|
||||
* Channels: give a capacity for the root channel and its sub-channels and
|
||||
segregate jobs in them. Allow for instance to restrict heavy jobs to be
|
||||
executed one at a time while little ones are executed 4 at a times.
|
||||
* Retries: Ability to retry jobs by raising a type of exception
|
||||
* Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next tries,
|
||||
retry after 1 minutes, ...
|
||||
* Job properties: priorities, estimated time of arrival (ETA), custom
|
||||
description, number of retries
|
||||
* Related Actions: link an action on the job view, such as open the record
|
||||
concerned by the job
|
||||
@@ -1,18 +0,0 @@
|
||||
.. [ The change log. The goal of this file is to help readers
|
||||
understand changes between version. The primary audience is
|
||||
end users and integrators. Purely technical changes such as
|
||||
code refactoring must not be mentioned here.
|
||||
|
||||
This file may contain ONE level of section titles, underlined
|
||||
with the ~ (tilde) character. Other section markers are
|
||||
forbidden and will likely break the structure of the README.rst
|
||||
or other documents where this fragment is included. ]
|
||||
|
||||
Next
|
||||
~~~~
|
||||
|
||||
* [ADD] Run jobrunner as a worker process instead of a thread in the main
|
||||
process (when running with --workers > 0)
|
||||
* [REF] ``@job`` and ``@related_action`` deprecated, any method can be delayed,
|
||||
and configured using ``queue.job.function`` records
|
||||
* [MIGRATION] from 13.0 branched at rev. e24ff4b
|
||||
@@ -1 +0,0 @@
|
||||
Be sure to have the ``requests`` library.
|
||||
@@ -1,18 +0,0 @@
|
||||
* After creating a new database or installing ``queue_job`` on an
|
||||
existing database, Odoo must be restarted for the runner to detect it.
|
||||
|
||||
* When Odoo shuts down normally, it waits for running jobs to finish.
|
||||
However, when the Odoo server crashes or is otherwise force-stopped,
|
||||
running jobs are interrupted while the runner has no chance to know
|
||||
they have been aborted. In such situations, jobs may remain in
|
||||
``started`` or ``enqueued`` state after the Odoo server is halted.
|
||||
Since the runner has no way to know if they are actually running or
|
||||
not, and does not know for sure if it is safe to restart the jobs,
|
||||
it does not attempt to restart them automatically. Such stale jobs
|
||||
therefore fill the running queue and prevent other jobs to start.
|
||||
You must therefore requeue them manually, either from the Jobs view,
|
||||
or by running the following SQL statement *before starting Odoo*:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
update queue_job set state='pending' where state in ('started', 'enqueued')
|
||||
@@ -1,455 +0,0 @@
|
||||
To use this module, you need to:
|
||||
|
||||
#. Go to ``Job Queue`` menu
|
||||
|
||||
Developers
|
||||
~~~~~~~~~~
|
||||
|
||||
Delaying jobs
|
||||
-------------
|
||||
|
||||
The fast way to enqueue a job for a method is to use ``with_delay()`` on a record
|
||||
or model:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_done(self):
|
||||
self.with_delay().print_confirmation_document(self.state)
|
||||
self.write({"state": "done"})
|
||||
return True
|
||||
|
||||
Here, the method ``print_confirmation_document()`` will be executed asynchronously
|
||||
as a job. ``with_delay()`` can take several parameters to define more precisely how
|
||||
the job is executed (priority, ...).
|
||||
|
||||
All the arguments passed to the method being delayed are stored in the job and
|
||||
passed to the method when it is executed asynchronously, including ``self``, so
|
||||
the current record is maintained during the job execution (warning: the context
|
||||
is not kept).
|
||||
|
||||
Dependencies can be expressed between jobs. To start a graph of jobs, use ``delayable()``
|
||||
on a record or model. The following is the equivalent of ``with_delay()`` but using the
|
||||
long form:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_done(self):
|
||||
delayable = self.delayable()
|
||||
delayable.print_confirmation_document(self.state)
|
||||
delayable.delay()
|
||||
self.write({"state": "done"})
|
||||
return True
|
||||
|
||||
Methods of Delayable objects return itself, so it can be used as a builder pattern,
|
||||
which in some cases allow to build the jobs dynamically:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_generate_simple_with_delayable(self):
|
||||
self.ensure_one()
|
||||
# Introduction of a delayable object, using a builder pattern
|
||||
# allowing to chain jobs or set properties. The delay() method
|
||||
# on the delayable object actually stores the delayable objects
|
||||
# in the queue_job table
|
||||
(
|
||||
self.delayable()
|
||||
.generate_thumbnail((50, 50))
|
||||
.set(priority=30)
|
||||
.set(description=_("generate xxx"))
|
||||
.delay()
|
||||
)
|
||||
|
||||
The simplest way to define a dependency is to use ``.on_done(job)`` on a Delayable:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_chain_done(self):
|
||||
self.ensure_one()
|
||||
job1 = self.browse(1).delayable().generate_thumbnail((50, 50))
|
||||
job2 = self.browse(1).delayable().generate_thumbnail((50, 50))
|
||||
job3 = self.browse(1).delayable().generate_thumbnail((50, 50))
|
||||
# job 3 is executed when job 2 is done which is executed when job 1 is done
|
||||
job1.on_done(job2.on_done(job3)).delay()
|
||||
|
||||
Delayables can be chained to form more complex graphs using the ``chain()`` and
|
||||
``group()`` primitives.
|
||||
A chain represents a sequence of jobs to execute in order, a group represents
|
||||
jobs which can be executed in parallel. Using ``chain()`` has the same effect as
|
||||
using several nested ``on_done()`` but is more readable. Both can be combined to
|
||||
form a graph, for instance we can group [A] of jobs, which blocks another group
|
||||
[B] of jobs. When and only when all the jobs of the group [A] are executed, the
|
||||
jobs of the group [B] are executed. The code would look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from odoo.addons.queue_job.delay import group, chain
|
||||
|
||||
def button_done(self):
|
||||
group_a = group(self.delayable().method_foo(), self.delayable().method_bar())
|
||||
group_b = group(self.delayable().method_baz(1), self.delayable().method_baz(2))
|
||||
chain(group_a, group_b).delay()
|
||||
self.write({"state": "done"})
|
||||
return True
|
||||
|
||||
When a failure happens in a graph of jobs, the execution of the jobs that depend on the
|
||||
failed job stops. They remain in a state ``wait_dependencies`` until their "parent" job is
|
||||
successful. This can happen in two ways: either the parent job retries and is successful
|
||||
on a second try, either the parent job is manually "set to done" by a user. In these two
|
||||
cases, the dependency is resolved and the graph will continue to be processed. Alternatively,
|
||||
the failed job and all its dependent jobs can be canceled by a user. The other jobs of the
|
||||
graph that do not depend on the failed job continue their execution in any case.
|
||||
|
||||
Note: ``delay()`` must be called on the delayable, chain, or group which is at the top
|
||||
of the graph. In the example above, if it was called on ``group_a``, then ``group_b``
|
||||
would never be delayed (but a warning would be shown).
|
||||
|
||||
It is also possible to split a job into several jobs, each one processing a part of the
|
||||
work. This can be useful to avoid very long jobs, parallelize some task and get more specific
|
||||
errors. Usage is as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_split_delayable(self):
|
||||
(
|
||||
self # Can be a big recordset, let's say 1000 records
|
||||
.delayable()
|
||||
.generate_thumbnail((50, 50))
|
||||
.set(priority=30)
|
||||
.set(description=_("generate xxx"))
|
||||
.split(50) # Split the job in 20 jobs of 50 records each
|
||||
.delay()
|
||||
)
|
||||
|
||||
The ``split()`` method takes a ``chain`` boolean keyword argument. If set to
|
||||
True, the jobs will be chained, meaning that the next job will only start when the previous
|
||||
one is done:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def button_increment_var(self):
|
||||
(
|
||||
self
|
||||
.delayable()
|
||||
.increment_counter()
|
||||
.split(1, chain=True) # Will exceute the jobs one after the other
|
||||
.delay()
|
||||
)
|
||||
|
||||
|
||||
Enqueing Job Options
|
||||
--------------------
|
||||
|
||||
* priority: default is 10, the closest it is to 0, the faster it will be
|
||||
executed
|
||||
* eta: Estimated Time of Arrival of the job. It will not be executed before this
|
||||
date/time
|
||||
* max_retries: default is 5, maximum number of retries before giving up and set
|
||||
the job state to 'failed'. A value of 0 means infinite retries.
|
||||
* description: human description of the job. If not set, description is computed
|
||||
from the function doc or method name
|
||||
* channel: the complete name of the channel to use to process the function. If
|
||||
specified it overrides the one defined on the function
|
||||
* identity_key: key uniquely identifying the job, if specified and a job with
|
||||
the same key has not yet been run, the new job will not be created
|
||||
|
||||
Configure default options for jobs
|
||||
----------------------------------
|
||||
|
||||
In earlier versions, jobs could be configured using the ``@job`` decorator.
|
||||
This is now obsolete, they can be configured using optional ``queue.job.function``
|
||||
and ``queue.job.channel`` XML records.
|
||||
|
||||
Example of channel:
|
||||
|
||||
.. code-block:: XML
|
||||
|
||||
<record id="channel_sale" model="queue.job.channel">
|
||||
<field name="name">sale</field>
|
||||
<field name="parent_id" ref="queue_job.channel_root" />
|
||||
</record>
|
||||
|
||||
Example of job function:
|
||||
|
||||
.. code-block:: XML
|
||||
|
||||
<record id="job_function_sale_order_action_done" model="queue.job.function">
|
||||
<field name="model_id" ref="sale.model_sale_order" />
|
||||
<field name="method">action_done</field>
|
||||
<field name="channel_id" ref="channel_sale" />
|
||||
<field name="related_action" eval='{"func_name": "custom_related_action"}' />
|
||||
<field name="retry_pattern" eval="{1: 60, 2: 180, 3: 10, 5: 300}" />
|
||||
</record>
|
||||
|
||||
The general form for the ``name`` is: ``<model.name>.method``.
|
||||
|
||||
The channel, related action and retry pattern options are optional, they are
|
||||
documented below.
|
||||
|
||||
When writing modules, if 2+ modules add a job function or channel with the same
|
||||
name (and parent for channels), they'll be merged in the same record, even if
|
||||
they have different xmlids. On uninstall, the merged record is deleted when all
|
||||
the modules using it are uninstalled.
|
||||
|
||||
|
||||
**Job function: model**
|
||||
|
||||
If the function is defined in an abstract model, you can not write
|
||||
``<field name="model_id" ref="xml_id_of_the_abstract_model"</field>``
|
||||
but you have to define a function for each model that inherits from the abstract model.
|
||||
|
||||
|
||||
**Job function: channel**
|
||||
|
||||
The channel where the job will be delayed. The default channel is ``root``.
|
||||
|
||||
**Job function: related action**
|
||||
|
||||
The *Related Action* appears as a button on the Job's view.
|
||||
The button will execute the defined action.
|
||||
|
||||
The default one is to open the view of the record related to the job (form view
|
||||
when there is a single record, list view for several records).
|
||||
In many cases, the default related action is enough and doesn't need
|
||||
customization, but it can be customized by providing a dictionary on the job
|
||||
function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"enable": False,
|
||||
"func_name": "related_action_partner",
|
||||
"kwargs": {"name": "Partner"},
|
||||
}
|
||||
|
||||
* ``enable``: when ``False``, the button has no effect (default: ``True``)
|
||||
* ``func_name``: name of the method on ``queue.job`` that returns an action
|
||||
* ``kwargs``: extra arguments to pass to the related action method
|
||||
|
||||
Example of related action code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class QueueJob(models.Model):
|
||||
_inherit = 'queue.job'
|
||||
|
||||
def related_action_partner(self, name):
|
||||
self.ensure_one()
|
||||
model = self.model_name
|
||||
partner = self.records
|
||||
action = {
|
||||
'name': name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': model,
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_id': partner.id,
|
||||
}
|
||||
return action
|
||||
|
||||
|
||||
**Job function: retry pattern**
|
||||
|
||||
When a job fails with a retryable error type, it is automatically
|
||||
retried later. By default, the retry is always 10 minutes later.
|
||||
|
||||
A retry pattern can be configured on the job function. What a pattern represents
|
||||
is "from X tries, postpone to Y seconds". It is expressed as a dictionary where
|
||||
keys are tries and values are seconds to postpone as integers:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
1: 10,
|
||||
5: 20,
|
||||
10: 30,
|
||||
15: 300,
|
||||
}
|
||||
|
||||
Based on this configuration, we can tell that:
|
||||
|
||||
* 5 first retries are postponed 10 seconds later
|
||||
* retries 5 to 10 postponed 20 seconds later
|
||||
* retries 10 to 15 postponed 30 seconds later
|
||||
* all subsequent retries postponed 5 minutes later
|
||||
|
||||
**Job Context**
|
||||
|
||||
The context of the recordset of the job, or any recordset passed in arguments of
|
||||
a job, is transferred to the job according to an allow-list.
|
||||
|
||||
The default allow-list is `("tz", "lang", "allowed_company_ids", "force_company", "active_test")`. It can
|
||||
be customized in ``Base._job_prepare_context_before_enqueue_keys``.
|
||||
**Bypass jobs on running Odoo**
|
||||
|
||||
When you are developing (ie: connector modules) you might want
|
||||
to bypass the queue job and run your code immediately.
|
||||
|
||||
To do so you can set `QUEUE_JOB__NO_DELAY=1` in your environment.
|
||||
|
||||
**Bypass jobs in tests**
|
||||
|
||||
When writing tests on job-related methods is always tricky to deal with
|
||||
delayed recordsets. To make your testing life easier
|
||||
you can set `queue_job__no_delay=True` in the context.
|
||||
|
||||
Tip: you can do this at test case level like this
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context=dict(
|
||||
cls.env.context,
|
||||
queue_job__no_delay=True, # no jobs thanks
|
||||
))
|
||||
|
||||
Then all your tests execute the job methods synchronously
|
||||
without delaying any jobs.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
**Asserting enqueued jobs**
|
||||
|
||||
The recommended way to test jobs, rather than running them directly and synchronously is to
|
||||
split the tests in two parts:
|
||||
|
||||
* one test where the job is mocked (trap jobs with ``trap_jobs()`` and the test
|
||||
only verifies that the job has been delayed with the expected arguments
|
||||
* one test that only calls the method of the job synchronously, to validate the
|
||||
proper behavior of this method only
|
||||
|
||||
Proceeding this way means that you can prove that jobs will be enqueued properly
|
||||
at runtime, and it ensures your code does not have a different behavior in tests
|
||||
and in production (because running your jobs synchronously may have a different
|
||||
behavior as they are in the same transaction / in the middle of the method).
|
||||
Additionally, it gives more control on the arguments you want to pass when
|
||||
calling the job's method (synchronously, this time, in the second type of
|
||||
tests), and it makes tests smaller.
|
||||
|
||||
The best way to run such assertions on the enqueued jobs is to use
|
||||
``odoo.addons.queue_job.tests.common.trap_jobs()``.
|
||||
|
||||
Inside this context manager, instead of being added in the database's queue,
|
||||
jobs are pushed in an in-memory list. The context manager then provides useful
|
||||
helpers to verify that jobs have been enqueued with the expected arguments. It
|
||||
even can run the jobs of its list synchronously! Details in
|
||||
``odoo.addons.queue_job.tests.common.JobsTester``.
|
||||
|
||||
A very small example (more details in ``tests/common.py``):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# code
|
||||
def my_job_method(self, name, count):
|
||||
self.write({"name": " ".join([name] * count)
|
||||
|
||||
def method_to_test(self):
|
||||
count = self.env["other.model"].search_count([])
|
||||
self.with_delay(priority=15).my_job_method("Hi!", count=count)
|
||||
return count
|
||||
|
||||
# tests
|
||||
from odoo.addons.queue_job.tests.common import trap_jobs
|
||||
|
||||
# first test only check the expected behavior of the method and the proper
|
||||
# enqueuing of jobs
|
||||
def test_method_to_test(self):
|
||||
with trap_jobs() as trap:
|
||||
result = self.env["model"].method_to_test()
|
||||
expected_count = 12
|
||||
|
||||
trap.assert_jobs_count(1, only=self.env["model"].my_job_method)
|
||||
trap.assert_enqueued_job(
|
||||
self.env["model"].my_job_method,
|
||||
args=("Hi!",),
|
||||
kwargs=dict(count=expected_count),
|
||||
properties=dict(priority=15)
|
||||
)
|
||||
self.assertEqual(result, expected_count)
|
||||
|
||||
|
||||
# second test to validate the behavior of the job unitarily
|
||||
def test_my_job_method(self):
|
||||
record = self.env["model"].browse(1)
|
||||
record.my_job_method("Hi!", count=12)
|
||||
self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!")
|
||||
|
||||
If you prefer, you can still test the whole thing in a single test, by calling
|
||||
``jobs_tester.perform_enqueued_jobs()`` in your test.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_method_to_test(self):
|
||||
with trap_jobs() as trap:
|
||||
result = self.env["model"].method_to_test()
|
||||
expected_count = 12
|
||||
|
||||
trap.assert_jobs_count(1, only=self.env["model"].my_job_method)
|
||||
trap.assert_enqueued_job(
|
||||
self.env["model"].my_job_method,
|
||||
args=("Hi!",),
|
||||
kwargs=dict(count=expected_count),
|
||||
properties=dict(priority=15)
|
||||
)
|
||||
self.assertEqual(result, expected_count)
|
||||
|
||||
trap.perform_enqueued_jobs()
|
||||
|
||||
record = self.env["model"].browse(1)
|
||||
record.my_job_method("Hi!", count=12)
|
||||
self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!")
|
||||
|
||||
**Execute jobs synchronously when running Odoo**
|
||||
|
||||
When you are developing (ie: connector modules) you might want
|
||||
to bypass the queue job and run your code immediately.
|
||||
|
||||
To do so you can set ``QUEUE_JOB__NO_DELAY=1`` in your environment.
|
||||
|
||||
.. WARNING:: Do not do this in production
|
||||
|
||||
**Execute jobs synchronously in tests**
|
||||
|
||||
You should use ``trap_jobs``, really, but if for any reason you could not use it,
|
||||
and still need to have job methods executed synchronously in your tests, you can
|
||||
do so by setting ``queue_job__no_delay=True`` in the context.
|
||||
|
||||
Tip: you can do this at test case level like this
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context=dict(
|
||||
cls.env.context,
|
||||
queue_job__no_delay=True, # no jobs thanks
|
||||
))
|
||||
|
||||
Then all your tests execute the job methods synchronously without delaying any
|
||||
jobs.
|
||||
|
||||
In tests you'll have to mute the logger like:
|
||||
|
||||
@mute_logger('odoo.addons.queue_job.models.base')
|
||||
|
||||
.. NOTE:: in graphs of jobs, the ``queue_job__no_delay`` context key must be in at
|
||||
least one job's env of the graph for the whole graph to be executed synchronously
|
||||
|
||||
|
||||
Tips and tricks
|
||||
---------------
|
||||
|
||||
* **Idempotency** (https://www.restapitutorial.com/lessons/idempotency.html): The queue_job should be idempotent so they can be retried several times without impact on the data.
|
||||
* **The job should test at the very beginning its relevance**: the moment the job will be executed is unknown by design. So the first task of a job should be to check if the related work is still relevant at the moment of the execution.
|
||||
|
||||
Patterns
|
||||
--------
|
||||
Through the time, two main patterns emerged:
|
||||
|
||||
1. For data exposed to users, a model should store the data and the model should be the creator of the job. The job is kept hidden from the users
|
||||
2. For technical data, that are not exposed to the users, it is generally alright to create directly jobs with data passed as arguments to the job, without intermediary models.
|
||||
@@ -1,8 +0,0 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_queue_job_manager,queue job manager,queue_job.model_queue_job,queue_job.group_queue_job_manager,1,1,1,1
|
||||
access_queue_job_lock_manager,queue job lock manager,queue_job.model_queue_job_lock,queue_job.group_queue_job_manager,1,0,0,0
|
||||
access_queue_job_function_manager,queue job functions manager,queue_job.model_queue_job_function,queue_job.group_queue_job_manager,1,1,1,1
|
||||
access_queue_job_channel_manager,queue job channel manager,queue_job.model_queue_job_channel,queue_job.group_queue_job_manager,1,1,1,1
|
||||
access_queue_requeue_job,queue requeue job manager,queue_job.model_queue_requeue_job,queue_job.group_queue_job_manager,1,1,1,1
|
||||
access_queue_jobs_to_done,queue jobs to done manager,queue_job.model_queue_jobs_to_done,queue_job.group_queue_job_manager,1,1,1,1
|
||||
access_queue_jobs_to_cancelled,queue jobs to cancelled manager,queue_job.model_queue_jobs_to_cancelled,queue_job.group_queue_job_manager,1,1,1,1
|
||||
|
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.module.category" id="module_category_queue_job">
|
||||
<field name="name">Job Queue</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
<record id="group_queue_job_manager" model="res.groups">
|
||||
<field name="name">Job Queue Manager</field>
|
||||
<field name="category_id" ref="module_category_queue_job" />
|
||||
<field
|
||||
name="users"
|
||||
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
|
||||
/>
|
||||
</record>
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record id="queue_job_comp_rule" model="ir.rule">
|
||||
<field name="name">Job Queue multi-company</field>
|
||||
<field name="model_id" ref="model_queue_job" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0,0,70,70"><!-- Generated with https://ivantodorovich.github.io/odoo-icon --><defs><linearGradient x1="70" y1="0" x2="0" y2="70" gradientUnits="userSpaceOnUse" id="color-1"><stop offset="0" stop-color="#ff6c68"/><stop offset="1" stop-color="#e53935"/></linearGradient></defs><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M3.5,70c-1.933,0 -3.5,-1.567 -3.5,-3.5v-63c0,-1.933 1.567,-3.5 3.5,-3.5h63c1.933,0 3.5,1.567 3.5,3.5v63c0,1.933 -1.567,3.5 -3.5,3.5z" id="box" fill="url(#color-1)"/><path d="M65,1h-61c-1.95033,0 -3.2667,0.63396 -3.9491,1.90189c0.28378,-1.64806 1.72001,-2.90189 3.4491,-2.90189h63c1.72965,0 3.16627,1.25466 3.44938,2.90352c-0.69803,-1.26902 -2.34782,-1.90352 -4.94938,-1.90352z" id="topBoxShadow" fill="#ffffff" opacity="0.383"/><path d="M4,69h61c2.66667,0 4.33333,-1 5,-3v0.5c0,1.933 -1.567,3.5 -3.5,3.5h-63c-1.933,0 -3.5,-1.567 -3.5,-3.5c0,-0.1611 0,-0.32777 0,-0.5c0.66667,2 2,3 4,3z" id="bottomBoxShadow" fill="#000000" opacity="0.383"/><path d="M12,16.55545h10.22243v10.22243h-10.22243v-10.22243M27.33333,19.1106v5.11091h30.66667v-5.11091h-30.66667M12,31.88879h10.22243v10.22243h-10.22243v-10.22243M27.33333,34.44393v5.11091h30.66667v-5.11091h-30.66667M12,47.22212h10.22243v10.22243h-10.22243v-10.22243M27.33333,49.77727v5.11091h30.66667v-5.11091z" id="shadow" fill="#000000" opacity="0.3"/><path d="M22.22243,14.55545l0,7.66605l5.11091,-5.11091h30.66667v5.11091l-10.22243,10.22243h10.22243v5.11091l-10.22243,10.22243h10.22243v5.11091l-17.11183,17.11183h-37.38817c-1.933,0 -3.5,-1.567 -3.5,-3.5l0,-39.94455l12,-12z" id="flatShadow" fill="#000000" opacity="0.324"/><path d="M12,14.55545h10.22243v10.22243h-10.22243v-10.22243M27.33333,17.1106v5.11091h30.66667v-5.11091h-30.66667M12,29.88879h10.22243v10.22243h-10.22243v-10.22243M27.33333,32.44393v5.11091h30.66667v-5.11091h-30.66667M12,45.22212h10.22243v10.22243h-10.22243v-10.22243M27.33333,47.77727v5.11091h30.66667v-5.11091z" id="icon" fill="#ffffff"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,998 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>README.rst</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
|
||||
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
|
||||
</a>
|
||||
<div class="section" id="job-queue">
|
||||
<h1>Job Queue</h1>
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:b92d06dbbf161572f2bf02e0c6a59282cea11cc5e903378094bead986f0125de
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Mature" src="https://img.shields.io/badge/maturity-Mature-brightgreen.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/queue/tree/16.0/queue_job"><img alt="OCA/queue" src="https://img.shields.io/badge/github-OCA%2Fqueue-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/queue-16-0/queue-16-0-queue_job"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>This addon adds an integrated Job Queue to Odoo.</p>
|
||||
<p>It allows to postpone method calls executed asynchronously.</p>
|
||||
<p>Jobs are executed in the background by a <tt class="docutils literal">Jobrunner</tt>, in their own transaction.</p>
|
||||
<p>Example:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">odoo</span><span class="w"> </span><span class="kn">import</span> <span class="n">models</span><span class="p">,</span> <span class="n">fields</span><span class="p">,</span> <span class="n">api</span><span class="w">
|
||||
|
||||
</span><span class="k">class</span><span class="w"> </span><span class="nc">MyModel</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s1">'my.model'</span><span class="w">
|
||||
|
||||
</span> <span class="k">def</span><span class="w"> </span><span class="nf">my_method</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">a</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="n">_logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s1">'executed with a: </span><span class="si">%s</span><span class="s1"> and k: </span><span class="si">%s</span><span class="s1">'</span><span class="p">,</span> <span class="n">a</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span><span class="w">
|
||||
|
||||
|
||||
</span><span class="k">class</span><span class="w"> </span><span class="nc">MyOtherModel</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s1">'my.other.model'</span><span class="w">
|
||||
|
||||
</span> <span class="k">def</span><span class="w"> </span><span class="nf">button_do_stuff</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s1">'my.model'</span><span class="p">]</span><span class="o">.</span><span class="n">with_delay</span><span class="p">()</span><span class="o">.</span><span class="n">my_method</span><span class="p">(</span><span class="s1">'a'</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
|
||||
</pre>
|
||||
<p>In the snippet of code above, when we call <tt class="docutils literal">button_do_stuff</tt>, a job <strong>capturing
|
||||
the method and arguments</strong> will be postponed. It will be executed as soon as the
|
||||
Jobrunner has a free bucket, which can be instantaneous if no other job is
|
||||
running.</p>
|
||||
<p>Features:</p>
|
||||
<ul class="simple">
|
||||
<li>Views for jobs, jobs are stored in PostgreSQL</li>
|
||||
<li>Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL’s NOTIFY</li>
|
||||
<li>Channels: give a capacity for the root channel and its sub-channels and
|
||||
segregate jobs in them. Allow for instance to restrict heavy jobs to be
|
||||
executed one at a time while little ones are executed 4 at a times.</li>
|
||||
<li>Retries: Ability to retry jobs by raising a type of exception</li>
|
||||
<li>Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next tries,
|
||||
retry after 1 minutes, …</li>
|
||||
<li>Job properties: priorities, estimated time of arrival (ETA), custom
|
||||
description, number of retries</li>
|
||||
<li>Related Actions: link an action on the job view, such as open the record
|
||||
concerned by the job</li>
|
||||
</ul>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#installation" id="toc-entry-1">Installation</a></li>
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-3">Usage</a><ul>
|
||||
<li><a class="reference internal" href="#developers" id="toc-entry-4">Developers</a><ul>
|
||||
<li><a class="reference internal" href="#delaying-jobs" id="toc-entry-5">Delaying jobs</a></li>
|
||||
<li><a class="reference internal" href="#enqueing-job-options" id="toc-entry-6">Enqueing Job Options</a></li>
|
||||
<li><a class="reference internal" href="#configure-default-options-for-jobs" id="toc-entry-7">Configure default options for jobs</a></li>
|
||||
<li><a class="reference internal" href="#testing" id="toc-entry-8">Testing</a></li>
|
||||
<li><a class="reference internal" href="#tips-and-tricks" id="toc-entry-9">Tips and tricks</a></li>
|
||||
<li><a class="reference internal" href="#patterns" id="toc-entry-10">Patterns</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-11">Known issues / Roadmap</a></li>
|
||||
<li><a class="reference internal" href="#changelog" id="toc-entry-12">Changelog</a><ul>
|
||||
<li><a class="reference internal" href="#next" id="toc-entry-13">Next</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-14">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-15">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-16">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-17">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-18">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="installation">
|
||||
<h2><a class="toc-backref" href="#toc-entry-1">Installation</a></h2>
|
||||
<p>Be sure to have the <tt class="docutils literal">requests</tt> library.</p>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h2><a class="toc-backref" href="#toc-entry-2">Configuration</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Using environment variables and command line:<ul>
|
||||
<li>Adjust environment variables (optional):<ul>
|
||||
<li><tt class="docutils literal">ODOO_QUEUE_JOB_CHANNELS=root:4</tt> or any other channels configuration.
|
||||
The default is <tt class="docutils literal">root:1</tt></li>
|
||||
<li>if <tt class="docutils literal">xmlrpc_port</tt> is not set: <tt class="docutils literal">ODOO_QUEUE_JOB_PORT=8069</tt></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Start Odoo with <tt class="docutils literal"><span class="pre">--load=web,queue_job</span></tt>
|
||||
and <tt class="docutils literal"><span class="pre">--workers</span></tt> greater than 1. <a class="footnote-reference" href="#footnote-1" id="footnote-reference-1">[1]</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Keep in mind that the number of workers should be greater than the number of
|
||||
channels. <tt class="docutils literal">queue_job</tt> will reuse normal Odoo workers to process jobs. It
|
||||
will not spawn its own workers.</li>
|
||||
<li>Using the Odoo configuration file:</li>
|
||||
</ul>
|
||||
<pre class="code ini literal-block">
|
||||
<span class="k">[options]</span><span class="w">
|
||||
</span><span class="na">(...)</span><span class="w">
|
||||
</span><span class="na">workers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">6</span><span class="w">
|
||||
</span><span class="na">server_wide_modules</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">web,queue_job</span><span class="w">
|
||||
|
||||
</span><span class="na">(...)</span><span class="w">
|
||||
</span><span class="k">[queue_job]</span><span class="w">
|
||||
</span><span class="na">channels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">root:2</span>
|
||||
</pre>
|
||||
<ul class="simple">
|
||||
<li>Environment variables have priority over the configuration file.</li>
|
||||
<li>Confirm the runner is starting correctly by checking the odoo log file:</li>
|
||||
</ul>
|
||||
<pre class="code literal-block">
|
||||
...INFO...queue_job.jobrunner.runner: starting
|
||||
...INFO...queue_job.jobrunner.runner: initializing database connections
|
||||
...INFO...queue_job.jobrunner.runner: queue job runner ready for db <dbname>
|
||||
...INFO...queue_job.jobrunner.runner: database connections ready
|
||||
</pre>
|
||||
<ul class="simple">
|
||||
<li>Create jobs (eg using <tt class="docutils literal">base_import_async</tt>) and observe they
|
||||
start immediately and in parallel.</li>
|
||||
<li>Tip: to enable debug logging for the queue job, use
|
||||
<tt class="docutils literal"><span class="pre">--log-handler=odoo.addons.queue_job:DEBUG</span></tt></li>
|
||||
</ul>
|
||||
<table class="docutils footnote" frame="void" id="footnote-1" rules="none">
|
||||
<colgroup><col class="label" /><col /></colgroup>
|
||||
<tbody valign="top">
|
||||
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-1">[1]</a></td><td>It works with the threaded Odoo server too, although this way
|
||||
of running Odoo is obviously not for production purposes.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ul class="simple">
|
||||
<li>Jobs that remain in <cite>enqueued</cite> or <cite>started</cite> state (because, for instance, their worker has been killed) will be automatically re-queued.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h2><a class="toc-backref" href="#toc-entry-3">Usage</a></h2>
|
||||
<p>To use this module, you need to:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>Go to <tt class="docutils literal">Job Queue</tt> menu</li>
|
||||
</ol>
|
||||
<div class="section" id="developers">
|
||||
<h3><a class="toc-backref" href="#toc-entry-4">Developers</a></h3>
|
||||
<div class="section" id="delaying-jobs">
|
||||
<h4><a class="toc-backref" href="#toc-entry-5">Delaying jobs</a></h4>
|
||||
<p>The fast way to enqueue a job for a method is to use <tt class="docutils literal">with_delay()</tt> on a record
|
||||
or model:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">button_done</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">with_delay</span><span class="p">()</span><span class="o">.</span><span class="n">print_confirmation_document</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">write</span><span class="p">({</span><span class="s2">"state"</span><span class="p">:</span> <span class="s2">"done"</span><span class="p">})</span><span class="w">
|
||||
</span> <span class="k">return</span> <span class="kc">True</span>
|
||||
</pre>
|
||||
<p>Here, the method <tt class="docutils literal">print_confirmation_document()</tt> will be executed asynchronously
|
||||
as a job. <tt class="docutils literal">with_delay()</tt> can take several parameters to define more precisely how
|
||||
the job is executed (priority, …).</p>
|
||||
<p>All the arguments passed to the method being delayed are stored in the job and
|
||||
passed to the method when it is executed asynchronously, including <tt class="docutils literal">self</tt>, so
|
||||
the current record is maintained during the job execution (warning: the context
|
||||
is not kept).</p>
|
||||
<p>Dependencies can be expressed between jobs. To start a graph of jobs, use <tt class="docutils literal">delayable()</tt>
|
||||
on a record or model. The following is the equivalent of <tt class="docutils literal">with_delay()</tt> but using the
|
||||
long form:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">button_done</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="n">delayable</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="n">delayable</span><span class="o">.</span><span class="n">print_confirmation_document</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="n">delayable</span><span class="o">.</span><span class="n">delay</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">write</span><span class="p">({</span><span class="s2">"state"</span><span class="p">:</span> <span class="s2">"done"</span><span class="p">})</span><span class="w">
|
||||
</span> <span class="k">return</span> <span class="kc">True</span>
|
||||
</pre>
|
||||
<p>Methods of Delayable objects return itself, so it can be used as a builder pattern,
|
||||
which in some cases allow to build the jobs dynamically:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">button_generate_simple_with_delayable</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">ensure_one</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="c1"># Introduction of a delayable object, using a builder pattern</span><span class="w">
|
||||
</span> <span class="c1"># allowing to chain jobs or set properties. The delay() method</span><span class="w">
|
||||
</span> <span class="c1"># on the delayable object actually stores the delayable objects</span><span class="w">
|
||||
</span> <span class="c1"># in the queue_job table</span><span class="w">
|
||||
</span> <span class="p">(</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">generate_thumbnail</span><span class="p">((</span><span class="mi">50</span><span class="p">,</span> <span class="mi">50</span><span class="p">))</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">priority</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="n">_</span><span class="p">(</span><span class="s2">"generate xxx"</span><span class="p">))</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">delay</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="p">)</span>
|
||||
</pre>
|
||||
<p>The simplest way to define a dependency is to use <tt class="docutils literal">.on_done(job)</tt> on a Delayable:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">button_chain_done</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">ensure_one</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="n">job1</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">browse</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="o">.</span><span class="n">generate_thumbnail</span><span class="p">((</span><span class="mi">50</span><span class="p">,</span> <span class="mi">50</span><span class="p">))</span><span class="w">
|
||||
</span> <span class="n">job2</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">browse</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="o">.</span><span class="n">generate_thumbnail</span><span class="p">((</span><span class="mi">50</span><span class="p">,</span> <span class="mi">50</span><span class="p">))</span><span class="w">
|
||||
</span> <span class="n">job3</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">browse</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="o">.</span><span class="n">generate_thumbnail</span><span class="p">((</span><span class="mi">50</span><span class="p">,</span> <span class="mi">50</span><span class="p">))</span><span class="w">
|
||||
</span> <span class="c1"># job 3 is executed when job 2 is done which is executed when job 1 is done</span><span class="w">
|
||||
</span> <span class="n">job1</span><span class="o">.</span><span class="n">on_done</span><span class="p">(</span><span class="n">job2</span><span class="o">.</span><span class="n">on_done</span><span class="p">(</span><span class="n">job3</span><span class="p">))</span><span class="o">.</span><span class="n">delay</span><span class="p">()</span>
|
||||
</pre>
|
||||
<p>Delayables can be chained to form more complex graphs using the <tt class="docutils literal">chain()</tt> and
|
||||
<tt class="docutils literal">group()</tt> primitives.
|
||||
A chain represents a sequence of jobs to execute in order, a group represents
|
||||
jobs which can be executed in parallel. Using <tt class="docutils literal">chain()</tt> has the same effect as
|
||||
using several nested <tt class="docutils literal">on_done()</tt> but is more readable. Both can be combined to
|
||||
form a graph, for instance we can group [A] of jobs, which blocks another group
|
||||
[B] of jobs. When and only when all the jobs of the group [A] are executed, the
|
||||
jobs of the group [B] are executed. The code would look like:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.queue_job.delay</span><span class="w"> </span><span class="kn">import</span> <span class="n">group</span><span class="p">,</span> <span class="n">chain</span><span class="w">
|
||||
|
||||
</span><span class="k">def</span><span class="w"> </span><span class="nf">button_done</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="n">group_a</span> <span class="o">=</span> <span class="n">group</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="o">.</span><span class="n">method_foo</span><span class="p">(),</span> <span class="bp">self</span><span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="o">.</span><span class="n">method_bar</span><span class="p">())</span><span class="w">
|
||||
</span> <span class="n">group_b</span> <span class="o">=</span> <span class="n">group</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="o">.</span><span class="n">method_baz</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="bp">self</span><span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="o">.</span><span class="n">method_baz</span><span class="p">(</span><span class="mi">2</span><span class="p">))</span><span class="w">
|
||||
</span> <span class="n">chain</span><span class="p">(</span><span class="n">group_a</span><span class="p">,</span> <span class="n">group_b</span><span class="p">)</span><span class="o">.</span><span class="n">delay</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">write</span><span class="p">({</span><span class="s2">"state"</span><span class="p">:</span> <span class="s2">"done"</span><span class="p">})</span><span class="w">
|
||||
</span> <span class="k">return</span> <span class="kc">True</span>
|
||||
</pre>
|
||||
<p>When a failure happens in a graph of jobs, the execution of the jobs that depend on the
|
||||
failed job stops. They remain in a state <tt class="docutils literal">wait_dependencies</tt> until their “parent” job is
|
||||
successful. This can happen in two ways: either the parent job retries and is successful
|
||||
on a second try, either the parent job is manually “set to done” by a user. In these two
|
||||
cases, the dependency is resolved and the graph will continue to be processed. Alternatively,
|
||||
the failed job and all its dependent jobs can be canceled by a user. The other jobs of the
|
||||
graph that do not depend on the failed job continue their execution in any case.</p>
|
||||
<p>Note: <tt class="docutils literal">delay()</tt> must be called on the delayable, chain, or group which is at the top
|
||||
of the graph. In the example above, if it was called on <tt class="docutils literal">group_a</tt>, then <tt class="docutils literal">group_b</tt>
|
||||
would never be delayed (but a warning would be shown).</p>
|
||||
<p>It is also possible to split a job into several jobs, each one processing a part of the
|
||||
work. This can be useful to avoid very long jobs, parallelize some task and get more specific
|
||||
errors. Usage is as follows:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">button_split_delayable</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="p">(</span><span class="w">
|
||||
</span> <span class="bp">self</span> <span class="c1"># Can be a big recordset, let's say 1000 records</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">generate_thumbnail</span><span class="p">((</span><span class="mi">50</span><span class="p">,</span> <span class="mi">50</span><span class="p">))</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">priority</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="n">_</span><span class="p">(</span><span class="s2">"generate xxx"</span><span class="p">))</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span> <span class="c1"># Split the job in 20 jobs of 50 records each</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">delay</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="p">)</span>
|
||||
</pre>
|
||||
<p>The <tt class="docutils literal">split()</tt> method takes a <tt class="docutils literal">chain</tt> boolean keyword argument. If set to
|
||||
True, the jobs will be chained, meaning that the next job will only start when the previous
|
||||
one is done:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">button_increment_var</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="p">(</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">delayable</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">increment_counter</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">chain</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> <span class="c1"># Will exceute the jobs one after the other</span><span class="w">
|
||||
</span> <span class="o">.</span><span class="n">delay</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="p">)</span>
|
||||
</pre>
|
||||
</div>
|
||||
<div class="section" id="enqueing-job-options">
|
||||
<h4><a class="toc-backref" href="#toc-entry-6">Enqueing Job Options</a></h4>
|
||||
<ul class="simple">
|
||||
<li>priority: default is 10, the closest it is to 0, the faster it will be
|
||||
executed</li>
|
||||
<li>eta: Estimated Time of Arrival of the job. It will not be executed before this
|
||||
date/time</li>
|
||||
<li>max_retries: default is 5, maximum number of retries before giving up and set
|
||||
the job state to ‘failed’. A value of 0 means infinite retries.</li>
|
||||
<li>description: human description of the job. If not set, description is computed
|
||||
from the function doc or method name</li>
|
||||
<li>channel: the complete name of the channel to use to process the function. If
|
||||
specified it overrides the one defined on the function</li>
|
||||
<li>identity_key: key uniquely identifying the job, if specified and a job with
|
||||
the same key has not yet been run, the new job will not be created</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configure-default-options-for-jobs">
|
||||
<h4><a class="toc-backref" href="#toc-entry-7">Configure default options for jobs</a></h4>
|
||||
<p>In earlier versions, jobs could be configured using the <tt class="docutils literal">@job</tt> decorator.
|
||||
This is now obsolete, they can be configured using optional <tt class="docutils literal">queue.job.function</tt>
|
||||
and <tt class="docutils literal">queue.job.channel</tt> XML records.</p>
|
||||
<p>Example of channel:</p>
|
||||
<pre class="code XML literal-block">
|
||||
<span class="nt"><record</span><span class="w"> </span><span class="na">id=</span><span class="s">"channel_sale"</span><span class="w"> </span><span class="na">model=</span><span class="s">"queue.job.channel"</span><span class="nt">></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"name"</span><span class="nt">></span>sale<span class="nt"></field></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"parent_id"</span><span class="w"> </span><span class="na">ref=</span><span class="s">"queue_job.channel_root"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"></record></span>
|
||||
</pre>
|
||||
<p>Example of job function:</p>
|
||||
<pre class="code XML literal-block">
|
||||
<span class="nt"><record</span><span class="w"> </span><span class="na">id=</span><span class="s">"job_function_sale_order_action_done"</span><span class="w"> </span><span class="na">model=</span><span class="s">"queue.job.function"</span><span class="nt">></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"model_id"</span><span class="w"> </span><span class="na">ref=</span><span class="s">"sale.model_sale_order"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"method"</span><span class="nt">></span>action_done<span class="nt"></field></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"channel_id"</span><span class="w"> </span><span class="na">ref=</span><span class="s">"channel_sale"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"related_action"</span><span class="w"> </span><span class="na">eval=</span><span class="s">'{"func_name": "custom_related_action"}'</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"retry_pattern"</span><span class="w"> </span><span class="na">eval=</span><span class="s">"{1: 60, 2: 180, 3: 10, 5: 300}"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"></record></span>
|
||||
</pre>
|
||||
<p>The general form for the <tt class="docutils literal">name</tt> is: <tt class="docutils literal"><span class="pre"><model.name>.method</span></tt>.</p>
|
||||
<p>The channel, related action and retry pattern options are optional, they are
|
||||
documented below.</p>
|
||||
<p>When writing modules, if 2+ modules add a job function or channel with the same
|
||||
name (and parent for channels), they’ll be merged in the same record, even if
|
||||
they have different xmlids. On uninstall, the merged record is deleted when all
|
||||
the modules using it are uninstalled.</p>
|
||||
<p><strong>Job function: model</strong></p>
|
||||
<p>If the function is defined in an abstract model, you can not write
|
||||
<tt class="docutils literal"><field <span class="pre">name="model_id"</span> <span class="pre">ref="xml_id_of_the_abstract_model"</field></span></tt>
|
||||
but you have to define a function for each model that inherits from the abstract model.</p>
|
||||
<p><strong>Job function: channel</strong></p>
|
||||
<p>The channel where the job will be delayed. The default channel is <tt class="docutils literal">root</tt>.</p>
|
||||
<p><strong>Job function: related action</strong></p>
|
||||
<p>The <em>Related Action</em> appears as a button on the Job’s view.
|
||||
The button will execute the defined action.</p>
|
||||
<p>The default one is to open the view of the record related to the job (form view
|
||||
when there is a single record, list view for several records).
|
||||
In many cases, the default related action is enough and doesn’t need
|
||||
customization, but it can be customized by providing a dictionary on the job
|
||||
function:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="p">{</span><span class="w">
|
||||
</span> <span class="s2">"enable"</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="s2">"func_name"</span><span class="p">:</span> <span class="s2">"related_action_partner"</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="s2">"kwargs"</span><span class="p">:</span> <span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"Partner"</span><span class="p">},</span><span class="w">
|
||||
</span><span class="p">}</span>
|
||||
</pre>
|
||||
<ul class="simple">
|
||||
<li><tt class="docutils literal">enable</tt>: when <tt class="docutils literal">False</tt>, the button has no effect (default: <tt class="docutils literal">True</tt>)</li>
|
||||
<li><tt class="docutils literal">func_name</tt>: name of the method on <tt class="docutils literal">queue.job</tt> that returns an action</li>
|
||||
<li><tt class="docutils literal">kwargs</tt>: extra arguments to pass to the related action method</li>
|
||||
</ul>
|
||||
<p>Example of related action code:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="k">class</span><span class="w"> </span><span class="nc">QueueJob</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="n">_inherit</span> <span class="o">=</span> <span class="s1">'queue.job'</span><span class="w">
|
||||
|
||||
</span> <span class="k">def</span><span class="w"> </span><span class="nf">related_action_partner</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">ensure_one</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="n">model</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">model_name</span><span class="w">
|
||||
</span> <span class="n">partner</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">records</span><span class="w">
|
||||
</span> <span class="n">action</span> <span class="o">=</span> <span class="p">{</span><span class="w">
|
||||
</span> <span class="s1">'name'</span><span class="p">:</span> <span class="n">name</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="s1">'type'</span><span class="p">:</span> <span class="s1">'ir.actions.act_window'</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="s1">'res_model'</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="s1">'view_type'</span><span class="p">:</span> <span class="s1">'form'</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="s1">'view_mode'</span><span class="p">:</span> <span class="s1">'form'</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="s1">'res_id'</span><span class="p">:</span> <span class="n">partner</span><span class="o">.</span><span class="n">id</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="p">}</span><span class="w">
|
||||
</span> <span class="k">return</span> <span class="n">action</span>
|
||||
</pre>
|
||||
<p><strong>Job function: retry pattern</strong></p>
|
||||
<p>When a job fails with a retryable error type, it is automatically
|
||||
retried later. By default, the retry is always 10 minutes later.</p>
|
||||
<p>A retry pattern can be configured on the job function. What a pattern represents
|
||||
is “from X tries, postpone to Y seconds”. It is expressed as a dictionary where
|
||||
keys are tries and values are seconds to postpone as integers:</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="p">{</span><span class="w">
|
||||
</span> <span class="mi">1</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="mi">5</span><span class="p">:</span> <span class="mi">20</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="mi">10</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="mi">15</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span><span class="w">
|
||||
</span><span class="p">}</span>
|
||||
</pre>
|
||||
<p>Based on this configuration, we can tell that:</p>
|
||||
<ul class="simple">
|
||||
<li>5 first retries are postponed 10 seconds later</li>
|
||||
<li>retries 5 to 10 postponed 20 seconds later</li>
|
||||
<li>retries 10 to 15 postponed 30 seconds later</li>
|
||||
<li>all subsequent retries postponed 5 minutes later</li>
|
||||
</ul>
|
||||
<p><strong>Job Context</strong></p>
|
||||
<p>The context of the recordset of the job, or any recordset passed in arguments of
|
||||
a job, is transferred to the job according to an allow-list.</p>
|
||||
<p>The default allow-list is <cite>(“tz”, “lang”, “allowed_company_ids”, “force_company”, “active_test”)</cite>. It can
|
||||
be customized in <tt class="docutils literal">Base._job_prepare_context_before_enqueue_keys</tt>.
|
||||
<strong>Bypass jobs on running Odoo</strong></p>
|
||||
<p>When you are developing (ie: connector modules) you might want
|
||||
to bypass the queue job and run your code immediately.</p>
|
||||
<p>To do so you can set <cite>QUEUE_JOB__NO_DELAY=1</cite> in your environment.</p>
|
||||
<p><strong>Bypass jobs in tests</strong></p>
|
||||
<p>When writing tests on job-related methods is always tricky to deal with
|
||||
delayed recordsets. To make your testing life easier
|
||||
you can set <cite>queue_job__no_delay=True</cite> in the context.</p>
|
||||
<p>Tip: you can do this at test case level like this</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="nd">@classmethod</span><span class="w">
|
||||
</span><span class="k">def</span><span class="w"> </span><span class="nf">setUpClass</span><span class="p">(</span><span class="bp">cls</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">setUpClass</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="bp">cls</span><span class="o">.</span><span class="n">env</span> <span class="o">=</span> <span class="bp">cls</span><span class="o">.</span><span class="n">env</span><span class="p">(</span><span class="n">context</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="w">
|
||||
</span> <span class="bp">cls</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">context</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">queue_job__no_delay</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="c1"># no jobs thanks</span><span class="w">
|
||||
</span> <span class="p">))</span>
|
||||
</pre>
|
||||
<p>Then all your tests execute the job methods synchronously
|
||||
without delaying any jobs.</p>
|
||||
</div>
|
||||
<div class="section" id="testing">
|
||||
<h4><a class="toc-backref" href="#toc-entry-8">Testing</a></h4>
|
||||
<p><strong>Asserting enqueued jobs</strong></p>
|
||||
<p>The recommended way to test jobs, rather than running them directly and synchronously is to
|
||||
split the tests in two parts:</p>
|
||||
<blockquote>
|
||||
<ul class="simple">
|
||||
<li>one test where the job is mocked (trap jobs with <tt class="docutils literal">trap_jobs()</tt> and the test
|
||||
only verifies that the job has been delayed with the expected arguments</li>
|
||||
<li>one test that only calls the method of the job synchronously, to validate the
|
||||
proper behavior of this method only</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<p>Proceeding this way means that you can prove that jobs will be enqueued properly
|
||||
at runtime, and it ensures your code does not have a different behavior in tests
|
||||
and in production (because running your jobs synchronously may have a different
|
||||
behavior as they are in the same transaction / in the middle of the method).
|
||||
Additionally, it gives more control on the arguments you want to pass when
|
||||
calling the job’s method (synchronously, this time, in the second type of
|
||||
tests), and it makes tests smaller.</p>
|
||||
<p>The best way to run such assertions on the enqueued jobs is to use
|
||||
<tt class="docutils literal">odoo.addons.queue_job.tests.common.trap_jobs()</tt>.</p>
|
||||
<p>Inside this context manager, instead of being added in the database’s queue,
|
||||
jobs are pushed in an in-memory list. The context manager then provides useful
|
||||
helpers to verify that jobs have been enqueued with the expected arguments. It
|
||||
even can run the jobs of its list synchronously! Details in
|
||||
<tt class="docutils literal">odoo.addons.queue_job.tests.common.JobsTester</tt>.</p>
|
||||
<p>A very small example (more details in <tt class="docutils literal">tests/common.py</tt>):</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="c1"># code</span><span class="w">
|
||||
</span><span class="k">def</span><span class="w"> </span><span class="nf">my_job_method</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">count</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">write</span><span class="p">({</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">" "</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">name</span><span class="p">]</span> <span class="o">*</span> <span class="n">count</span><span class="p">)</span><span class="w">
|
||||
|
||||
</span><span class="k">def</span><span class="w"> </span><span class="nf">method_to_test</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="n">count</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"other.model"</span><span class="p">]</span><span class="o">.</span><span class="n">search_count</span><span class="p">([])</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">with_delay</span><span class="p">(</span><span class="n">priority</span><span class="o">=</span><span class="mi">15</span><span class="p">)</span><span class="o">.</span><span class="n">my_job_method</span><span class="p">(</span><span class="s2">"Hi!"</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="n">count</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="k">return</span> <span class="n">count</span><span class="w">
|
||||
|
||||
</span><span class="c1"># tests</span><span class="w">
|
||||
</span><span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.queue_job.tests.common</span><span class="w"> </span><span class="kn">import</span> <span class="n">trap_jobs</span><span class="w">
|
||||
|
||||
</span><span class="c1"># first test only check the expected behavior of the method and the proper</span><span class="w">
|
||||
</span><span class="c1"># enqueuing of jobs</span><span class="w">
|
||||
</span><span class="k">def</span><span class="w"> </span><span class="nf">test_method_to_test</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="k">with</span> <span class="n">trap_jobs</span><span class="p">()</span> <span class="k">as</span> <span class="n">trap</span><span class="p">:</span><span class="w">
|
||||
</span> <span class="n">result</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"model"</span><span class="p">]</span><span class="o">.</span><span class="n">method_to_test</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="n">expected_count</span> <span class="o">=</span> <span class="mi">12</span><span class="w">
|
||||
|
||||
</span> <span class="n">trap</span><span class="o">.</span><span class="n">assert_jobs_count</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">only</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"model"</span><span class="p">]</span><span class="o">.</span><span class="n">my_job_method</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="n">trap</span><span class="o">.</span><span class="n">assert_enqueued_job</span><span class="p">(</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"model"</span><span class="p">]</span><span class="o">.</span><span class="n">my_job_method</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="s2">"Hi!"</span><span class="p">,),</span><span class="w">
|
||||
</span> <span class="n">kwargs</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="n">count</span><span class="o">=</span><span class="n">expected_count</span><span class="p">),</span><span class="w">
|
||||
</span> <span class="n">properties</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="n">priority</span><span class="o">=</span><span class="mi">15</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="p">)</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">expected_count</span><span class="p">)</span><span class="w">
|
||||
|
||||
|
||||
</span> <span class="c1"># second test to validate the behavior of the job unitarily</span><span class="w">
|
||||
</span> <span class="k">def</span><span class="w"> </span><span class="nf">test_my_job_method</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="n">record</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"model"</span><span class="p">]</span><span class="o">.</span><span class="n">browse</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="n">record</span><span class="o">.</span><span class="n">my_job_method</span><span class="p">(</span><span class="s2">"Hi!"</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="mi">12</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">record</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="s2">"Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!"</span><span class="p">)</span>
|
||||
</pre>
|
||||
<p>If you prefer, you can still test the whole thing in a single test, by calling
|
||||
<tt class="docutils literal">jobs_tester.perform_enqueued_jobs()</tt> in your test.</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="k">def</span><span class="w"> </span><span class="nf">test_method_to_test</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="k">with</span> <span class="n">trap_jobs</span><span class="p">()</span> <span class="k">as</span> <span class="n">trap</span><span class="p">:</span><span class="w">
|
||||
</span> <span class="n">result</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"model"</span><span class="p">]</span><span class="o">.</span><span class="n">method_to_test</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="n">expected_count</span> <span class="o">=</span> <span class="mi">12</span><span class="w">
|
||||
|
||||
</span> <span class="n">trap</span><span class="o">.</span><span class="n">assert_jobs_count</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">only</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"model"</span><span class="p">]</span><span class="o">.</span><span class="n">my_job_method</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="n">trap</span><span class="o">.</span><span class="n">assert_enqueued_job</span><span class="p">(</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"model"</span><span class="p">]</span><span class="o">.</span><span class="n">my_job_method</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="s2">"Hi!"</span><span class="p">,),</span><span class="w">
|
||||
</span> <span class="n">kwargs</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="n">count</span><span class="o">=</span><span class="n">expected_count</span><span class="p">),</span><span class="w">
|
||||
</span> <span class="n">properties</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="n">priority</span><span class="o">=</span><span class="mi">15</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="p">)</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">expected_count</span><span class="p">)</span><span class="w">
|
||||
|
||||
</span> <span class="n">trap</span><span class="o">.</span><span class="n">perform_enqueued_jobs</span><span class="p">()</span><span class="w">
|
||||
|
||||
</span> <span class="n">record</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"model"</span><span class="p">]</span><span class="o">.</span><span class="n">browse</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="n">record</span><span class="o">.</span><span class="n">my_job_method</span><span class="p">(</span><span class="s2">"Hi!"</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="mi">12</span><span class="p">)</span><span class="w">
|
||||
</span> <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">record</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="s2">"Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!"</span><span class="p">)</span>
|
||||
</pre>
|
||||
<p><strong>Execute jobs synchronously when running Odoo</strong></p>
|
||||
<p>When you are developing (ie: connector modules) you might want
|
||||
to bypass the queue job and run your code immediately.</p>
|
||||
<p>To do so you can set <tt class="docutils literal">QUEUE_JOB__NO_DELAY=1</tt> in your environment.</p>
|
||||
<div class="admonition warning">
|
||||
<p class="first admonition-title">Warning</p>
|
||||
<p class="last">Do not do this in production</p>
|
||||
</div>
|
||||
<p><strong>Execute jobs synchronously in tests</strong></p>
|
||||
<p>You should use <tt class="docutils literal">trap_jobs</tt>, really, but if for any reason you could not use it,
|
||||
and still need to have job methods executed synchronously in your tests, you can
|
||||
do so by setting <tt class="docutils literal">queue_job__no_delay=True</tt> in the context.</p>
|
||||
<p>Tip: you can do this at test case level like this</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="nd">@classmethod</span><span class="w">
|
||||
</span><span class="k">def</span><span class="w"> </span><span class="nf">setUpClass</span><span class="p">(</span><span class="bp">cls</span><span class="p">):</span><span class="w">
|
||||
</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">setUpClass</span><span class="p">()</span><span class="w">
|
||||
</span> <span class="bp">cls</span><span class="o">.</span><span class="n">env</span> <span class="o">=</span> <span class="bp">cls</span><span class="o">.</span><span class="n">env</span><span class="p">(</span><span class="n">context</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="w">
|
||||
</span> <span class="bp">cls</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">context</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">queue_job__no_delay</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="c1"># no jobs thanks</span><span class="w">
|
||||
</span> <span class="p">))</span>
|
||||
</pre>
|
||||
<p>Then all your tests execute the job methods synchronously without delaying any
|
||||
jobs.</p>
|
||||
<p>In tests you’ll have to mute the logger like:</p>
|
||||
<blockquote>
|
||||
@mute_logger(‘odoo.addons.queue_job.models.base’)</blockquote>
|
||||
<div class="admonition note">
|
||||
<p class="first admonition-title">Note</p>
|
||||
<p class="last">in graphs of jobs, the <tt class="docutils literal">queue_job__no_delay</tt> context key must be in at
|
||||
least one job’s env of the graph for the whole graph to be executed synchronously</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="tips-and-tricks">
|
||||
<h4><a class="toc-backref" href="#toc-entry-9">Tips and tricks</a></h4>
|
||||
<ul class="simple">
|
||||
<li><strong>Idempotency</strong> (<a class="reference external" href="https://www.restapitutorial.com/lessons/idempotency.html">https://www.restapitutorial.com/lessons/idempotency.html</a>): The queue_job should be idempotent so they can be retried several times without impact on the data.</li>
|
||||
<li><strong>The job should test at the very beginning its relevance</strong>: the moment the job will be executed is unknown by design. So the first task of a job should be to check if the related work is still relevant at the moment of the execution.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="patterns">
|
||||
<h4><a class="toc-backref" href="#toc-entry-10">Patterns</a></h4>
|
||||
<p>Through the time, two main patterns emerged:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>For data exposed to users, a model should store the data and the model should be the creator of the job. The job is kept hidden from the users</li>
|
||||
<li>For technical data, that are not exposed to the users, it is generally alright to create directly jobs with data passed as arguments to the job, without intermediary models.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="known-issues-roadmap">
|
||||
<h2><a class="toc-backref" href="#toc-entry-11">Known issues / Roadmap</a></h2>
|
||||
<ul class="simple">
|
||||
<li>After creating a new database or installing <tt class="docutils literal">queue_job</tt> on an
|
||||
existing database, Odoo must be restarted for the runner to detect it.</li>
|
||||
<li>When Odoo shuts down normally, it waits for running jobs to finish.
|
||||
However, when the Odoo server crashes or is otherwise force-stopped,
|
||||
running jobs are interrupted while the runner has no chance to know
|
||||
they have been aborted. In such situations, jobs may remain in
|
||||
<tt class="docutils literal">started</tt> or <tt class="docutils literal">enqueued</tt> state after the Odoo server is halted.
|
||||
Since the runner has no way to know if they are actually running or
|
||||
not, and does not know for sure if it is safe to restart the jobs,
|
||||
it does not attempt to restart them automatically. Such stale jobs
|
||||
therefore fill the running queue and prevent other jobs to start.
|
||||
You must therefore requeue them manually, either from the Jobs view,
|
||||
or by running the following SQL statement <em>before starting Odoo</em>:</li>
|
||||
</ul>
|
||||
<pre class="code sql literal-block">
|
||||
<span class="k">update</span><span class="w"> </span><span class="n">queue_job</span><span class="w"> </span><span class="k">set</span><span class="w"> </span><span class="k">state</span><span class="o">=</span><span class="s1">'pending'</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="k">state</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="p">(</span><span class="s1">'started'</span><span class="p">,</span><span class="w"> </span><span class="s1">'enqueued'</span><span class="p">)</span>
|
||||
</pre>
|
||||
</div>
|
||||
<div class="section" id="changelog">
|
||||
<h2><a class="toc-backref" href="#toc-entry-12">Changelog</a></h2>
|
||||
<!-- [ The change log. The goal of this file is to help readers
|
||||
understand changes between version. The primary audience is
|
||||
end users and integrators. Purely technical changes such as
|
||||
code refactoring must not be mentioned here.
|
||||
|
||||
This file may contain ONE level of section titles, underlined
|
||||
with the ~ (tilde) character. Other section markers are
|
||||
forbidden and will likely break the structure of the README.rst
|
||||
or other documents where this fragment is included. ] -->
|
||||
<div class="section" id="next">
|
||||
<h3><a class="toc-backref" href="#toc-entry-13">Next</a></h3>
|
||||
<ul class="simple">
|
||||
<li>[ADD] Run jobrunner as a worker process instead of a thread in the main
|
||||
process (when running with –workers > 0)</li>
|
||||
<li>[REF] <tt class="docutils literal">@job</tt> and <tt class="docutils literal">@related_action</tt> deprecated, any method can be delayed,
|
||||
and configured using <tt class="docutils literal">queue.job.function</tt> records</li>
|
||||
<li>[MIGRATION] from 13.0 branched at rev. e24ff4b</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h2><a class="toc-backref" href="#toc-entry-14">Bug Tracker</a></h2>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/queue/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/queue/issues/new?body=module:%20queue_job%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h2><a class="toc-backref" href="#toc-entry-15">Credits</a></h2>
|
||||
<div class="section" id="authors">
|
||||
<h3><a class="toc-backref" href="#toc-entry-16">Authors</a></h3>
|
||||
<ul class="simple">
|
||||
<li>Camptocamp</li>
|
||||
<li>ACSONE SA/NV</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h3><a class="toc-backref" href="#toc-entry-17">Contributors</a></h3>
|
||||
<ul class="simple">
|
||||
<li>Guewen Baconnier <<a class="reference external" href="mailto:guewen.baconnier@camptocamp.com">guewen.baconnier@camptocamp.com</a>></li>
|
||||
<li>Stéphane Bidoul <<a class="reference external" href="mailto:stephane.bidoul@acsone.eu">stephane.bidoul@acsone.eu</a>></li>
|
||||
<li>Matthieu Dietrich <<a class="reference external" href="mailto:matthieu.dietrich@camptocamp.com">matthieu.dietrich@camptocamp.com</a>></li>
|
||||
<li>Jos De Graeve <<a class="reference external" href="mailto:Jos.DeGraeve@apertoso.be">Jos.DeGraeve@apertoso.be</a>></li>
|
||||
<li>David Lefever <<a class="reference external" href="mailto:dl@taktik.be">dl@taktik.be</a>></li>
|
||||
<li>Laurent Mignon <<a class="reference external" href="mailto:laurent.mignon@acsone.eu">laurent.mignon@acsone.eu</a>></li>
|
||||
<li>Laetitia Gangloff <<a class="reference external" href="mailto:laetitia.gangloff@acsone.eu">laetitia.gangloff@acsone.eu</a>></li>
|
||||
<li>Cédric Pigeon <<a class="reference external" href="mailto:cedric.pigeon@acsone.eu">cedric.pigeon@acsone.eu</a>></li>
|
||||
<li>Tatiana Deribina <<a class="reference external" href="mailto:tatiana.deribina@avoin.systems">tatiana.deribina@avoin.systems</a>></li>
|
||||
<li>Souheil Bejaoui <<a class="reference external" href="mailto:souheil.bejaoui@acsone.eu">souheil.bejaoui@acsone.eu</a>></li>
|
||||
<li>Eric Antones <<a class="reference external" href="mailto:eantones@nuobit.com">eantones@nuobit.com</a>></li>
|
||||
<li>Simone Orsi <<a class="reference external" href="mailto:simone.orsi@camptocamp.com">simone.orsi@camptocamp.com</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h3><a class="toc-backref" href="#toc-entry-18">Maintainers</a></h3>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
</a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
|
||||
<p><a class="reference external image-reference" href="https://github.com/guewen"><img alt="guewen" src="https://github.com/guewen.png?size=40px" /></a></p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/queue/tree/16.0/queue_job">OCA/queue</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,143 +0,0 @@
|
||||
/* @odoo-module */
|
||||
/* global vis */
|
||||
|
||||
import {loadCSS, loadJS} from "@web/core/assets";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {standardFieldProps} from "@web/views/fields/standard_field_props";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
|
||||
const {Component, onWillStart, useEffect, useRef} = owl;
|
||||
|
||||
export class JobDirectGraph extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
this.rootRef = useRef("root_vis");
|
||||
this.network = null;
|
||||
onWillStart(async () => {
|
||||
await loadJS("/queue_job/static/lib/vis/vis-network.min.js");
|
||||
loadCSS("/queue_job/static/lib/vis/vis-network.min.css");
|
||||
});
|
||||
useEffect(() => {
|
||||
this.renderNetwork();
|
||||
this._fitNetwork();
|
||||
return () => {
|
||||
if (this.network) {
|
||||
this.$el.empty();
|
||||
}
|
||||
return this.rootRef.el;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get $el() {
|
||||
return $(this.rootRef.el);
|
||||
}
|
||||
|
||||
get resId() {
|
||||
return this.props.record.data.id;
|
||||
}
|
||||
|
||||
get context() {
|
||||
return this.props.record.getFieldContext(this.props.name);
|
||||
}
|
||||
|
||||
get model() {
|
||||
return this.props.record.resModel;
|
||||
}
|
||||
|
||||
htmlTitle(html) {
|
||||
const container = document.createElement("div");
|
||||
container.innerHTML = html;
|
||||
return container;
|
||||
}
|
||||
|
||||
renderNetwork() {
|
||||
if (this.network) {
|
||||
this.$el.empty();
|
||||
}
|
||||
let nodes = this.props.value.nodes || [];
|
||||
if (!nodes.length) {
|
||||
return;
|
||||
}
|
||||
nodes = nodes.map((node) => {
|
||||
node.title = this.htmlTitle(node.title || "");
|
||||
return node;
|
||||
});
|
||||
|
||||
const edges = [];
|
||||
_.each(this.props.value.edges || [], function (edge) {
|
||||
const edgeFrom = edge[0];
|
||||
const edgeTo = edge[1];
|
||||
edges.push({
|
||||
from: edgeFrom,
|
||||
to: edgeTo,
|
||||
arrows: "to",
|
||||
});
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: new vis.DataSet(nodes),
|
||||
edges: new vis.DataSet(edges),
|
||||
};
|
||||
const options = {
|
||||
// Fix the seed to have always the same result for the same graph
|
||||
layout: {randomSeed: 1},
|
||||
};
|
||||
// Arbitrary threshold, generation becomes very slow at some
|
||||
// point, and disabling the stabilization helps to have a fast result.
|
||||
// Actually, it stabilizes, but is displayed while stabilizing, rather
|
||||
// than showing a blank canvas.
|
||||
if (nodes.length > 100) {
|
||||
options.physics = {stabilization: false};
|
||||
}
|
||||
const network = new vis.Network(this.$el[0], data, options);
|
||||
network.selectNodes([this.resId]);
|
||||
var self = this;
|
||||
network.on("dragging", function () {
|
||||
// By default, dragging changes the selected node
|
||||
// to the dragged one, we want to keep the current
|
||||
// job selected
|
||||
network.selectNodes([self.resId]);
|
||||
});
|
||||
network.on("click", function (params) {
|
||||
if (params.nodes.length > 0) {
|
||||
var resId = params.nodes[0];
|
||||
if (resId !== self.resId) {
|
||||
self.openDependencyJob(resId);
|
||||
}
|
||||
} else {
|
||||
// Clicked outside of the nodes, we want to
|
||||
// keep the current job selected
|
||||
network.selectNodes([self.resId]);
|
||||
}
|
||||
});
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
async openDependencyJob(resId) {
|
||||
const action = await this.orm.call(
|
||||
this.model,
|
||||
"get_formview_action",
|
||||
[[resId]],
|
||||
{
|
||||
context: this.context,
|
||||
}
|
||||
);
|
||||
await this.action.doAction(action);
|
||||
}
|
||||
|
||||
_fitNetwork() {
|
||||
if (this.network) {
|
||||
this.network.fit(this.network.body.nodeIndices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JobDirectGraph.props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
|
||||
JobDirectGraph.template = "queue.JobDirectGraph";
|
||||
|
||||
registry.category("fields").add("job_directed_graph", JobDirectGraph);
|
||||
@@ -1,10 +0,0 @@
|
||||
.o_field_job_directed_graph {
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
border: 1px solid lightgray;
|
||||
|
||||
div.root_vis {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="queue.JobDirectGraph" owl="1">
|
||||
<div id="props.id" t-ref="root_vis" class="root_vis" />
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,11 +0,0 @@
|
||||
from . import test_run_rob_controller
|
||||
from . import test_runner_channels
|
||||
from . import test_runner_runner
|
||||
from . import test_delayable
|
||||
from . import test_delayable_split
|
||||
from . import test_json_field
|
||||
from . import test_model_job_channel
|
||||
from . import test_model_job_function
|
||||
from . import test_queue_job_protected_write
|
||||
from . import test_wizards
|
||||
from . import test_requeue_dead_job
|
||||
@@ -1,466 +0,0 @@
|
||||
# Copyright 2019 Camptocamp
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
import doctest
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
from contextlib import contextmanager
|
||||
from itertools import groupby
|
||||
from operator import attrgetter
|
||||
from unittest import TestCase, mock
|
||||
|
||||
from odoo.addons.queue_job.delay import Graph
|
||||
|
||||
# pylint: disable=odoo-addons-relative-import
|
||||
from odoo.addons.queue_job.job import Job
|
||||
|
||||
|
||||
@contextmanager
|
||||
def trap_jobs():
|
||||
"""Context Manager used to test enqueuing of jobs
|
||||
|
||||
Trapping jobs allows to split the tests in:
|
||||
|
||||
* the part that delays the job with the expected arguments in one test
|
||||
* the execution of the job itself in a second test
|
||||
|
||||
When the jobs are trapped, they are not executed at all, however, we
|
||||
can verify they have been enqueued with the correct arguments and
|
||||
properties.
|
||||
|
||||
Then in a second test, we can call the job method directly with the
|
||||
arguments to test.
|
||||
|
||||
The context manager yields a instance of ``JobsTrap``, which provides
|
||||
utilities and assert methods.
|
||||
|
||||
Example of method to test::
|
||||
|
||||
def button_that_uses_delayable_chain(self):
|
||||
delayables = chain(
|
||||
self.delayable(
|
||||
channel="root.test",
|
||||
description="Test",
|
||||
eta=15,
|
||||
identity_key=identity_exact,
|
||||
max_retries=1,
|
||||
priority=15,
|
||||
).testing_method(1, foo=2),
|
||||
self.delayable().testing_method('x', foo='y'),
|
||||
self.delayable().no_description(),
|
||||
)
|
||||
delayables.delay()
|
||||
|
||||
Example of usage in a test::
|
||||
|
||||
with trap_jobs() as trap:
|
||||
self.env['test.queue.job'].button_that_uses_delayable_chain()
|
||||
|
||||
trap.assert_jobs_count(3)
|
||||
trap.assert_jobs_count(
|
||||
2, only=self.env['test.queue.job'].testing_method
|
||||
|
||||
)
|
||||
trap.assert_jobs_count(
|
||||
1, only=self.env['test.queue.job'].no_description
|
||||
)
|
||||
|
||||
trap.assert_enqueued_job(
|
||||
self.env['test.queue.job'].testing_method,
|
||||
args=(1,),
|
||||
kwargs={"foo": 2},
|
||||
properties=dict(
|
||||
channel="root.test",
|
||||
description="Test",
|
||||
eta=15,
|
||||
identity_key=identity_exact,
|
||||
max_retries=1,
|
||||
priority=15,
|
||||
)
|
||||
)
|
||||
trap.assert_enqueued_job(
|
||||
self.env['test.queue.job'].testing_method,
|
||||
args=("x",),
|
||||
kwargs={"foo": "y"},
|
||||
)
|
||||
trap.assert_enqueued_job(
|
||||
self.env['test.queue.job'].no_description,
|
||||
)
|
||||
|
||||
# optionally, you can perform the jobs synchronously (without going
|
||||
# to the database)
|
||||
jobs_tester.perform_enqueued_jobs()
|
||||
"""
|
||||
with mock.patch(
|
||||
"odoo.addons.queue_job.delay.Job",
|
||||
name="Job Class",
|
||||
auto_spec=True,
|
||||
unsafe=True,
|
||||
) as job_cls_mock:
|
||||
with JobsTrap(job_cls_mock) as trap:
|
||||
yield trap
|
||||
|
||||
|
||||
class JobCall(typing.NamedTuple):
|
||||
method: typing.Callable
|
||||
args: tuple
|
||||
kwargs: dict
|
||||
properties: dict
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, JobCall):
|
||||
return NotImplemented
|
||||
return (
|
||||
self.method.__self__ == other.method.__self__
|
||||
and self.method.__func__ == other.method.__func__
|
||||
and self.args == other.args
|
||||
and self.kwargs == other.kwargs
|
||||
and self.properties == other.properties
|
||||
)
|
||||
|
||||
|
||||
class JobsTrap:
|
||||
"""Used by ``trap_jobs()``, provide assert methods on the trapped jobs
|
||||
|
||||
Look the documentation of ``trap_jobs()`` for a usage example.
|
||||
|
||||
The ``store`` method of the Job instances is mocked so they are never
|
||||
saved in database.
|
||||
|
||||
Helpers for tests:
|
||||
|
||||
* ``jobs_count``
|
||||
* ``assert_jobs_count``
|
||||
* ``assert_enqueued_job``
|
||||
* ``perform_enqueued_jobs``
|
||||
|
||||
You can also access the list of calls that were made to enqueue the jobs in
|
||||
the ``calls`` attribute, and the generated jobs in the ``enqueued_jobs``.
|
||||
"""
|
||||
|
||||
def __init__(self, job_mock):
|
||||
self.job_mock = job_mock
|
||||
self.job_mock.side_effect = self._add_job
|
||||
# 1 call == 1 job, they share the same position in the lists
|
||||
self.calls = []
|
||||
self.enqueued_jobs = []
|
||||
self._store_patchers = []
|
||||
self._test_case = TestCase()
|
||||
|
||||
def jobs_count(self, only=None):
|
||||
"""Return the count of enqueued jobs
|
||||
|
||||
``only`` is an option method on which the count is filtered
|
||||
"""
|
||||
if only:
|
||||
return len(self._filtered_enqueued_jobs(only))
|
||||
return len(self.enqueued_jobs)
|
||||
|
||||
def assert_jobs_count(self, expected, only=None):
|
||||
"""Raise an assertion error if the count of enqueued jobs does not match
|
||||
|
||||
``only`` is an option method on which the count is filtered
|
||||
"""
|
||||
self._test_case.assertEqual(self.jobs_count(only=only), expected)
|
||||
|
||||
def assert_enqueued_job(self, method, args=None, kwargs=None, properties=None):
|
||||
"""Raise an assertion error if the expected method has not been enqueued
|
||||
|
||||
* ``method`` is the method (as method object) delayed as job
|
||||
* ``args`` is a tuple of arguments passed to the job method
|
||||
* ``kwargs`` is a dict of keyword arguments passed to the job method
|
||||
* ``properties`` is a dict of job properties (priority, eta, ...)
|
||||
|
||||
The args and the kwargs *must* be match exactly what has been enqueued
|
||||
in the job method. The properties are optional: if the job has been
|
||||
enqueued with a custom description but the assert method is not called
|
||||
with ``description`` in the properties, it still matches the call.
|
||||
However, if a ``description`` is passed in the assert's properties, it
|
||||
must match.
|
||||
"""
|
||||
if properties is None:
|
||||
properties = {}
|
||||
if args is None:
|
||||
args = ()
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
expected_call = JobCall(
|
||||
method=method,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
properties=properties,
|
||||
)
|
||||
actual_calls = []
|
||||
for call in self.calls:
|
||||
checked_properties = {
|
||||
key: value
|
||||
for key, value in call.properties.items()
|
||||
if key in properties
|
||||
}
|
||||
# build copy of calls with only the properties that we want to
|
||||
# check
|
||||
actual_calls.append(
|
||||
JobCall(
|
||||
method=call.method,
|
||||
args=call.args,
|
||||
kwargs=call.kwargs,
|
||||
properties=checked_properties,
|
||||
)
|
||||
)
|
||||
|
||||
if expected_call not in actual_calls:
|
||||
raise AssertionError(
|
||||
"Job %s was not enqueued.\n"
|
||||
"Actual enqueued jobs:\n%s"
|
||||
% (
|
||||
self._format_job_call(expected_call),
|
||||
"\n".join(
|
||||
" * %s" % (self._format_job_call(call),)
|
||||
for call in actual_calls
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def perform_enqueued_jobs(self):
|
||||
"""Perform the enqueued jobs synchronously"""
|
||||
|
||||
def by_graph(job):
|
||||
return job.graph_uuid or ""
|
||||
|
||||
sorted_jobs = sorted(self.enqueued_jobs, key=by_graph)
|
||||
self.enqueued_jobs = []
|
||||
for graph_uuid, jobs in groupby(sorted_jobs, key=by_graph):
|
||||
if graph_uuid:
|
||||
self._perform_graph_jobs(jobs)
|
||||
else:
|
||||
self._perform_single_jobs(jobs)
|
||||
|
||||
def _perform_single_jobs(self, jobs):
|
||||
# we probably don't want to replicate a perfect order here, but at
|
||||
# least respect the priority
|
||||
for job in sorted(jobs, key=attrgetter("priority")):
|
||||
job.perform()
|
||||
|
||||
def _perform_graph_jobs(self, jobs):
|
||||
graph = Graph()
|
||||
for job in jobs:
|
||||
graph.add_vertex(job)
|
||||
for parent in job.depends_on:
|
||||
graph.add_edge(parent, job)
|
||||
|
||||
for job in graph.topological_sort():
|
||||
job.perform()
|
||||
|
||||
def _add_job(self, *args, **kwargs):
|
||||
job = Job(*args, **kwargs)
|
||||
if not job.identity_key or all(
|
||||
j.identity_key != job.identity_key for j in self.enqueued_jobs
|
||||
):
|
||||
self._prepare_context(job)
|
||||
self.enqueued_jobs.append(job)
|
||||
|
||||
patcher = mock.patch.object(job, "store")
|
||||
self._store_patchers.append(patcher)
|
||||
patcher.start()
|
||||
|
||||
job_args = kwargs.pop("args", None) or ()
|
||||
job_kwargs = kwargs.pop("kwargs", None) or {}
|
||||
self.calls.append(
|
||||
JobCall(
|
||||
method=args[0],
|
||||
args=job_args,
|
||||
kwargs=job_kwargs,
|
||||
properties=kwargs,
|
||||
)
|
||||
)
|
||||
return job
|
||||
|
||||
def _prepare_context(self, job):
|
||||
# pylint: disable=context-overridden
|
||||
job_model = job.job_model.with_context({})
|
||||
field_records = job_model._fields["records"]
|
||||
# Filter the context to simulate store/load of the job
|
||||
job.recordset = field_records.convert_to_write(job.recordset, job_model)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
for patcher in self._store_patchers:
|
||||
patcher.stop()
|
||||
|
||||
def _filtered_enqueued_jobs(self, job_method):
|
||||
enqueued_jobs = [
|
||||
job
|
||||
for job in self.enqueued_jobs
|
||||
if job.func.__self__ == job_method.__self__
|
||||
and job.func.__func__ == job_method.__func__
|
||||
]
|
||||
return enqueued_jobs
|
||||
|
||||
def _format_job_call(self, call):
|
||||
method_all_args = []
|
||||
if call.args:
|
||||
method_all_args.append(", ".join("%s" % (arg,) for arg in call.args))
|
||||
if call.kwargs:
|
||||
method_all_args.append(
|
||||
", ".join("%s=%s" % (key, value) for key, value in call.kwargs.items())
|
||||
)
|
||||
return "<%s>.%s(%s) with properties (%s)" % (
|
||||
call.method.__self__,
|
||||
call.method.__name__,
|
||||
", ".join(method_all_args),
|
||||
", ".join("%s=%s" % (key, value) for key, value in call.properties.items()),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.calls)
|
||||
|
||||
|
||||
class JobCounter:
|
||||
def __init__(self, env):
|
||||
super().__init__()
|
||||
self.env = env
|
||||
self.existing = self.search_all()
|
||||
|
||||
def count_all(self):
|
||||
return len(self.search_all())
|
||||
|
||||
def count_created(self):
|
||||
return len(self.search_created())
|
||||
|
||||
def count_existing(self):
|
||||
return len(self.existing)
|
||||
|
||||
def search_created(self):
|
||||
return self.search_all() - self.existing
|
||||
|
||||
def search_all(self):
|
||||
return self.env["queue.job"].search([])
|
||||
|
||||
|
||||
class JobMixin:
|
||||
def job_counter(self):
|
||||
return JobCounter(self.env)
|
||||
|
||||
def perform_jobs(self, jobs):
|
||||
for job in jobs.search_created():
|
||||
Job.load(self.env, job.uuid).perform()
|
||||
|
||||
@contextmanager
|
||||
def trap_jobs(self):
|
||||
with trap_jobs() as trap:
|
||||
yield trap
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mock_with_delay():
|
||||
"""Context Manager mocking ``with_delay()``
|
||||
|
||||
DEPRECATED: use ``trap_jobs()'``.
|
||||
|
||||
Mocking this method means we can decorrelate the tests in:
|
||||
|
||||
* the part that delay the job with the expected arguments
|
||||
* the execution of the job itself
|
||||
|
||||
The first kind of test does not need to actually create the jobs in the
|
||||
database, as we can inspect how the Mocks were called.
|
||||
|
||||
The second kind of test calls directly the method decorated by ``@job``
|
||||
with the arguments that we want to test.
|
||||
|
||||
The context manager returns 2 mocks:
|
||||
* the first allow to check that with_delay() was called and with which
|
||||
arguments
|
||||
* the second to check which job method was called and with which arguments.
|
||||
|
||||
Example of test::
|
||||
|
||||
def test_export(self):
|
||||
with mock_with_delay() as (delayable_cls, delayable):
|
||||
# inside this method, there is a call
|
||||
# partner.with_delay(priority=15).export_record('test')
|
||||
self.record.run_export()
|
||||
|
||||
# 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.env['res.partner'],)
|
||||
)
|
||||
self.assertDictEqual(delay_kwargs, {priority: 15})
|
||||
|
||||
# check what's passed to the job method 'export_record'
|
||||
self.assertEqual(delayable.export_record.call_count, 1)
|
||||
delay_args, delay_kwargs = delayable.export_record.call_args
|
||||
self.assertEqual(delay_args, ('test',))
|
||||
self.assertDictEqual(delay_kwargs, {})
|
||||
|
||||
An example of the first kind of test:
|
||||
https://github.com/camptocamp/connector-jira/blob/0ca4261b3920d5e8c2ae4bb0fc352ea3f6e9d2cd/connector_jira/tests/test_batch_timestamp_import.py#L43-L76 # noqa
|
||||
And the second kind:
|
||||
https://github.com/camptocamp/connector-jira/blob/0ca4261b3920d5e8c2ae4bb0fc352ea3f6e9d2cd/connector_jira/tests/test_import_task.py#L34-L46 # noqa
|
||||
|
||||
"""
|
||||
with mock.patch(
|
||||
"odoo.addons.queue_job.models.base.DelayableRecordset",
|
||||
name="DelayableRecordset",
|
||||
spec=True,
|
||||
) as delayable_cls:
|
||||
# prepare the mocks
|
||||
delayable = mock.MagicMock(name="DelayableBinding")
|
||||
delayable_cls.return_value = delayable
|
||||
yield delayable_cls, delayable
|
||||
|
||||
|
||||
class OdooDocTestCase(doctest.DocTestCase):
|
||||
"""
|
||||
We need a custom DocTestCase class in order to:
|
||||
- define test_tags to run as part of standard tests
|
||||
- output a more meaningful test name than default "DocTestCase.runTest"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, doctest, optionflags=0, setUp=None, tearDown=None, checker=None, seq=0
|
||||
):
|
||||
super().__init__(
|
||||
doctest._dt_test,
|
||||
optionflags=optionflags,
|
||||
setUp=setUp,
|
||||
tearDown=tearDown,
|
||||
checker=checker,
|
||||
)
|
||||
self.test_sequence = seq
|
||||
|
||||
def setUp(self):
|
||||
"""Log an extra statement which test is started."""
|
||||
super().setUp()
|
||||
logging.getLogger(__name__).info("Running tests for %s", self._dt_test.name)
|
||||
|
||||
|
||||
def load_doctests(module):
|
||||
"""
|
||||
Generates a tests loading method for the doctests of the given module
|
||||
https://docs.python.org/3/library/unittest.html#load-tests-protocol
|
||||
"""
|
||||
|
||||
def load_tests(loader, tests, ignore):
|
||||
"""
|
||||
Apply the 'test_tags' attribute to each DocTestCase found by the DocTestSuite.
|
||||
Also extend the DocTestCase class trivially to fit the class teardown
|
||||
that Odoo backported for its own test classes from Python 3.8.
|
||||
"""
|
||||
if sys.version_info < (3, 8):
|
||||
doctest.DocTestCase.doClassCleanups = lambda: None
|
||||
doctest.DocTestCase.tearDown_exceptions = []
|
||||
|
||||
for idx, test in enumerate(doctest.DocTestSuite(module)):
|
||||
odoo_test = OdooDocTestCase(test, seq=idx)
|
||||
odoo_test.test_tags = {"standard", "at_install", "queue_job", "doctest"}
|
||||
tests.addTest(odoo_test)
|
||||
|
||||
return tests
|
||||
|
||||
return load_tests
|
||||
@@ -1,287 +0,0 @@
|
||||
# copyright 2019 Camptocamp
|
||||
# license agpl-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
import gc
|
||||
import logging
|
||||
from unittest import mock
|
||||
|
||||
from odoo.tests import common
|
||||
|
||||
from odoo.addons.queue_job.delay import Delayable, DelayableGraph
|
||||
|
||||
|
||||
class TestDelayable(common.BaseCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.recordset = mock.MagicMock(name="recordset")
|
||||
|
||||
def test_delayable_set(self):
|
||||
# Use gc for garbage collection and use assertLogs to suppress WARNING
|
||||
with self.assertLogs("odoo.addons.queue_job.delay", level=logging.WARNING):
|
||||
dl = Delayable(self.recordset)
|
||||
dl.set(priority=15)
|
||||
self.assertEqual(dl.priority, 15)
|
||||
dl.set({"priority": 20, "description": "test"})
|
||||
self.assertEqual(dl.priority, 20)
|
||||
self.assertEqual(dl.description, "test")
|
||||
del dl
|
||||
gc.collect()
|
||||
|
||||
def test_delayable_set_unknown(self):
|
||||
# Use gc for garbage collection and use assertLogs to suppress WARNING
|
||||
with self.assertLogs("odoo.addons.queue_job.delay", level=logging.WARNING):
|
||||
dl = Delayable(self.recordset)
|
||||
with self.assertRaises(ValueError):
|
||||
dl.set(foo=15)
|
||||
del dl
|
||||
gc.collect()
|
||||
|
||||
def test_graph_add_vertex_edge(self):
|
||||
graph = DelayableGraph()
|
||||
graph.add_vertex("a")
|
||||
self.assertEqual(graph._graph, {"a": set()})
|
||||
graph.add_edge("a", "b")
|
||||
self.assertEqual(graph._graph, {"a": {"b"}, "b": set()})
|
||||
graph.add_edge("b", "c")
|
||||
self.assertEqual(graph._graph, {"a": {"b"}, "b": {"c"}, "c": set()})
|
||||
|
||||
def test_graph_vertices(self):
|
||||
graph = DelayableGraph({"a": {"b"}, "b": {"c"}, "c": set()})
|
||||
self.assertEqual(graph.vertices(), {"a", "b", "c"})
|
||||
|
||||
def test_graph_edges(self):
|
||||
graph = DelayableGraph(
|
||||
{"a": {"b"}, "b": {"c", "d"}, "c": {"e"}, "d": set(), "e": set()}
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(graph.edges()),
|
||||
sorted(
|
||||
[
|
||||
("a", "b"),
|
||||
("b", "c"),
|
||||
("b", "d"),
|
||||
("c", "e"),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
def test_graph_connect(self):
|
||||
# Use gc for garbage collection and use assertLogs to suppress WARNING
|
||||
with self.assertLogs("odoo.addons.queue_job.delay", level=logging.WARNING):
|
||||
node_tail = Delayable(self.recordset)
|
||||
node_tail2 = Delayable(self.recordset)
|
||||
node_middle = Delayable(self.recordset)
|
||||
node_top = Delayable(self.recordset)
|
||||
node_middle.on_done(node_tail)
|
||||
node_middle.on_done(node_tail2)
|
||||
node_top.on_done(node_middle)
|
||||
collected = node_top._graph._connect_graphs()
|
||||
self.assertEqual(
|
||||
collected._graph,
|
||||
{
|
||||
node_tail: set(),
|
||||
node_tail2: set(),
|
||||
node_middle: {node_tail, node_tail2},
|
||||
node_top: {node_middle},
|
||||
},
|
||||
)
|
||||
|
||||
del node_tail, node_tail2, node_middle, node_top, collected
|
||||
gc.collect()
|
||||
|
||||
def test_graph_paths(self):
|
||||
graph = DelayableGraph(
|
||||
{"a": {"b"}, "b": {"c", "d"}, "c": {"e"}, "d": set(), "e": set()}
|
||||
)
|
||||
paths = list(graph.paths("a"))
|
||||
self.assertEqual(sorted(paths), sorted([["a", "b", "d"], ["a", "b", "c", "e"]]))
|
||||
paths = list(graph.paths("b"))
|
||||
self.assertEqual(sorted(paths), sorted([["b", "d"], ["b", "c", "e"]]))
|
||||
paths = list(graph.paths("c"))
|
||||
self.assertEqual(paths, [["c", "e"]])
|
||||
paths = list(graph.paths("d"))
|
||||
self.assertEqual(paths, [["d"]])
|
||||
paths = list(graph.paths("e"))
|
||||
self.assertEqual(paths, [["e"]])
|
||||
|
||||
def test_graph_repr(self):
|
||||
graph = DelayableGraph(
|
||||
{"a": {"b"}, "b": {"c", "d"}, "c": {"e"}, "d": set(), "e": set()}
|
||||
)
|
||||
actual = repr(graph)
|
||||
expected = ["'a' → 'b' → 'c' → 'e'", "'a' → 'b' → 'd'"]
|
||||
self.assertEqual(sorted(actual.split("\n")), expected)
|
||||
|
||||
def test_graph_topological_sort(self):
|
||||
# the graph is an example from
|
||||
# https://en.wikipedia.org/wiki/Topological_sorting
|
||||
# if you want a visual representation
|
||||
graph = DelayableGraph(
|
||||
{
|
||||
5: {11},
|
||||
7: {11, 8},
|
||||
3: {8, 10},
|
||||
11: {2, 9, 10},
|
||||
2: set(),
|
||||
8: {9},
|
||||
9: set(),
|
||||
10: set(),
|
||||
}
|
||||
)
|
||||
|
||||
# these are all the pre-computed combinations that
|
||||
# respect the dependencies order
|
||||
valid_solutions = [
|
||||
[3, 5, 7, 8, 11, 2, 9, 10],
|
||||
[3, 5, 7, 8, 11, 2, 10, 9],
|
||||
[3, 5, 7, 8, 11, 9, 2, 10],
|
||||
[3, 5, 7, 8, 11, 9, 10, 2],
|
||||
[3, 5, 7, 8, 11, 10, 2, 9],
|
||||
[3, 5, 7, 8, 11, 10, 9, 2],
|
||||
[3, 5, 7, 11, 2, 8, 9, 10],
|
||||
[3, 5, 7, 11, 2, 8, 10, 9],
|
||||
[3, 5, 7, 11, 2, 10, 8, 9],
|
||||
[3, 5, 7, 11, 8, 2, 9, 10],
|
||||
[3, 5, 7, 11, 8, 2, 10, 9],
|
||||
[3, 5, 7, 11, 8, 9, 2, 10],
|
||||
[3, 5, 7, 11, 8, 9, 10, 2],
|
||||
[3, 5, 7, 11, 8, 10, 2, 9],
|
||||
[3, 5, 7, 11, 8, 10, 9, 2],
|
||||
[3, 5, 7, 11, 10, 2, 8, 9],
|
||||
[3, 5, 7, 11, 10, 8, 2, 9],
|
||||
[3, 5, 7, 11, 10, 8, 9, 2],
|
||||
[3, 7, 5, 8, 11, 2, 9, 10],
|
||||
[3, 7, 5, 8, 11, 2, 10, 9],
|
||||
[3, 7, 5, 8, 11, 9, 2, 10],
|
||||
[3, 7, 5, 8, 11, 9, 10, 2],
|
||||
[3, 7, 5, 8, 11, 10, 2, 9],
|
||||
[3, 7, 5, 8, 11, 10, 9, 2],
|
||||
[3, 7, 5, 11, 2, 8, 9, 10],
|
||||
[3, 7, 5, 11, 2, 8, 10, 9],
|
||||
[3, 7, 5, 11, 2, 10, 8, 9],
|
||||
[3, 7, 5, 11, 8, 2, 9, 10],
|
||||
[3, 7, 5, 11, 8, 2, 10, 9],
|
||||
[3, 7, 5, 11, 8, 9, 2, 10],
|
||||
[3, 7, 5, 11, 8, 9, 10, 2],
|
||||
[3, 7, 5, 11, 8, 10, 2, 9],
|
||||
[3, 7, 5, 11, 8, 10, 9, 2],
|
||||
[3, 7, 5, 11, 10, 2, 8, 9],
|
||||
[3, 7, 5, 11, 10, 8, 2, 9],
|
||||
[3, 7, 5, 11, 10, 8, 9, 2],
|
||||
[3, 7, 8, 5, 11, 2, 9, 10],
|
||||
[3, 7, 8, 5, 11, 2, 10, 9],
|
||||
[3, 7, 8, 5, 11, 9, 2, 10],
|
||||
[3, 7, 8, 5, 11, 9, 10, 2],
|
||||
[3, 7, 8, 5, 11, 10, 2, 9],
|
||||
[3, 7, 8, 5, 11, 10, 9, 2],
|
||||
[5, 3, 7, 8, 11, 2, 9, 10],
|
||||
[5, 3, 7, 8, 11, 2, 10, 9],
|
||||
[5, 3, 7, 8, 11, 9, 2, 10],
|
||||
[5, 3, 7, 8, 11, 9, 10, 2],
|
||||
[5, 3, 7, 8, 11, 10, 2, 9],
|
||||
[5, 3, 7, 8, 11, 10, 9, 2],
|
||||
[5, 3, 7, 11, 2, 8, 9, 10],
|
||||
[5, 3, 7, 11, 2, 8, 10, 9],
|
||||
[5, 3, 7, 11, 2, 10, 8, 9],
|
||||
[5, 3, 7, 11, 8, 2, 9, 10],
|
||||
[5, 3, 7, 11, 8, 2, 10, 9],
|
||||
[5, 3, 7, 11, 8, 9, 2, 10],
|
||||
[5, 3, 7, 11, 8, 9, 10, 2],
|
||||
[5, 3, 7, 11, 8, 10, 2, 9],
|
||||
[5, 3, 7, 11, 8, 10, 9, 2],
|
||||
[5, 3, 7, 11, 10, 2, 8, 9],
|
||||
[5, 3, 7, 11, 10, 8, 2, 9],
|
||||
[5, 3, 7, 11, 10, 8, 9, 2],
|
||||
[5, 7, 3, 8, 11, 2, 9, 10],
|
||||
[5, 7, 3, 8, 11, 2, 10, 9],
|
||||
[5, 7, 3, 8, 11, 9, 2, 10],
|
||||
[5, 7, 3, 8, 11, 9, 10, 2],
|
||||
[5, 7, 3, 8, 11, 10, 2, 9],
|
||||
[5, 7, 3, 8, 11, 10, 9, 2],
|
||||
[5, 7, 3, 11, 2, 8, 9, 10],
|
||||
[5, 7, 3, 11, 2, 8, 10, 9],
|
||||
[5, 7, 3, 11, 2, 10, 8, 9],
|
||||
[5, 7, 3, 11, 8, 2, 9, 10],
|
||||
[5, 7, 3, 11, 8, 2, 10, 9],
|
||||
[5, 7, 3, 11, 8, 9, 2, 10],
|
||||
[5, 7, 3, 11, 8, 9, 10, 2],
|
||||
[5, 7, 3, 11, 8, 10, 2, 9],
|
||||
[5, 7, 3, 11, 8, 10, 9, 2],
|
||||
[5, 7, 3, 11, 10, 2, 8, 9],
|
||||
[5, 7, 3, 11, 10, 8, 2, 9],
|
||||
[5, 7, 3, 11, 10, 8, 9, 2],
|
||||
[5, 7, 11, 2, 3, 8, 9, 10],
|
||||
[5, 7, 11, 2, 3, 8, 10, 9],
|
||||
[5, 7, 11, 2, 3, 10, 8, 9],
|
||||
[5, 7, 11, 3, 2, 8, 9, 10],
|
||||
[5, 7, 11, 3, 2, 8, 10, 9],
|
||||
[5, 7, 11, 3, 2, 10, 8, 9],
|
||||
[5, 7, 11, 3, 8, 2, 9, 10],
|
||||
[5, 7, 11, 3, 8, 2, 10, 9],
|
||||
[5, 7, 11, 3, 8, 9, 2, 10],
|
||||
[5, 7, 11, 3, 8, 9, 10, 2],
|
||||
[5, 7, 11, 3, 8, 10, 2, 9],
|
||||
[5, 7, 11, 3, 8, 10, 9, 2],
|
||||
[5, 7, 11, 3, 10, 2, 8, 9],
|
||||
[5, 7, 11, 3, 10, 8, 2, 9],
|
||||
[5, 7, 11, 3, 10, 8, 9, 2],
|
||||
[7, 3, 5, 8, 11, 2, 9, 10],
|
||||
[7, 3, 5, 8, 11, 2, 10, 9],
|
||||
[7, 3, 5, 8, 11, 9, 2, 10],
|
||||
[7, 3, 5, 8, 11, 9, 10, 2],
|
||||
[7, 3, 5, 8, 11, 10, 2, 9],
|
||||
[7, 3, 5, 8, 11, 10, 9, 2],
|
||||
[7, 3, 5, 11, 2, 8, 9, 10],
|
||||
[7, 3, 5, 11, 2, 8, 10, 9],
|
||||
[7, 3, 5, 11, 2, 10, 8, 9],
|
||||
[7, 3, 5, 11, 8, 2, 9, 10],
|
||||
[7, 3, 5, 11, 8, 2, 10, 9],
|
||||
[7, 3, 5, 11, 8, 9, 2, 10],
|
||||
[7, 3, 5, 11, 8, 9, 10, 2],
|
||||
[7, 3, 5, 11, 8, 10, 2, 9],
|
||||
[7, 3, 5, 11, 8, 10, 9, 2],
|
||||
[7, 3, 5, 11, 10, 2, 8, 9],
|
||||
[7, 3, 5, 11, 10, 8, 2, 9],
|
||||
[7, 3, 5, 11, 10, 8, 9, 2],
|
||||
[7, 3, 8, 5, 11, 2, 9, 10],
|
||||
[7, 3, 8, 5, 11, 2, 10, 9],
|
||||
[7, 3, 8, 5, 11, 9, 2, 10],
|
||||
[7, 3, 8, 5, 11, 9, 10, 2],
|
||||
[7, 3, 8, 5, 11, 10, 2, 9],
|
||||
[7, 3, 8, 5, 11, 10, 9, 2],
|
||||
[7, 5, 3, 8, 11, 2, 9, 10],
|
||||
[7, 5, 3, 8, 11, 2, 10, 9],
|
||||
[7, 5, 3, 8, 11, 9, 2, 10],
|
||||
[7, 5, 3, 8, 11, 9, 10, 2],
|
||||
[7, 5, 3, 8, 11, 10, 2, 9],
|
||||
[7, 5, 3, 8, 11, 10, 9, 2],
|
||||
[7, 5, 3, 11, 2, 8, 9, 10],
|
||||
[7, 5, 3, 11, 2, 8, 10, 9],
|
||||
[7, 5, 3, 11, 2, 10, 8, 9],
|
||||
[7, 5, 3, 11, 8, 2, 9, 10],
|
||||
[7, 5, 3, 11, 8, 2, 10, 9],
|
||||
[7, 5, 3, 11, 8, 9, 2, 10],
|
||||
[7, 5, 3, 11, 8, 9, 10, 2],
|
||||
[7, 5, 3, 11, 8, 10, 2, 9],
|
||||
[7, 5, 3, 11, 8, 10, 9, 2],
|
||||
[7, 5, 3, 11, 10, 2, 8, 9],
|
||||
[7, 5, 3, 11, 10, 8, 2, 9],
|
||||
[7, 5, 3, 11, 10, 8, 9, 2],
|
||||
[7, 5, 11, 2, 3, 8, 9, 10],
|
||||
[7, 5, 11, 2, 3, 8, 10, 9],
|
||||
[7, 5, 11, 2, 3, 10, 8, 9],
|
||||
[7, 5, 11, 3, 2, 8, 9, 10],
|
||||
[7, 5, 11, 3, 2, 8, 10, 9],
|
||||
[7, 5, 11, 3, 2, 10, 8, 9],
|
||||
[7, 5, 11, 3, 8, 2, 9, 10],
|
||||
[7, 5, 11, 3, 8, 2, 10, 9],
|
||||
[7, 5, 11, 3, 8, 9, 2, 10],
|
||||
[7, 5, 11, 3, 8, 9, 10, 2],
|
||||
[7, 5, 11, 3, 8, 10, 2, 9],
|
||||
[7, 5, 11, 3, 8, 10, 9, 2],
|
||||
[7, 5, 11, 3, 10, 2, 8, 9],
|
||||
[7, 5, 11, 3, 10, 8, 2, 9],
|
||||
[7, 5, 11, 3, 10, 8, 9, 2],
|
||||
]
|
||||
|
||||
self.assertIn(list(graph.topological_sort()), valid_solutions)
|
||||
@@ -1,93 +0,0 @@
|
||||
# Copyright 2024 Akretion (http://www.akretion.com).
|
||||
# @author Florian Mounier <florian.mounier@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.tests import common
|
||||
|
||||
from odoo.addons.queue_job.delay import Delayable
|
||||
|
||||
|
||||
class TestDelayableSplit(common.BaseCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
class FakeRecordSet(list):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._name = "recordset"
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, slice):
|
||||
return FakeRecordSet(super().__getitem__(key))
|
||||
return super().__getitem__(key)
|
||||
|
||||
def method(self, arg, kwarg=None):
|
||||
"""Method to be called"""
|
||||
return arg, kwarg
|
||||
|
||||
self.FakeRecordSet = FakeRecordSet
|
||||
|
||||
def test_delayable_split_no_method_call_beforehand(self):
|
||||
dl = Delayable(self.FakeRecordSet(range(20)))
|
||||
with self.assertRaises(ValueError):
|
||||
dl.split(3)
|
||||
|
||||
def test_delayable_split_10_3(self):
|
||||
dl = Delayable(self.FakeRecordSet(range(10)))
|
||||
dl.method("arg", kwarg="kwarg")
|
||||
group = dl.split(3)
|
||||
self.assertEqual(len(group._delayables), 4)
|
||||
delayables = sorted(list(group._delayables), key=lambda x: x.description)
|
||||
self.assertEqual(delayables[0].recordset, self.FakeRecordSet([0, 1, 2]))
|
||||
self.assertEqual(delayables[1].recordset, self.FakeRecordSet([3, 4, 5]))
|
||||
self.assertEqual(delayables[2].recordset, self.FakeRecordSet([6, 7, 8]))
|
||||
self.assertEqual(delayables[3].recordset, self.FakeRecordSet([9]))
|
||||
self.assertEqual(delayables[0].description, "Method to be called (split 1/4)")
|
||||
self.assertEqual(delayables[1].description, "Method to be called (split 2/4)")
|
||||
self.assertEqual(delayables[2].description, "Method to be called (split 3/4)")
|
||||
self.assertEqual(delayables[3].description, "Method to be called (split 4/4)")
|
||||
self.assertNotEqual(delayables[0]._job_method, dl._job_method)
|
||||
self.assertNotEqual(delayables[1]._job_method, dl._job_method)
|
||||
self.assertNotEqual(delayables[2]._job_method, dl._job_method)
|
||||
self.assertNotEqual(delayables[3]._job_method, dl._job_method)
|
||||
self.assertEqual(delayables[0]._job_method.__name__, dl._job_method.__name__)
|
||||
self.assertEqual(delayables[1]._job_method.__name__, dl._job_method.__name__)
|
||||
self.assertEqual(delayables[2]._job_method.__name__, dl._job_method.__name__)
|
||||
self.assertEqual(delayables[3]._job_method.__name__, dl._job_method.__name__)
|
||||
self.assertEqual(delayables[0]._job_args, ("arg",))
|
||||
self.assertEqual(delayables[1]._job_args, ("arg",))
|
||||
self.assertEqual(delayables[2]._job_args, ("arg",))
|
||||
self.assertEqual(delayables[3]._job_args, ("arg",))
|
||||
self.assertEqual(delayables[0]._job_kwargs, {"kwarg": "kwarg"})
|
||||
self.assertEqual(delayables[1]._job_kwargs, {"kwarg": "kwarg"})
|
||||
self.assertEqual(delayables[2]._job_kwargs, {"kwarg": "kwarg"})
|
||||
self.assertEqual(delayables[3]._job_kwargs, {"kwarg": "kwarg"})
|
||||
|
||||
def test_delayable_split_10_5(self):
|
||||
dl = Delayable(self.FakeRecordSet(range(10)))
|
||||
dl.method("arg", kwarg="kwarg")
|
||||
group = dl.split(5)
|
||||
self.assertEqual(len(group._delayables), 2)
|
||||
delayables = sorted(list(group._delayables), key=lambda x: x.description)
|
||||
self.assertEqual(delayables[0].recordset, self.FakeRecordSet([0, 1, 2, 3, 4]))
|
||||
self.assertEqual(delayables[1].recordset, self.FakeRecordSet([5, 6, 7, 8, 9]))
|
||||
self.assertEqual(delayables[0].description, "Method to be called (split 1/2)")
|
||||
self.assertEqual(delayables[1].description, "Method to be called (split 2/2)")
|
||||
|
||||
def test_delayable_split_10_10(self):
|
||||
dl = Delayable(self.FakeRecordSet(range(10)))
|
||||
dl.method("arg", kwarg="kwarg")
|
||||
group = dl.split(10)
|
||||
self.assertEqual(len(group._delayables), 1)
|
||||
delayables = sorted(list(group._delayables), key=lambda x: x.description)
|
||||
self.assertEqual(delayables[0].recordset, self.FakeRecordSet(range(10)))
|
||||
self.assertEqual(delayables[0].description, "Method to be called (split 1/1)")
|
||||
|
||||
def test_delayable_split_10_20(self):
|
||||
dl = Delayable(self.FakeRecordSet(range(10)))
|
||||
dl.method("arg", kwarg="kwarg")
|
||||
group = dl.split(20)
|
||||
self.assertEqual(len(group._delayables), 1)
|
||||
delayables = sorted(list(group._delayables), key=lambda x: x.description)
|
||||
self.assertEqual(delayables[0].recordset, self.FakeRecordSet(range(10)))
|
||||
self.assertEqual(delayables[0].description, "Method to be called (split 1/1)")
|
||||
@@ -1,154 +0,0 @@
|
||||
# copyright 2016 Camptocamp
|
||||
# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo.tests import common
|
||||
|
||||
# pylint: disable=odoo-addons-relative-import
|
||||
# we are testing, we want to test as we were an external consumer of the API
|
||||
from odoo.addons.queue_job.fields import JobDecoder, JobEncoder
|
||||
|
||||
|
||||
class TestJson(common.TransactionCase):
|
||||
def test_encoder_recordset(self):
|
||||
demo_user = self.env.ref("base.user_demo")
|
||||
context = demo_user.context_get()
|
||||
partner = self.env(user=demo_user, context=context).ref("base.main_partner")
|
||||
value = partner
|
||||
value_json = json.dumps(value, cls=JobEncoder)
|
||||
expected_context = context.copy()
|
||||
expected_context.pop("uid")
|
||||
expected = {
|
||||
"uid": demo_user.id,
|
||||
"_type": "odoo_recordset",
|
||||
"model": "res.partner",
|
||||
"ids": [partner.id],
|
||||
"su": False,
|
||||
"context": expected_context,
|
||||
}
|
||||
self.assertEqual(json.loads(value_json), expected)
|
||||
|
||||
def test_encoder_recordset_list(self):
|
||||
demo_user = self.env.ref("base.user_demo")
|
||||
context = demo_user.context_get()
|
||||
partner = self.env(user=demo_user, context=context).ref("base.main_partner")
|
||||
value = ["a", 1, partner]
|
||||
value_json = json.dumps(value, cls=JobEncoder)
|
||||
expected_context = context.copy()
|
||||
expected_context.pop("uid")
|
||||
expected = [
|
||||
"a",
|
||||
1,
|
||||
{
|
||||
"uid": demo_user.id,
|
||||
"_type": "odoo_recordset",
|
||||
"model": "res.partner",
|
||||
"ids": [partner.id],
|
||||
"su": False,
|
||||
"context": expected_context,
|
||||
},
|
||||
]
|
||||
self.assertEqual(json.loads(value_json), expected)
|
||||
|
||||
def test_decoder_recordset(self):
|
||||
demo_user = self.env.ref("base.user_demo")
|
||||
context = demo_user.context_get()
|
||||
partner = self.env(user=demo_user).ref("base.main_partner")
|
||||
value_json = (
|
||||
'{"_type": "odoo_recordset",'
|
||||
'"model": "res.partner",'
|
||||
'"su": false,'
|
||||
'"ids": [%s],"uid": %s, '
|
||||
'"context": {"tz": "%s", "lang": "%s"}}'
|
||||
% (partner.id, demo_user.id, context["tz"], context["lang"])
|
||||
)
|
||||
expected = partner
|
||||
value = json.loads(value_json, cls=JobDecoder, env=self.env)
|
||||
self.assertEqual(value, expected)
|
||||
self.assertEqual(demo_user, expected.env.user)
|
||||
|
||||
def test_decoder_recordset_list(self):
|
||||
demo_user = self.env.ref("base.user_demo")
|
||||
context = demo_user.context_get()
|
||||
partner = self.env(user=demo_user).ref("base.main_partner")
|
||||
value_json = (
|
||||
'["a", 1, '
|
||||
'{"_type": "odoo_recordset",'
|
||||
'"model": "res.partner",'
|
||||
'"su": false,'
|
||||
'"ids": [%s],"uid": %s, '
|
||||
'"context": {"tz": "%s", "lang": "%s"}}]'
|
||||
% (partner.id, demo_user.id, context["tz"], context["lang"])
|
||||
)
|
||||
expected = ["a", 1, partner]
|
||||
value = json.loads(value_json, cls=JobDecoder, env=self.env)
|
||||
self.assertEqual(value, expected)
|
||||
self.assertEqual(demo_user, expected[2].env.user)
|
||||
|
||||
def test_decoder_recordset_list_without_user(self):
|
||||
value_json = (
|
||||
'["a", 1, {"_type": "odoo_recordset",' '"model": "res.users", "ids": [1]}]'
|
||||
)
|
||||
expected = ["a", 1, self.env.ref("base.user_root")]
|
||||
value = json.loads(value_json, cls=JobDecoder, env=self.env)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
def test_encoder_datetime(self):
|
||||
value = ["a", 1, datetime(2017, 4, 19, 8, 48, 50, 1)]
|
||||
value_json = json.dumps(value, cls=JobEncoder)
|
||||
expected = [
|
||||
"a",
|
||||
1,
|
||||
{"_type": "datetime_isoformat", "value": "2017-04-19T08:48:50.000001"},
|
||||
]
|
||||
self.assertEqual(json.loads(value_json), expected)
|
||||
|
||||
def test_decoder_datetime(self):
|
||||
value_json = (
|
||||
'["a", 1, {"_type": "datetime_isoformat",'
|
||||
'"value": "2017-04-19T08:48:50.000001"}]'
|
||||
)
|
||||
expected = ["a", 1, datetime(2017, 4, 19, 8, 48, 50, 1)]
|
||||
value = json.loads(value_json, cls=JobDecoder, env=self.env)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
def test_encoder_date(self):
|
||||
value = ["a", 1, date(2017, 4, 19)]
|
||||
value_json = json.dumps(value, cls=JobEncoder)
|
||||
expected = ["a", 1, {"_type": "date_isoformat", "value": "2017-04-19"}]
|
||||
self.assertEqual(json.loads(value_json), expected)
|
||||
|
||||
def test_decoder_date(self):
|
||||
value_json = '["a", 1, {"_type": "date_isoformat",' '"value": "2017-04-19"}]'
|
||||
expected = ["a", 1, date(2017, 4, 19)]
|
||||
value = json.loads(value_json, cls=JobDecoder, env=self.env)
|
||||
self.assertEqual(value, expected)
|
||||
|
||||
def test_encoder_etree(self):
|
||||
etree_el = etree.Element("root", attr="val")
|
||||
etree_el.append(etree.Element("child", attr="val"))
|
||||
value = ["a", 1, etree_el]
|
||||
value_json = json.dumps(value, cls=JobEncoder)
|
||||
expected = [
|
||||
"a",
|
||||
1,
|
||||
{
|
||||
"_type": "etree_element",
|
||||
"value": '<root attr="val"><child attr="val"/></root>',
|
||||
},
|
||||
]
|
||||
self.assertEqual(json.loads(value_json), expected)
|
||||
|
||||
def test_decoder_etree(self):
|
||||
value_json = '["a", 1, {"_type": "etree_element", "value": \
|
||||
"<root attr=\\"val\\"><child attr=\\"val\\"/></root>"}]'
|
||||
etree_el = etree.Element("root", attr="val")
|
||||
etree_el.append(etree.Element("child", attr="val"))
|
||||
expected = ["a", 1, etree.tostring(etree_el)]
|
||||
value = json.loads(value_json, cls=JobDecoder, env=self.env)
|
||||
value[2] = etree.tostring(value[2])
|
||||
self.assertEqual(value, expected)
|
||||
@@ -1,59 +0,0 @@
|
||||
# copyright 2018 Camptocamp
|
||||
# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
import odoo
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestJobChannel(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.Channel = self.env["queue.job.channel"]
|
||||
self.root_channel = self.Channel.search([("name", "=", "root")])
|
||||
|
||||
def test_channel_new(self):
|
||||
channel = self.Channel.new()
|
||||
self.assertFalse(channel.name)
|
||||
self.assertFalse(channel.complete_name)
|
||||
|
||||
def test_channel_create(self):
|
||||
channel = self.Channel.create(
|
||||
{"name": "test", "parent_id": self.root_channel.id}
|
||||
)
|
||||
self.assertEqual(channel.name, "test")
|
||||
self.assertEqual(channel.complete_name, "root.test")
|
||||
channel2 = self.Channel.create({"name": "test", "parent_id": channel.id})
|
||||
self.assertEqual(channel2.name, "test")
|
||||
self.assertEqual(channel2.complete_name, "root.test.test")
|
||||
|
||||
@odoo.tools.mute_logger("odoo.sql_db")
|
||||
def test_channel_complete_name_uniq(self):
|
||||
channel = self.Channel.create(
|
||||
{"name": "test", "parent_id": self.root_channel.id}
|
||||
)
|
||||
self.assertEqual(channel.name, "test")
|
||||
self.assertEqual(channel.complete_name, "root.test")
|
||||
|
||||
self.Channel.create({"name": "test", "parent_id": self.root_channel.id})
|
||||
|
||||
# Flush process all the pending recomputations (or at least the
|
||||
# given field and flush the pending updates to the database.
|
||||
# It is normally called on commit.
|
||||
|
||||
# The context manager 'with self.assertRaises(IntegrityError)' purposefully
|
||||
# not uses here due to its 'flush_all()' method inside it and exception raises
|
||||
# before the line 'self.env.flush_all()'. So, we are expecting an IntegrityError.
|
||||
try:
|
||||
self.env.flush_all()
|
||||
except IntegrityError as ex:
|
||||
self.assertIn("queue_job_channel_name_uniq", ex.pgerror)
|
||||
else:
|
||||
self.assertEqual(True, False)
|
||||
|
||||
def test_channel_name_get(self):
|
||||
channel = self.Channel.create(
|
||||
{"name": "test", "parent_id": self.root_channel.id}
|
||||
)
|
||||
self.assertEqual(channel.name_get(), [(channel.id, "root.test")])
|
||||
@@ -1,57 +0,0 @@
|
||||
# copyright 2020 Camptocamp
|
||||
# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestJobFunction(common.TransactionCase):
|
||||
def test_function_name_compute(self):
|
||||
function = self.env["queue.job.function"].create(
|
||||
{"model_id": self.env.ref("base.model_res_users").id, "method": "read"}
|
||||
)
|
||||
self.assertEqual(function.name, "<res.users>.read")
|
||||
|
||||
def test_function_name_inverse(self):
|
||||
function = self.env["queue.job.function"].create({"name": "<res.users>.read"})
|
||||
self.assertEqual(function.model_id.model, "res.users")
|
||||
self.assertEqual(function.method, "read")
|
||||
|
||||
def test_function_name_inverse_invalid_regex(self):
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
self.env["queue.job.function"].create({"name": "<res.users.read"})
|
||||
|
||||
def test_function_name_inverse_model_not_found(self):
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
self.env["queue.job.function"].create(
|
||||
{"name": "<this.model.does.not.exist>.read"}
|
||||
)
|
||||
|
||||
def test_function_job_config(self):
|
||||
channel = self.env["queue.job.channel"].create(
|
||||
{"name": "foo", "parent_id": self.env.ref("queue_job.channel_root").id}
|
||||
)
|
||||
job_function = self.env["queue.job.function"].create(
|
||||
{
|
||||
"model_id": self.env.ref("base.model_res_users").id,
|
||||
"method": "read",
|
||||
"channel_id": channel.id,
|
||||
"edit_retry_pattern": "{1: 2, 3: 4}",
|
||||
"edit_related_action": (
|
||||
'{"enable": True,'
|
||||
' "func_name": "related_action_foo",'
|
||||
' "kwargs": {"b": 1}}'
|
||||
),
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.env["queue.job.function"].job_config("<res.users>.read"),
|
||||
self.env["queue.job.function"].JobConfig(
|
||||
channel="root.foo",
|
||||
retry_pattern={1: 2, 3: 4},
|
||||
related_action_enable=True,
|
||||
related_action_func_name="related_action_foo",
|
||||
related_action_kwargs={"b": 1},
|
||||
job_function_id=job_function.id,
|
||||
),
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
# copyright 2020 Camptocamp
|
||||
# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestJobWriteProtected(common.TransactionCase):
|
||||
def test_create_error(self):
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
self.env["queue.job"].create(
|
||||
{"uuid": "test", "model_name": "res.partner", "method_name": "write"}
|
||||
)
|
||||
|
||||
def test_write_protected_field_error(self):
|
||||
job_ = self.env["res.partner"].with_delay().create({"name": "test"})
|
||||
db_job = job_.db_record()
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
db_job.method_name = "unlink"
|
||||
|
||||
def test_write_allow_no_protected_field_error(self):
|
||||
job_ = self.env["res.partner"].with_delay().create({"name": "test"})
|
||||
db_job = job_.db_record()
|
||||
db_job.priority = 30
|
||||
self.assertEqual(db_job.priority, 30)
|
||||
@@ -1,133 +0,0 @@
|
||||
# Copyright 2025 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from contextlib import closing
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from odoo.addons.queue_job.job import Job
|
||||
from odoo.addons.queue_job.jobrunner.runner import Database
|
||||
|
||||
|
||||
class TestRequeueDeadJob(TransactionCase):
|
||||
def create_dummy_job(self, uuid):
|
||||
"""
|
||||
Create dummy job for tests
|
||||
"""
|
||||
return (
|
||||
self.env["queue.job"]
|
||||
.with_context(
|
||||
_job_edit_sentinel=self.env["queue.job"].EDIT_SENTINEL,
|
||||
)
|
||||
.create(
|
||||
{
|
||||
"uuid": uuid,
|
||||
"user_id": self.env.user.id,
|
||||
"state": "pending",
|
||||
"model_name": "queue.job",
|
||||
"method_name": "write",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def get_locks(self, uuid, cr=None):
|
||||
"""
|
||||
Retrieve lock rows
|
||||
"""
|
||||
if cr is None:
|
||||
cr = self.env.cr
|
||||
|
||||
cr.execute(
|
||||
"""
|
||||
SELECT
|
||||
queue_job_id
|
||||
FROM
|
||||
queue_job_lock
|
||||
WHERE
|
||||
queue_job_id IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
queue_job
|
||||
WHERE
|
||||
uuid = %s
|
||||
)
|
||||
FOR UPDATE SKIP LOCKED
|
||||
""",
|
||||
[uuid],
|
||||
)
|
||||
|
||||
return cr.fetchall()
|
||||
|
||||
def test_add_lock_record(self):
|
||||
queue_job = self.create_dummy_job("test_add_lock")
|
||||
job_obj = Job.load(self.env, queue_job.uuid)
|
||||
|
||||
job_obj.set_started()
|
||||
self.assertEqual(job_obj.state, "started")
|
||||
|
||||
locks = self.get_locks(job_obj.uuid)
|
||||
|
||||
self.assertEqual(1, len(locks))
|
||||
|
||||
def test_lock(self):
|
||||
queue_job = self.create_dummy_job("test_lock")
|
||||
job_obj = Job.load(self.env, queue_job.uuid)
|
||||
|
||||
job_obj.set_started()
|
||||
job_obj.store()
|
||||
|
||||
locks = self.get_locks(job_obj.uuid)
|
||||
|
||||
self.assertEqual(1, len(locks))
|
||||
|
||||
# commit to update queue_job records in DB
|
||||
self.env.cr.commit() # pylint: disable=E8102
|
||||
|
||||
job_obj.lock()
|
||||
|
||||
with closing(self.env.registry.cursor()) as new_cr:
|
||||
locks = self.get_locks(job_obj.uuid, new_cr)
|
||||
|
||||
# Row should be locked
|
||||
self.assertEqual(0, len(locks))
|
||||
|
||||
# clean up
|
||||
queue_job.unlink()
|
||||
|
||||
self.env.cr.commit() # pylint: disable=E8102
|
||||
|
||||
# because we committed the cursor, the savepoint of the test method is
|
||||
# gone, and this would break TransactionCase cleanups
|
||||
self.cr.execute("SAVEPOINT test_%d" % self._savepoint_id)
|
||||
|
||||
def test_requeue_dead_jobs(self):
|
||||
uuid = "test_requeue_dead_jobs"
|
||||
|
||||
queue_job = self.create_dummy_job(uuid)
|
||||
job_obj = Job.load(self.env, queue_job.uuid)
|
||||
|
||||
job_obj.set_enqueued()
|
||||
# simulate enqueuing was in the past
|
||||
job_obj.date_enqueued = datetime.now() - timedelta(minutes=1)
|
||||
job_obj.set_started()
|
||||
|
||||
job_obj.store()
|
||||
self.env.cr.commit() # pylint: disable=E8102
|
||||
|
||||
# requeue dead jobs using current cursor
|
||||
query = Database(self.env.cr.dbname)._query_requeue_dead_jobs()
|
||||
self.env.cr.execute(query)
|
||||
|
||||
uuids_requeued = self.env.cr.fetchall()
|
||||
|
||||
self.assertEqual(len(uuids_requeued), 1)
|
||||
self.assertEqual(uuids_requeued[0][0], uuid)
|
||||
|
||||
# clean up
|
||||
queue_job.unlink()
|
||||
self.env.cr.commit() # pylint: disable=E8102
|
||||
|
||||
# because we committed the cursor, the savepoint of the test method is
|
||||
# gone, and this would break TransactionCase cleanups
|
||||
self.cr.execute("SAVEPOINT test_%d" % self._savepoint_id)
|
||||
@@ -1,17 +0,0 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from ..controllers.main import RunJobController
|
||||
from ..job import Job
|
||||
|
||||
|
||||
class TestRunJobController(TransactionCase):
|
||||
def test_get_failure_values(self):
|
||||
method = self.env["res.users"].mapped
|
||||
job = Job(method)
|
||||
ctrl = RunJobController()
|
||||
rslt = ctrl._get_failure_values(job, "info", Exception("zero", "one"))
|
||||
self.assertEqual(
|
||||
rslt, {"exc_info": "info", "exc_name": "Exception", "exc_message": "zero"}
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
# Copyright 2015-2016 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
# pylint: disable=odoo-addons-relative-import
|
||||
# we are testing, we want to test as we were an external consumer of the API
|
||||
from odoo.addons.queue_job.jobrunner import channels
|
||||
|
||||
from .common import load_doctests
|
||||
|
||||
load_tests = load_doctests(channels)
|
||||
@@ -1,59 +0,0 @@
|
||||
# Copyright 2015-2016 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
# pylint: disable=odoo-addons-relative-import
|
||||
# we are testing, we want to test as we were an external consumer of the API
|
||||
import os
|
||||
|
||||
from odoo.tests import BaseCase, tagged
|
||||
|
||||
from odoo.addons.queue_job.jobrunner import runner
|
||||
|
||||
from .common import load_doctests
|
||||
|
||||
load_tests = load_doctests(runner)
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestRunner(BaseCase):
|
||||
@classmethod
|
||||
def _is_open_file_descriptor(cls, fd):
|
||||
try:
|
||||
os.fstat(fd)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def test_runner_file_descriptor(self):
|
||||
a_runner = runner.QueueJobRunner.from_environ_or_config()
|
||||
|
||||
read_fd, write_fd = a_runner._stop_pipe
|
||||
self.assertTrue(self._is_open_file_descriptor(read_fd))
|
||||
self.assertTrue(self._is_open_file_descriptor(write_fd))
|
||||
|
||||
del a_runner
|
||||
|
||||
self.assertFalse(self._is_open_file_descriptor(read_fd))
|
||||
self.assertFalse(self._is_open_file_descriptor(write_fd))
|
||||
|
||||
def test_runner_file_closed_read_descriptor(self):
|
||||
a_runner = runner.QueueJobRunner.from_environ_or_config()
|
||||
|
||||
read_fd, write_fd = a_runner._stop_pipe
|
||||
os.close(read_fd)
|
||||
|
||||
del a_runner
|
||||
|
||||
self.assertFalse(self._is_open_file_descriptor(read_fd))
|
||||
self.assertFalse(self._is_open_file_descriptor(write_fd))
|
||||
|
||||
def test_runner_file_closed_write_descriptor(self):
|
||||
a_runner = runner.QueueJobRunner.from_environ_or_config()
|
||||
|
||||
read_fd, write_fd = a_runner._stop_pipe
|
||||
os.close(write_fd)
|
||||
|
||||
del a_runner
|
||||
|
||||
self.assertFalse(self._is_open_file_descriptor(read_fd))
|
||||
self.assertFalse(self._is_open_file_descriptor(write_fd))
|
||||
@@ -1,48 +0,0 @@
|
||||
# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestWizards(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.job = (
|
||||
self.env["queue.job"]
|
||||
.with_context(
|
||||
_job_edit_sentinel=self.env["queue.job"].EDIT_SENTINEL,
|
||||
)
|
||||
.create(
|
||||
{
|
||||
"uuid": "test",
|
||||
"user_id": self.env.user.id,
|
||||
"state": "failed",
|
||||
"model_name": "queue.job",
|
||||
"method_name": "write",
|
||||
"args": (),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def _wizard(self, model_name):
|
||||
return (
|
||||
self.env[model_name]
|
||||
.with_context(
|
||||
active_model=self.job._name,
|
||||
active_ids=self.job.ids,
|
||||
)
|
||||
.create({})
|
||||
)
|
||||
|
||||
def test_01_requeue(self):
|
||||
wizard = self._wizard("queue.requeue.job")
|
||||
wizard.requeue()
|
||||
self.assertEqual(self.job.state, "pending")
|
||||
|
||||
def test_02_cancel(self):
|
||||
wizard = self._wizard("queue.jobs.to.cancelled")
|
||||
wizard.set_cancelled()
|
||||
self.assertEqual(self.job.state, "cancelled")
|
||||
|
||||
def test_03_done(self):
|
||||
wizard = self._wizard("queue.jobs.to.done")
|
||||
wizard.set_done()
|
||||
self.assertEqual(self.job.state, "done")
|
||||
@@ -1,40 +0,0 @@
|
||||
# Copyright 2023 Camptocamp
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def must_run_without_delay(env):
|
||||
"""Retrun true if jobs have to run immediately.
|
||||
|
||||
:param env: `odoo.api.Environment` instance
|
||||
"""
|
||||
# TODO: drop in v17
|
||||
if os.getenv("TEST_QUEUE_JOB_NO_DELAY"):
|
||||
_logger.warning(
|
||||
"`TEST_QUEUE_JOB_NO_DELAY` env var found. NO JOB scheduled. "
|
||||
"Note that this key is deprecated: please use `QUEUE_JOB__NO_DELAY`"
|
||||
)
|
||||
return True
|
||||
|
||||
if os.getenv("QUEUE_JOB__NO_DELAY"):
|
||||
_logger.warning("`QUEUE_JOB__NO_DELAY` env var found. NO JOB scheduled.")
|
||||
return True
|
||||
|
||||
# TODO: drop in v17
|
||||
deprecated_keys = ("_job_force_sync", "test_queue_job_no_delay")
|
||||
for key in deprecated_keys:
|
||||
if env.context.get(key):
|
||||
_logger.warning(
|
||||
"`%s` ctx key found. NO JOB scheduled. "
|
||||
"Note that this key is deprecated: please use `queue_job__no_delay`",
|
||||
key,
|
||||
)
|
||||
return True
|
||||
|
||||
if env.context.get("queue_job__no_delay"):
|
||||
_logger.info("`queue_job__no_delay` ctx key found. NO JOB scheduled.")
|
||||
return True
|
||||
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_queue_job_channel_form" model="ir.ui.view">
|
||||
<field name="name">queue.job.channel.form</field>
|
||||
<field name="model">queue.job.channel</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Channels">
|
||||
<group>
|
||||
<field
|
||||
name="name"
|
||||
attrs="{'required': [('name', '!=', 'root')], 'readonly': [('name', '=', 'root')]}"
|
||||
/>
|
||||
<field
|
||||
name="parent_id"
|
||||
attrs="{'required': [('name', '!=', 'root')], 'readonly': [('name', '=', 'root')]}"
|
||||
/>
|
||||
<field name="complete_name" />
|
||||
<field name="removal_interval" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="job_function_ids" widget="many2many_tags" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_queue_job_channel_tree" model="ir.ui.view">
|
||||
<field name="name">queue.job.channel.tree</field>
|
||||
<field name="model">queue.job.channel</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="complete_name" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_queue_job_channel_search" model="ir.ui.view">
|
||||
<field name="name">queue.job.channel.search</field>
|
||||
<field name="model">queue.job.channel</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Channels">
|
||||
<field name="name" />
|
||||
<field name="complete_name" />
|
||||
<field name="parent_id" />
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_queue_job_channel" model="ir.actions.act_window">
|
||||
<field name="name">Channels</field>
|
||||
<field name="res_model">queue.job.channel</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="view_id" ref="view_queue_job_channel_tree" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_queue_job_function_form" model="ir.ui.view">
|
||||
<field name="name">queue.job.function.form</field>
|
||||
<field name="model">queue.job.function</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Job Functions">
|
||||
<group>
|
||||
<field name="name" readonly="1" />
|
||||
<field name="model_id" required="1" />
|
||||
<field name="method" required="1" />
|
||||
<field name="channel_id" />
|
||||
<field name="edit_retry_pattern" widget="ace" />
|
||||
<field name="edit_related_action" widget="ace" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_queue_job_function_tree" model="ir.ui.view">
|
||||
<field name="name">queue.job.function.tree</field>
|
||||
<field name="model">queue.job.function</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="channel_id" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_queue_job_function_search" model="ir.ui.view">
|
||||
<field name="name">queue.job.function.search</field>
|
||||
<field name="model">queue.job.function</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Job Functions">
|
||||
<field name="name" />
|
||||
<field name="channel_id" />
|
||||
<group expand="0" string="Group By">
|
||||
<filter
|
||||
name="group_by_channel"
|
||||
string="Channel"
|
||||
context="{'group_by': 'channel_id'}"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_queue_job_function" model="ir.actions.act_window">
|
||||
<field name="name">Job Functions</field>
|
||||
<field name="res_model">queue.job.function</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="view_id" ref="view_queue_job_function_tree" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<menuitem
|
||||
id="menu_queue_job_root"
|
||||
name="Job Queue"
|
||||
web_icon="queue_job,static/description/icon.png"
|
||||
groups="group_queue_job_manager"
|
||||
/>
|
||||
|
||||
<menuitem id="menu_queue" name="Queue" parent="menu_queue_job_root" />
|
||||
|
||||
<menuitem
|
||||
id="menu_queue_job"
|
||||
action="action_queue_job"
|
||||
sequence="10"
|
||||
parent="menu_queue"
|
||||
/>
|
||||
|
||||
<menuitem
|
||||
id="menu_queue_job_channel"
|
||||
action="action_queue_job_channel"
|
||||
sequence="12"
|
||||
parent="menu_queue"
|
||||
/>
|
||||
|
||||
<menuitem
|
||||
id="menu_queue_job_function"
|
||||
action="action_queue_job_function"
|
||||
sequence="14"
|
||||
parent="menu_queue"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
@@ -1,343 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_queue_job_form" model="ir.ui.view">
|
||||
<field name="name">queue.job.form</field>
|
||||
<field name="model">queue.job</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Jobs" create="false" delete="false">
|
||||
<header>
|
||||
<button
|
||||
name="requeue"
|
||||
states="failed"
|
||||
class="oe_highlight"
|
||||
string="Requeue Job"
|
||||
type="object"
|
||||
groups="queue_job.group_queue_job_manager"
|
||||
/>
|
||||
<button
|
||||
name="button_done"
|
||||
states="wait_dependencies,pending,enqueued,failed"
|
||||
class="oe_highlight"
|
||||
string="Set to 'Done'"
|
||||
type="object"
|
||||
groups="queue_job.group_queue_job_manager"
|
||||
/>
|
||||
<button
|
||||
name="button_cancelled"
|
||||
states="pending,enqueued,failed"
|
||||
class="oe_highlight"
|
||||
string="Cancel job"
|
||||
type="object"
|
||||
groups="queue_job.group_queue_job_manager"
|
||||
/>
|
||||
<button name="open_related_action" string="Related" type="object" />
|
||||
<field
|
||||
name="state"
|
||||
widget="statusbar"
|
||||
statusbar_visible="wait_dependencies,pending,enqueued,started,done"
|
||||
statusbar_colors='{"failed":"red","done":"green","cancelled":"yellow"}'
|
||||
/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
name="open_graph_jobs"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-sitemap"
|
||||
attrs="{'invisible':[('graph_uuid', '=', False)]}"
|
||||
>
|
||||
<field
|
||||
name="graph_jobs_count"
|
||||
widget="statinfo"
|
||||
string="Graph Jobs"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<h1>
|
||||
<field name="name" class="oe_inline" />
|
||||
</h1>
|
||||
<group>
|
||||
<field name="uuid" />
|
||||
<field name="graph_uuid" />
|
||||
<field name="func_string" groups="base.group_no_one" />
|
||||
<field name="job_function_id" />
|
||||
<field name="channel" />
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="priority" />
|
||||
<field name="eta" />
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<field name="user_id" />
|
||||
<field name="worker_pid" groups="base.group_no_one" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_created" />
|
||||
<field name="date_enqueued" groups="base.group_no_one" />
|
||||
<field name="date_started" />
|
||||
<field name="date_done" />
|
||||
<!-- Do not use float_time as it does not work properly -->
|
||||
<field name="exec_time" string="Time (s)" />
|
||||
</group>
|
||||
</group>
|
||||
<group colspan="4">
|
||||
<div colspan="2">
|
||||
<label for="retry" string="Current try / max. retries" />
|
||||
<field name="retry" class="oe_inline" /> /
|
||||
<field name="max_retries" class="oe_inline" />
|
||||
<br />
|
||||
<span
|
||||
class="oe_grey oe_inline"
|
||||
> If the max. retries is 0, the number of retries is infinite.</span>
|
||||
</div>
|
||||
</group>
|
||||
<notebook>
|
||||
<page
|
||||
name="results"
|
||||
string="Results"
|
||||
attrs="{'invisible': [('result', '=', False), ('exc_info', '=', False)]}"
|
||||
>
|
||||
<group
|
||||
name="result"
|
||||
string="Result"
|
||||
attrs="{'invisible': [('result', '=', False)]}"
|
||||
>
|
||||
<div id="result" colspan="2">
|
||||
<field nolabel="1" name="result" />
|
||||
</div>
|
||||
</group>
|
||||
<group
|
||||
name="exc_info"
|
||||
string="Exception Information"
|
||||
attrs="{'invisible': [('exc_info', '=', False)]}"
|
||||
colspan="4"
|
||||
>
|
||||
<div id="exc_name" colspan="4">
|
||||
<label for="exc_name" string="Exception:" />
|
||||
<field name="exc_name" class="oe_inline" />
|
||||
</div>
|
||||
<field colspan="2" nolabel="1" name="exc_info" />
|
||||
</group>
|
||||
</page>
|
||||
<page
|
||||
name="dependencies"
|
||||
string="Dependencies"
|
||||
attrs="{'invisible': [('graph_uuid', '=', False)]}"
|
||||
>
|
||||
<field
|
||||
nolabel="1"
|
||||
name="dependency_graph"
|
||||
widget="job_directed_graph"
|
||||
/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers" />
|
||||
<field name="activity_ids" widget="mail_activity" />
|
||||
<field name="message_ids" widget="mail_thread" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_queue_job_tree" model="ir.ui.view">
|
||||
<field name="name">queue.job.tree</field>
|
||||
<field name="model">queue.job</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree
|
||||
create="false"
|
||||
delete="false"
|
||||
decoration-danger="state == 'failed'"
|
||||
decoration-muted="state == 'done'"
|
||||
>
|
||||
<field name="name" />
|
||||
<field name="model_name" optional="show" />
|
||||
<field name="state" />
|
||||
<field name="date_created" />
|
||||
<field
|
||||
name="eta"
|
||||
widget="remaining_days"
|
||||
string="Remaining days to execute"
|
||||
optional="hide"
|
||||
/>
|
||||
<field name="date_done" optional="show" />
|
||||
<field name="exec_time" optional="show" />
|
||||
<field name="priority" optional="hide" />
|
||||
<field name="exc_name" optional="hide" />
|
||||
<field name="exc_message" optional="hide" />
|
||||
<field name="uuid" optional="show" />
|
||||
<field name="channel" optional="show" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_queue_job_pivot" model="ir.ui.view">
|
||||
<field name="name">queue.job.pivot</field>
|
||||
<field name="model">queue.job</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Jobs">
|
||||
<field name="exec_time" type="measure" />
|
||||
<field name="job_function_id" type="row" />
|
||||
<field name="date_done" type="col" interval="week" />
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_queue_job_graph" model="ir.ui.view">
|
||||
<field name="name">queue.job.graph</field>
|
||||
<field name="model">queue.job</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Jobs">
|
||||
<field name="job_function_id" type="row" />
|
||||
<field name="exec_time" type="measure" />
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_queue_job_search" model="ir.ui.view">
|
||||
<field name="name">queue.job.search</field>
|
||||
<field name="model">queue.job</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Jobs">
|
||||
<field name="uuid" />
|
||||
<field name="graph_uuid" />
|
||||
<field name="name" />
|
||||
<field name="func_string" />
|
||||
<field name="channel" />
|
||||
<field name="job_function_id" />
|
||||
<field name="model_name" />
|
||||
<field name="priority" />
|
||||
<field name="exc_name" />
|
||||
<field name="exc_message" />
|
||||
<field name="exc_info" />
|
||||
<field name="result" />
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
widget="selection"
|
||||
/>
|
||||
<filter
|
||||
name="wait_dependencies"
|
||||
string="Wait Dependencies"
|
||||
domain="[('state', '=', 'wait_dependencies')]"
|
||||
/>
|
||||
<filter
|
||||
name="pending"
|
||||
string="Pending"
|
||||
domain="[('state', '=', 'pending')]"
|
||||
/>
|
||||
<filter
|
||||
name="enqueued"
|
||||
string="Enqueued"
|
||||
domain="[('state', '=', 'enqueued')]"
|
||||
/>
|
||||
<filter
|
||||
name="started"
|
||||
string="Started"
|
||||
domain="[('state', '=', 'started')]"
|
||||
/>
|
||||
<filter name="done" string="Done" domain="[('state', '=', 'done')]" />
|
||||
<filter
|
||||
name="failed"
|
||||
string="Failed"
|
||||
domain="[('state', '=', 'failed')]"
|
||||
/>
|
||||
<filter
|
||||
name="cancelled"
|
||||
string="Cancelled"
|
||||
domain="[('state', '=', 'cancelled')]"
|
||||
/>
|
||||
<separator />
|
||||
<filter
|
||||
name="retried"
|
||||
string="Tried many times"
|
||||
domain="[('retry', '>', 1)]"
|
||||
/>
|
||||
<separator />
|
||||
<filter
|
||||
name="last_24_hours"
|
||||
string="Last 24 hours"
|
||||
domain="[('date_created', '>=', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d'))]"
|
||||
/>
|
||||
<filter
|
||||
name="last_7_days"
|
||||
string="Last 7 days"
|
||||
domain="[('date_created', '>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"
|
||||
/>
|
||||
<filter
|
||||
name="last_30_days"
|
||||
string="Last 30 days"
|
||||
domain="[('date_created', '>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"
|
||||
/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter
|
||||
name="group_by_channel"
|
||||
string="Channel"
|
||||
context="{'group_by': 'channel'}"
|
||||
/>
|
||||
<filter
|
||||
name="group_by_job_function_id"
|
||||
string="Job Function"
|
||||
context="{'group_by': 'job_function_id'}"
|
||||
/>
|
||||
<filter
|
||||
name="group_by_state"
|
||||
string="State"
|
||||
context="{'group_by': 'state'}"
|
||||
/>
|
||||
<filter
|
||||
name="group_by_priority"
|
||||
string="Priority"
|
||||
context="{'group_by': 'priority'}"
|
||||
/>
|
||||
<filter
|
||||
name="group_by_model_name"
|
||||
string="Model"
|
||||
context="{'group_by': 'model_name'}"
|
||||
/>
|
||||
<filter
|
||||
name="group_by_exc_name"
|
||||
string="Exception"
|
||||
context="{'group_by': 'exc_name'}"
|
||||
/>
|
||||
<filter
|
||||
name="group_by_exc_message"
|
||||
string="Exception message"
|
||||
context="{'group_by': 'exc_message'}"
|
||||
/>
|
||||
<filter
|
||||
name="group_by_graph"
|
||||
string="Graph"
|
||||
context="{'group_by': 'graph_uuid'}"
|
||||
/>
|
||||
<filter
|
||||
name="group_by_date_created"
|
||||
string="Created date"
|
||||
context="{'group_by': 'date_created'}"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_queue_job" model="ir.actions.act_window">
|
||||
<field name="name">Jobs</field>
|
||||
<field name="res_model">queue.job</field>
|
||||
<field name="view_mode">tree,form,pivot,graph</field>
|
||||
<field name="context">{'search_default_wait_dependencies': 1,
|
||||
'search_default_pending': 1,
|
||||
'search_default_enqueued': 1,
|
||||
'search_default_started': 1,
|
||||
'search_default_failed': 1}</field>
|
||||
<field name="view_id" ref="view_queue_job_tree" />
|
||||
<field name="search_view_id" ref="view_queue_job_search" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,3 +0,0 @@
|
||||
from . import queue_requeue_job
|
||||
from . import queue_jobs_to_done
|
||||
from . import queue_jobs_to_cancelled
|
||||
@@ -1,17 +0,0 @@
|
||||
# Copyright 2013-2020 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class SetJobsToCancelled(models.TransientModel):
|
||||
_inherit = "queue.requeue.job"
|
||||
_name = "queue.jobs.to.cancelled"
|
||||
_description = "Cancel all selected jobs"
|
||||
|
||||
def set_cancelled(self):
|
||||
jobs = self.job_ids.filtered(
|
||||
lambda x: x.state in ("pending", "failed", "enqueued")
|
||||
)
|
||||
jobs.button_cancelled()
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_set_jobs_cancelled" model="ir.ui.view">
|
||||
<field name="name">Cancel Jobs</field>
|
||||
<field name="model">queue.jobs.to.cancelled</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group string="The selected jobs will be cancelled.">
|
||||
<field name="job_ids" nolabel="1" colspan="2" />
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="set_cancelled"
|
||||
string="Cancel jobs"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_set_jobs_cancelled" model="ir.actions.act_window">
|
||||
<field name="name">Cancel jobs</field>
|
||||
<field name="res_model">queue.jobs.to.cancelled</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_set_jobs_cancelled" />
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="queue_job.model_queue_job" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,15 +0,0 @@
|
||||
# Copyright 2013-2020 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class SetJobsToDone(models.TransientModel):
|
||||
_inherit = "queue.requeue.job"
|
||||
_name = "queue.jobs.to.done"
|
||||
_description = "Set all selected jobs to done"
|
||||
|
||||
def set_done(self):
|
||||
jobs = self.job_ids
|
||||
jobs.button_done()
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_set_jobs_done" model="ir.ui.view">
|
||||
<field name="name">Set Jobs to Done</field>
|
||||
<field name="model">queue.jobs.to.done</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Set jobs done">
|
||||
<group string="The selected jobs will be set to done.">
|
||||
<field name="job_ids" nolabel="1" colspan="2" />
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="set_done"
|
||||
string="Set to done"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_set_jobs_done" model="ir.actions.act_window">
|
||||
<field name="name">Set jobs to done</field>
|
||||
<field name="res_model">queue.jobs.to.done</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_set_jobs_done" />
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="queue_job.model_queue_job" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,25 +0,0 @@
|
||||
# Copyright 2013-2020 Camptocamp SA
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class QueueRequeueJob(models.TransientModel):
|
||||
_name = "queue.requeue.job"
|
||||
_description = "Wizard to requeue a selection of jobs"
|
||||
|
||||
def _default_job_ids(self):
|
||||
res = False
|
||||
context = self.env.context
|
||||
if context.get("active_model") == "queue.job" and context.get("active_ids"):
|
||||
res = context["active_ids"]
|
||||
return res
|
||||
|
||||
job_ids = fields.Many2many(
|
||||
comodel_name="queue.job", string="Jobs", default=lambda r: r._default_job_ids()
|
||||
)
|
||||
|
||||
def requeue(self):
|
||||
jobs = self.job_ids
|
||||
jobs.requeue()
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_requeue_job" model="ir.ui.view">
|
||||
<field name="name">Requeue Jobs</field>
|
||||
<field name="model">queue.requeue.job</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Requeue Jobs">
|
||||
<group string="The selected jobs will be requeued.">
|
||||
<field name="job_ids" nolabel="1" colspan="2" />
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="requeue"
|
||||
string="Requeue"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_requeue_job" model="ir.actions.act_window">
|
||||
<field name="name">Requeue Jobs</field>
|
||||
<field name="res_model">queue.requeue.job</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_requeue_job" />
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="queue_job.model_queue_job" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user