Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,48 @@
# Job Queue
Odoo addon: queue_job
## Installation
```bash
pip install odoo-bringout-oca-queue-queue_job
```
## Dependencies
This addon depends on:
- mail
- base_sparse_field
- web
## Manifest Information
- **Name**: Job Queue
- **Version**: 16.0.2.11.4
- **Category**: Generic Modules
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/queue](https://github.com/OCA/queue) branch 16.0, addon `queue_job`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Queue_job Module - queue_job
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for queue_job. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,17 @@
# Controllers
HTTP routes provided by this module.
```mermaid
sequenceDiagram
participant U as User/Client
participant C as Module Controllers
participant O as ORM/Views
U->>C: HTTP GET/POST (routes)
C->>O: ORM operations, render templates
O-->>U: HTML/JSON/PDF
```
Notes
- See files in controllers/ for route definitions.

View file

@ -0,0 +1,7 @@
# Dependencies
This addon depends on:
- [mail](../../odoo-bringout-oca-ocb-mail)
- [base_sparse_field](../../odoo-bringout-oca-ocb-base_sparse_field)
- [web](../../odoo-bringout-oca-ocb-web)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon queue_job or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-queue-queue_job"
# or
uv pip install odoo-bringout-oca-queue-queue_job"
```

View file

@ -0,0 +1,17 @@
# Models
Detected core models and extensions in queue_job.
```mermaid
classDiagram
class queue_job
class queue_job_channel
class queue_job_function
class queue_job_lock
class base
class ir_model_fields
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: queue_job. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon queue_job
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,42 @@
# Security
Access control and security definitions in queue_job.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[ir.model.access.csv](../queue_job/security/ir.model.access.csv)**
- 7 model access rules
## Record Rules
Row-level security rules defined in:
## Security Groups & Configuration
Security groups and permissions defined in:
- **[security.xml](../queue_job/security/security.xml)**
- 1 security groups defined
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[ir.model.access.csv](../queue_job/security/ir.model.access.csv)**
- Model access permissions (CRUD rights)
- **[security.xml](../queue_job/security/security.xml)**
- Security groups, categories, and XML-based rules
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon queue_job
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,44 @@
[project]
name = "odoo-bringout-oca-queue-queue_job"
version = "16.0.0"
description = "Job Queue - Odoo addon"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-mail>=16.0.0",
"odoo-bringout-oca-queue-base_sparse_field>=16.0.0",
"odoo-bringout-oca-ocb-web>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["queue_job"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

@ -0,0 +1,707 @@
.. 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:7cc71c9aa5e6aec02e31b34ff1df8b45615787be688cbb13ac7714243400d7f8
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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.

View file

@ -0,0 +1,10 @@
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

View file

@ -0,0 +1,35 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
{
"name": "Job Queue",
"version": "16.0.2.11.4",
"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",
}

View file

@ -0,0 +1 @@
from . import main

View file

@ -0,0 +1,312 @@
# 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)
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 = getattr(orig_exception, "name", 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,
)

View file

@ -0,0 +1,28 @@
<?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>

View file

@ -0,0 +1,6 @@
<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>

View file

@ -0,0 +1,666 @@
# 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__

View file

@ -0,0 +1,43 @@
# 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"""

View file

@ -0,0 +1,123 @@
# 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

View file

@ -0,0 +1,940 @@
# 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 "Pristup Odbijen"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
msgid "Action Needed"
msgstr "Potrebna akcija"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
msgid "Activities"
msgstr "Aktivnosti"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
msgid "Activity Exception Decoration"
msgstr "Dekoracija iznimke aktivnosti"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
msgid "Activity State"
msgstr "Status aktivnosti"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
msgid "Activity Type Icon"
msgstr "Ikona tipa aktivnosti"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
msgid "Args"
msgstr "Argumenti"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
msgid "Attachment Count"
msgstr "Broj priloga"
#. 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 "AutoVacuum red poslova"
#. module: queue_job
#: model:ir.model,name:queue_job.model_base
msgid "Base"
msgstr "Osnova"
#. 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 "Otkaži"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
msgid "Cancel all selected jobs"
msgstr "Otkaži sve odabrane poslove"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Cancel job"
msgstr "Otkaži posao"
#. 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 "Otkaži poslove"
#. 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 "Otkazan"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Cancelled by %s"
msgstr "Otkazao %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 "Ne možete promijeniti glavni kanal"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job_channel.py:0
#, python-format
msgid "Cannot remove the root channel"
msgstr "Ne možete ukloniti glavni kanal"
#. 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 "Potpuno ime kanala mora biti jedinstveno"
#. 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 "Kanali"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
msgid "Company"
msgstr "Preduzeće"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
msgid "Complete Method Name"
msgstr "Potpuno ime metode"
#. 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 "Puni naziv"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
msgid "Created Date"
msgstr "Datum kreiranja"
#. 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 "Kreirao"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Created date"
msgstr "Datum kreiranja"
#. 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 "Kreirano"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
msgid "Current try"
msgstr "Trenutni pokušaj"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Current try / max. retries"
msgstr "Trenutni pokušaj / maks. pokušaja"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
msgid "Date Cancelled"
msgstr "Datum otkazivanja"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
msgid "Date Done"
msgstr "Datum završetka"
#. 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 "Ovisnosti"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
msgid "Dependency Graph"
msgstr "Grafik ovisnosti"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
msgid "Description"
msgstr "Opis"
#. 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 "Prikazani naziv"
#. 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 "Gotovo"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
msgid "Enqueue Time"
msgstr "Vrijeme stavljanja u red"
#. 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 "Stavljen u red"
#. 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 "Opis"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
msgid "Exception Info"
msgstr "Informacije o iznimci"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Exception Information"
msgstr "Informacije o iznimci"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
msgid "Exception Message"
msgstr "Poruka iznimke"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Exception message"
msgstr "Poruka iznimke"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Exception:"
msgstr "Iznimka:"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
msgid "Execute only after"
msgstr "Izvrši tek nakon"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
msgid "Execution Time (avg)"
msgstr "Vrijeme izvršavanja (prosj.)"
#. 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 "Neuspješan"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
msgid "Field Type"
msgstr "Tip polja"
#. module: queue_job
#: model:ir.model,name:queue_job.model_ir_model_fields
msgid "Fields"
msgstr "Polja"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
msgid "Followers"
msgstr "Pratioci"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
msgid "Followers (Partners)"
msgstr "Pratioci (Partneri)"
#. 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 ikona npr. fa-tasks"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Graph"
msgstr "Dijagram"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Graph Jobs"
msgstr "Grafički poslovi"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
msgid "Graph Jobs Count"
msgstr "Broj poslova grafika"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
msgid "Graph UUID"
msgstr "UUID grafika"
#. 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 "Grupiši po"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
msgid "Has Message"
msgstr "Ima poruku"
#. 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 "Ikona"
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
msgid "Icon to indicate an exception activity."
msgstr "Ikona za prikaz iznimki."
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
msgid "Identity Key"
msgstr "Ključ identiteta"
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
msgid "If checked, new messages require your attention."
msgstr "Ako je zakačeno, nove poruke će zahtjevati vašu pažnju"
#. 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 "Ako je označeno neke poruke mogu imati grešku u dostavi."
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job_function.py:0
#, python-format
msgid "Invalid job function: {}"
msgstr "Neispravna funkcija posla: {}"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
msgid "Is Follower"
msgstr "Pratilac"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_job_channel
msgid "Job Channels"
msgstr "Kanali poslova"
#. 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 "Funkcija posla"
#. 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 "Funkcije posla"
#. 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 "Red poslova"
#. module: queue_job
#: model:res.groups,name:queue_job.group_queue_job_manager
msgid "Job Queue Manager"
msgstr "Upravljač reda poslova"
#. module: queue_job
#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
msgid "Job Serialized"
msgstr "Serijalizovani posao"
#. module: queue_job
#: model:mail.message.subtype,name:queue_job.mt_job_failed
msgid "Job failed"
msgstr "Posao neuspješan"
#. 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 "Posao prekinut i postavljen na završeno: nema šta da se radi."
#. 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 "Zadaci"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Jobs for graph %s"
msgstr "Poslovi za grafik %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 "Zadnjih 24 sata"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Last 30 days"
msgstr "Posljednih 30 dana"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Last 7 days"
msgstr "Posljednjih 7 dana"
#. 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 "Zadnje mijenjano"
#. 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 "Zadnji ažurirao"
#. 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 "Zadnje ažurirano"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_main_attachment_id
msgid "Main Attachment"
msgstr "Glavna zakačka"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Manually set to done by %s"
msgstr "Ručno postavljen na završeno od %s"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
msgid "Max. retries"
msgstr "Maks. pokušaja"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
msgid "Message Delivery error"
msgstr "Greška pri isporuci poruke"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
msgid "Messages"
msgstr "Poruke"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
msgid "Method"
msgstr "Metoda"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
msgid "Method Name"
msgstr "Ime metode"
#. 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 {} nije pronađen"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
msgid "My Activity Deadline"
msgstr "Rok za moju aktivnost"
#. 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 "Naziv:"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
msgid "Next Activity Deadline"
msgstr "Krajnji rok za sljedeću aktivnost"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
msgid "Next Activity Summary"
msgstr "Pregled sljedeće aktivnosti"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
msgid "Next Activity Type"
msgstr "Tip sljedeće aktivnosti"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "No action available for this job"
msgstr "Nema dostupne akcije za ovaj posao"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Not allowed to change field(s): {}"
msgstr "Nije dozvoljeno mijenjanje polja: {}"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
msgid "Number of Actions"
msgstr "Broj akcija"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
msgid "Number of errors"
msgstr "Broj grešaka"
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
msgid "Number of messages requiring action"
msgstr "Broj poruka koje zahtijevaju aktivnost"
#. 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 "Broj poruka sa greškama pri isporuci"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
msgid "Parent Channel"
msgstr "Roditeljski kanal"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job_channel.py:0
#, python-format
msgid "Parent channel required."
msgstr "Potreban je roditeljski kanal."
#. 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 "Na čekanju"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
msgid "Priority"
msgstr "Prioritet"
#. module: queue_job
#: model:ir.ui.menu,name:queue_job.menu_queue
msgid "Queue"
msgstr "Red"
#. 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 "Red poslova"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_job_lock
msgid "Queue Job Lock"
msgstr "Zaključavanje reda poslova"
#. 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 "Poslovi reda morah biti kreirani pozivom 'with_delay()'."
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids
msgid "Record"
msgstr "Zapis"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
msgid "Record(s)"
msgstr "Zapis(i)"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Related"
msgstr "Povezano"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
msgid "Related Action"
msgstr "Povezana akcija"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
msgid "Related Action (serialized)"
msgstr "Povezana akcija (serijalizovana)"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Related Record"
msgstr "Povezani zapis"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Related Records"
msgstr "Povezani zapisi"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
msgid "Remaining days to execute"
msgstr "Preostalo dana za izvršavanje"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
msgid "Removal Interval"
msgstr "Interval uklanjanja"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
msgid "Requeue"
msgstr "Vrati u red"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Requeue Job"
msgstr "Vrati posao u red"
#. 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 "Vrati poslove u red"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
msgid "Responsible User"
msgstr "Odgovorni korisnik"
#. 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 "Rezultat"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Results"
msgstr "Rezultati"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
msgid "Retry Pattern"
msgstr "Uzorak ponovnog pokušaja"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
msgid "Retry Pattern (serialized)"
msgstr "Uzorak ponovnog pokušaja (serijalizovan)"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_jobs_to_done
msgid "Set all selected jobs to done"
msgstr "Postavi sve odabrane poslove na završeno"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
msgid "Set jobs done"
msgstr "Označi poslove kao završene"
#. module: queue_job
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
msgid "Set jobs to done"
msgstr "Postavi poslove na završeno"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Set to 'Done'"
msgstr "Postavi na 'Završeno'"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
msgid "Set to done"
msgstr "Postavi na završeno"
#. 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 "Jedinstveni dijeljeni identifikator grafika. Prazan za pojedinačni posao."
#. 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 "Početni datum"
#. 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 "Započeto"
#. 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 "Status"
#. 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 "Zadatak"
#. 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 "Odabrani poslovi će biti otkazani."
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
msgid "The selected jobs will be requeued."
msgstr "Odabrani poslovi će biti vraćeni u red."
#. 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 "Odabrani poslovi će biti postavljeni na završeno."
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Time (s)"
msgstr "Vrijeme (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 "Vrijeme potrebno za izvršavanje ovog posla u sekundama. Prosjek kad je grupisano."
#. 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 "Vrsta aktivnosti iznimke na zapisu."
#. 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 korisnika"
#. 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 "Čeka ovisnosti"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_requeue_job
msgid "Wizard to requeue a selection of jobs"
msgstr "Čarobnjak za vraćanje odabranog skupa poslova u red"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
msgid "Worker Pid"
msgstr "PID radnika"

View file

@ -0,0 +1,958 @@
# 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
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: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

View file

@ -0,0 +1,999 @@
# 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-02-27 11:06+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\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.6.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 ""
"<br/>\n"
" <span class=\"oe_grey oe_inline\"> Se il massimo "
"di tentativi è 0, il numero di tentativi è infinito.</span>"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/controllers/main.py:0
#, python-format
msgid "Access Denied"
msgstr "Accesso negato"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
msgid "Action Needed"
msgstr "Azione richiesta"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
msgid "Activities"
msgstr "Attività"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
msgid "Activity Exception Decoration"
msgstr "Decorazione eccezione attività"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
msgid "Activity State"
msgstr "Stato attività"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
msgid "Activity Type Icon"
msgstr "Icona tipo attività"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
msgid "Args"
msgstr "Argomenti"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
msgid "Attachment Count"
msgstr "Conteggio allegati"
#. 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 "Auto pulizia coda lavoro"
#. 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 "Annulla"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
msgid "Cancel all selected jobs"
msgstr "Annulla tutti i lavori selezionati"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Cancel job"
msgstr "Annulla lavoro"
#. 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 "Annulla lavori"
#. 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 "Annullata"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Cancelled by %s"
msgstr "Annullata da %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 "Non si può cambiare il canale radice"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job_channel.py:0
#, python-format
msgid "Cannot remove the root channel"
msgstr "Non si può rimuovere il canale radice"
#. 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 "Canale"
#. module: queue_job
#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
msgid "Channel complete name must be unique"
msgstr "Il nome completo del canale deve essere univoco"
#. 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 "Canali"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
msgid "Company"
msgstr "Azienda"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
msgid "Complete Method Name"
msgstr "Nome completo metodo"
#. 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 "Nome completo"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
msgid "Created Date"
msgstr "Data creazione"
#. 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 "Creato da"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Created date"
msgstr "Data creazione"
#. 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 "Creato il"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
msgid "Current try"
msgstr "Tentativo attuale"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Current try / max. retries"
msgstr "Tentativo attuale / massimo tentativi"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
msgid "Date Cancelled"
msgstr "Data annullamento"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
msgid "Date Done"
msgstr "Data completamento"
#. 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 "Dipendenze"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
msgid "Dependency Graph"
msgstr "Grafico dipendenza"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
msgid "Description"
msgstr "Descrizione"
#. 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 "Nome visualizzato"
#. 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 "Completata"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
msgid "Enqueue Time"
msgstr "Ora accodamento"
#. 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 "In coda"
#. 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 "Eccezione"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
msgid "Exception Info"
msgstr "Informazioni eccezione"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Exception Information"
msgstr "Informazioni eccezione"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
msgid "Exception Message"
msgstr "Messaggio eccezione"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Exception message"
msgstr "Messaggio eccezione"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Exception:"
msgstr "Eccezione:"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
msgid "Execute only after"
msgstr "Eseguire solo dopo"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
msgid "Execution Time (avg)"
msgstr "Tempo esecuzione (medio)"
#. 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 "Fallito"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
msgid "Field Type"
msgstr "Tipo campo"
#. module: queue_job
#: model:ir.model,name:queue_job.model_ir_model_fields
msgid "Fields"
msgstr "Campi"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
msgid "Followers"
msgstr "Seguito da"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
msgid "Followers (Partners)"
msgstr "Seguito da (partner)"
#. 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 "Icona Font Awesome es. fa-tasks"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Graph"
msgstr "Grafico"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Graph Jobs"
msgstr "Grafico lavori"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
msgid "Graph Jobs Count"
msgstr "Grafico conteggio lavori"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
msgid "Graph UUID"
msgstr "Grafico 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 "Raggruppa per"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
msgid "Has Message"
msgstr "Ha un messaggio"
#. 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 "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 indicare un'attività eccezione."
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
msgid "Identity Key"
msgstr "Chiave identità"
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
msgid "If checked, new messages require your attention."
msgstr "Se selezionata, nuovi messaggi richiedono attenzione."
#. 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 "Se selezionata, alcuni messaggi hanno un errore di consegna."
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job_function.py:0
#, python-format
msgid "Invalid job function: {}"
msgstr "Funzione lavoro non valida: {}"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
msgid "Is Follower"
msgstr "Segue"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_job_channel
msgid "Job Channels"
msgstr "Canali lavoro"
#. 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 "Funzione lavoro"
#. 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 "Funzioni lavoro"
#. 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 "Coda lavoro"
#. module: queue_job
#: model:res.groups,name:queue_job.group_queue_job_manager
msgid "Job Queue Manager"
msgstr "Gestore coda lavoro"
#. module: queue_job
#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
msgid "Job Serialized"
msgstr "Lavoro serializzato"
#. module: queue_job
#: model:mail.message.subtype,name:queue_job.mt_job_failed
msgid "Job failed"
msgstr "Lavro fallito"
#. 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 "Lavoro interrotto e impostato a completato: nulla da fare."
#. 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 "Lavori"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Jobs for graph %s"
msgstr "Lavori per grafico %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 "Ultime 24 ore"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Last 30 days"
msgstr "Ultimi 30 giorni"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Last 7 days"
msgstr "Ultimi 7 giorni"
#. 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 "Ultima modifica il"
#. 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 "Ultimo aggiornamento di"
#. 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 "Ultimo aggiornamento il"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_main_attachment_id
msgid "Main Attachment"
msgstr "Allegato principale"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Manually set to done by %s"
msgstr "Impostato manualmente a completato da %s"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
msgid "Max. retries"
msgstr "Massimo tentativi"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
msgid "Message Delivery error"
msgstr "Errore di consegna messaggio"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
msgid "Messages"
msgstr "Messaggi"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
msgid "Method"
msgstr "Metodo"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
msgid "Method Name"
msgstr "Nome metodo"
#. 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 "Modello"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job_function.py:0
#, python-format
msgid "Model {} not found"
msgstr "Modello {} non trovato"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
msgid "My Activity Deadline"
msgstr "Scadenza mia attività"
#. 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 "Nome"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
msgid "Next Activity Deadline"
msgstr "Scadenza prossima attività"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
msgid "Next Activity Summary"
msgstr "Riepilogo prossima attività"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
msgid "Next Activity Type"
msgstr "Tipo prossima attività"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "No action available for this job"
msgstr "Nessuna azione disponibile per questo lavoro"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Not allowed to change field(s): {}"
msgstr "Non autorizzato a modificare i campi: {}"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
msgid "Number of Actions"
msgstr "Numero di azioni"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
msgid "Number of errors"
msgstr "Numero di errori"
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
msgid "Number of messages requiring action"
msgstr "Numero di messaggi che richiedono un'azione"
#. 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 "Numero di messaggi con errore di consegna"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
msgid "Parent Channel"
msgstr "Canale padre"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job_channel.py:0
#, python-format
msgid "Parent channel required."
msgstr "Richiesto canale padre."
#. 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 ""
"Schema derivante dal conteggio dei tentativi degli errori ripetibili, numero "
"di secondi per ritardare l'esecuzione successiva. Impostando il numero di "
"secondi ad una tupla di due elementi o un elenco renderà causale "
"l'intervallo tra i tentativi tra i due valori.\n"
"Esempio: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
"Esempio: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
"Vedere la descrizione del modulo per i dettagli."
#. 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 "In attesa"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
msgid "Priority"
msgstr "Priorità"
#. module: queue_job
#: model:ir.ui.menu,name:queue_job.menu_queue
msgid "Queue"
msgstr "Coda"
#. 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 "Lavoro in coda"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_job_lock
msgid "Queue Job Lock"
msgstr "Blocco coda lavoro"
#. 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 "Il lavoro in coda deve essere creato chiamando 'with_delay()'."
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__record_ids
msgid "Record"
msgstr "Record"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
msgid "Record(s)"
msgstr "Record"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Related"
msgstr "Collegato"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
msgid "Related Action"
msgstr "Azione collegata"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
msgid "Related Action (serialized)"
msgstr "Azione collegata (serializzata)"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Related Record"
msgstr "Record collegato"
#. module: queue_job
#. odoo-python
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
msgid "Related Records"
msgstr "Record collegati"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
msgid "Remaining days to execute"
msgstr "Giorni rimanenti all'esecuzione"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
msgid "Removal Interval"
msgstr "Intervallo rimozione"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
msgid "Requeue"
msgstr "Rimetti in coda"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Requeue Job"
msgstr "Riaccoda lavoro"
#. 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 "Riaccoda lavori"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
msgid "Responsible User"
msgstr "Utente responsabile"
#. 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 "Risultato"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Results"
msgstr "Risultati"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
msgid "Retry Pattern"
msgstr "Riprova schema"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
msgid "Retry Pattern (serialized)"
msgstr "Riprova schema (serializzato)"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_jobs_to_done
msgid "Set all selected jobs to done"
msgstr "Imposta a completati tutti i lavori selezionati"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
msgid "Set jobs done"
msgstr "Imposta i lavori a completato"
#. module: queue_job
#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
msgid "Set jobs to done"
msgstr "Imposta i lavori a completato"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Set to 'Done'"
msgstr "Imposta come 'Completato'"
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
msgid "Set to done"
msgstr "Imposta a completato"
#. 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 ""
"Singolo identificatore condiviso di un grafico. Vuoto per un lavoro singolo."
#. 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 ""
"Qualcosa è andato male durante l'esecuzione del lavoro %s. Maggiori dettagli "
"nella sezione 'informazioni eccezione'."
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
msgid "Start Date"
msgstr "Data inizio"
#. 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 "Iniziato"
#. 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 "Stato"
#. 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 ""
"Stato in base alle attività\n"
"Scaduto: la data richiesta è trascorsa\n"
"Oggi: la data attività è oggi\n"
"Pianificato: attività future."
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
msgid "Task"
msgstr "Lavoro"
#. 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 ""
"L'azione quando si usa il pulsante 'Azione collegata' in un lavoro. L'azione "
"predefinita è di aprire la vista del record collegato al lavoro. Configrata "
"come dizionari con chiavi opzionali: enable, func_name, kwargs.\n"
"Vedere la descrizione del modulo per i dettagli."
#. 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 ""
"Il lavoro fallirà se il numero di tentativi raggiunge il massimo.\n"
"I tentativi sono infiniti quando vuoto."
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
msgid "The selected jobs will be cancelled."
msgstr "I lavori selezionati verranno annullati."
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
msgid "The selected jobs will be requeued."
msgstr "I lavori selezionati verranno riaccodati."
#. 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 "I lavori selezionati verranno impostati a completato."
#. module: queue_job
#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
msgid "Time (s)"
msgstr "Ora (e)"
#. 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 ""
"Tempo in secondi richiesto per eseguire il lavoro. Medio quando raggruppati."
#. 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 "Tipo di attività eccezione sul record."
#. 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 ""
"Formato inaspettato di azione colegata per {}.\n"
"Esempio di formato valido:\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 ""
"Formatto inatteso dello schema del nuovo tentativo per {}.\n"
"Esempio di formati validi:\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 "ID utente"
#. 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 "Attesa dipendenze"
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_requeue_job
msgid "Wizard to requeue a selection of jobs"
msgstr "Procedura guidata per riaccodare una selezione di lavori"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
msgid "Worker Pid"
msgstr "PID worker"
#, python-format
#~ msgid "If both parameters are 0, ALL jobs will be requeued!"
#~ msgstr "Se entrambi i parametri sono 0, tutti i lavori verranno riaccodati!"
#~ msgid "Jobs Garbage Collector"
#~ msgstr "Garbage collector lavori"
#, python-format
#~ msgid ""
#~ "Unexpected format of Retry Pattern for {}.\n"
#~ "Example of valid format:\n"
#~ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
#~ msgstr ""
#~ "Formato inaspettato di schema tentativo per {}.\n"
#~ "Esempio di formato valido:\n"
#~ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"

View file

@ -0,0 +1,940 @@
# 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
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: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 ""

View file

@ -0,0 +1,989 @@
# 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
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: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ı"

View file

@ -0,0 +1,990 @@
# 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
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: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 "未读消息数量"

View file

@ -0,0 +1,962 @@
# 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 functools import total_ordering
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
@total_ordering
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
# TODO to deprecate (not called anymore)
@classmethod
def enqueue(
cls,
func,
args=None,
kwargs=None,
priority=None,
eta=None,
max_retries=None,
description=None,
channel=None,
identity_key=None,
):
"""Create a Job and enqueue it in the queue. Return the job uuid.
This expects the arguments specific to the job to be already extracted
from the ones to pass to the job function.
If the identity key is the same than the one in a pending job,
no job is created and the existing job is returned
"""
new_job = cls(
func=func,
args=args,
kwargs=kwargs,
priority=priority,
eta=eta,
max_retries=max_retries,
description=description,
channel=channel,
identity_key=identity_key,
)
return new_job._enqueue_job()
# TODO to deprecate (not called anymore)
def _enqueue_job(self):
if self.identity_key:
existing = self.job_record_with_same_identity_key()
if existing:
_logger.debug(
"a job has not been enqueued due to having "
"the same identity key (%s) than job %s",
self.identity_key,
existing.uuid,
)
return Job._load_from_db_record(existing)
self.store()
_logger.debug(
"enqueued %s:%s(*%r, **%r) with uuid: %s",
self.recordset,
self.method_name,
self.args,
self.kwargs,
self.uuid,
)
return self
@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 sorting_key(self):
return self.eta, self.priority, self.date_created, self.seq
def __lt__(self, other):
if self.eta and not other.eta:
return True
elif not self.eta and other.eta:
return False
return self.sorting_key() < other.sorting_key()
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
)

View file

@ -0,0 +1,163 @@
# 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

View file

@ -0,0 +1,13 @@
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

View file

@ -0,0 +1,618 @@
# 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()
@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")

View file

@ -0,0 +1,47 @@
# 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

View file

@ -0,0 +1,33 @@
# 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;
"""
)

View file

@ -0,0 +1,11 @@
# 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"],
)

View file

@ -0,0 +1,10 @@
# 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;")

View file

@ -0,0 +1,6 @@
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

View file

@ -0,0 +1,266 @@
# 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()
}

View file

@ -0,0 +1,13 @@
# 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"},
)

View file

@ -0,0 +1,457 @@
# 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
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()
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):
self._cr.execute(
"SELECT indexname FROM pg_indexes WHERE indexname = %s ",
("queue_job_identity_key_state_partial_index",),
)
if not self._cr.fetchone():
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;"
)
@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),
],
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")

View file

@ -0,0 +1,94 @@
# 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

View file

@ -0,0 +1,273 @@
# 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

View file

@ -0,0 +1,16 @@
# 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,
)

View file

@ -0,0 +1,33 @@
# 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();
"""
)

View file

@ -0,0 +1,25 @@
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

View file

@ -0,0 +1,50 @@
* 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.

View file

@ -0,0 +1,12 @@
* 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>

View file

@ -0,0 +1,46 @@
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

View file

@ -0,0 +1,18 @@
.. [ 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

View file

@ -0,0 +1 @@
Be sure to have the ``requests`` library.

View file

@ -0,0 +1,18 @@
* 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')

View file

@ -0,0 +1,455 @@
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.

View file

@ -0,0 +1,8 @@
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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_queue_job_manager queue job manager queue_job.model_queue_job queue_job.group_queue_job_manager 1 1 1 1
3 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
4 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
5 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
6 access_queue_requeue_job queue requeue job manager queue_job.model_queue_requeue_job queue_job.group_queue_job_manager 1 1 1 1
7 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
8 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

View file

@ -0,0 +1,27 @@
<?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.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,998 @@
<!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:7cc71c9aa5e6aec02e31b34ff1df8b45615787be688cbb13ac7714243400d7f8
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<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&amp;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 PostgreSQLs 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 &lt;dbname&gt;
...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">&quot;state&quot;</span><span class="p">:</span> <span class="s2">&quot;done&quot;</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">&quot;state&quot;</span><span class="p">:</span> <span class="s2">&quot;done&quot;</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">&quot;generate xxx&quot;</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">&quot;state&quot;</span><span class="p">:</span> <span class="s2">&quot;done&quot;</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">&quot;generate xxx&quot;</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">&#64;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">&lt;record</span><span class="w"> </span><span class="na">id=</span><span class="s">&quot;channel_sale&quot;</span><span class="w"> </span><span class="na">model=</span><span class="s">&quot;queue.job.channel&quot;</span><span class="nt">&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;name&quot;</span><span class="nt">&gt;</span>sale<span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;parent_id&quot;</span><span class="w"> </span><span class="na">ref=</span><span class="s">&quot;queue_job.channel_root&quot;</span><span class="w"> </span><span class="nt">/&gt;</span><span class="w">
</span><span class="nt">&lt;/record&gt;</span>
</pre>
<p>Example of job function:</p>
<pre class="code XML literal-block">
<span class="nt">&lt;record</span><span class="w"> </span><span class="na">id=</span><span class="s">&quot;job_function_sale_order_action_done&quot;</span><span class="w"> </span><span class="na">model=</span><span class="s">&quot;queue.job.function&quot;</span><span class="nt">&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;model_id&quot;</span><span class="w"> </span><span class="na">ref=</span><span class="s">&quot;sale.model_sale_order&quot;</span><span class="w"> </span><span class="nt">/&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;method&quot;</span><span class="nt">&gt;</span>action_done<span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;channel_id&quot;</span><span class="w"> </span><span class="na">ref=</span><span class="s">&quot;channel_sale&quot;</span><span class="w"> </span><span class="nt">/&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;related_action&quot;</span><span class="w"> </span><span class="na">eval=</span><span class="s">'{&quot;func_name&quot;: &quot;custom_related_action&quot;}'</span><span class="w"> </span><span class="nt">/&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;retry_pattern&quot;</span><span class="w"> </span><span class="na">eval=</span><span class="s">&quot;{1: 60, 2: 180, 3: 10, 5: 300}&quot;</span><span class="w"> </span><span class="nt">/&gt;</span><span class="w">
</span><span class="nt">&lt;/record&gt;</span>
</pre>
<p>The general form for the <tt class="docutils literal">name</tt> is: <tt class="docutils literal"><span class="pre">&lt;model.name&gt;.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), theyll 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">&lt;field <span class="pre">name=&quot;model_id&quot;</span> <span class="pre">ref=&quot;xml_id_of_the_abstract_model&quot;&lt;/field&gt;</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 Jobs 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 doesnt 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">&quot;enable&quot;</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;func_name&quot;</span><span class="p">:</span> <span class="s2">&quot;related_action_partner&quot;</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;kwargs&quot;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;Partner&quot;</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">&#64;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 jobs 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 databases 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">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot; &quot;</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">&quot;other.model&quot;</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">&quot;Hi!&quot;</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">&quot;model&quot;</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">&quot;model&quot;</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">&quot;model&quot;</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">&quot;Hi!&quot;</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">&quot;model&quot;</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">&quot;Hi!&quot;</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">&quot;Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!&quot;</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">&quot;model&quot;</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">&quot;model&quot;</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">&quot;model&quot;</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">&quot;Hi!&quot;</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">&quot;model&quot;</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">&quot;Hi!&quot;</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">&quot;Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!&quot;</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">&#64;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 youll have to mute the logger like:</p>
<blockquote>
&#64;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 jobs 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 &gt; 0)</li>
<li>[REF] <tt class="docutils literal">&#64;job</tt> and <tt class="docutils literal">&#64;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 &lt;<a class="reference external" href="mailto:guewen.baconnier&#64;camptocamp.com">guewen.baconnier&#64;camptocamp.com</a>&gt;</li>
<li>Stéphane Bidoul &lt;<a class="reference external" href="mailto:stephane.bidoul&#64;acsone.eu">stephane.bidoul&#64;acsone.eu</a>&gt;</li>
<li>Matthieu Dietrich &lt;<a class="reference external" href="mailto:matthieu.dietrich&#64;camptocamp.com">matthieu.dietrich&#64;camptocamp.com</a>&gt;</li>
<li>Jos De Graeve &lt;<a class="reference external" href="mailto:Jos.DeGraeve&#64;apertoso.be">Jos.DeGraeve&#64;apertoso.be</a>&gt;</li>
<li>David Lefever &lt;<a class="reference external" href="mailto:dl&#64;taktik.be">dl&#64;taktik.be</a>&gt;</li>
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
<li>Laetitia Gangloff &lt;<a class="reference external" href="mailto:laetitia.gangloff&#64;acsone.eu">laetitia.gangloff&#64;acsone.eu</a>&gt;</li>
<li>Cédric Pigeon &lt;<a class="reference external" href="mailto:cedric.pigeon&#64;acsone.eu">cedric.pigeon&#64;acsone.eu</a>&gt;</li>
<li>Tatiana Deribina &lt;<a class="reference external" href="mailto:tatiana.deribina&#64;avoin.systems">tatiana.deribina&#64;avoin.systems</a>&gt;</li>
<li>Souheil Bejaoui &lt;<a class="reference external" href="mailto:souheil.bejaoui&#64;acsone.eu">souheil.bejaoui&#64;acsone.eu</a>&gt;</li>
<li>Eric Antones &lt;<a class="reference external" href="mailto:eantones&#64;nuobit.com">eantones&#64;nuobit.com</a>&gt;</li>
<li>Simone Orsi &lt;<a class="reference external" href="mailto:simone.orsi&#64;camptocamp.com">simone.orsi&#64;camptocamp.com</a>&gt;</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

View file

@ -0,0 +1,143 @@
/* @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);

View file

@ -0,0 +1,10 @@
.o_field_job_directed_graph {
width: 600px;
height: 400px;
border: 1px solid lightgray;
div.root_vis {
height: 100%;
width: 100%;
}
}

View file

@ -0,0 +1,8 @@
<?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>

View file

@ -0,0 +1,10 @@
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

View file

@ -0,0 +1,458 @@
# 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.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 __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

View file

@ -0,0 +1,287 @@
# 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)

View file

@ -0,0 +1,93 @@
# 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)")

View file

@ -0,0 +1,154 @@
# 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)

View file

@ -0,0 +1,59 @@
# 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")])

View file

@ -0,0 +1,57 @@
# 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,
),
)

View file

@ -0,0 +1,25 @@
# 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)

View file

@ -0,0 +1,133 @@
# 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)

View file

@ -0,0 +1,10 @@
# 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)

View file

@ -0,0 +1,10 @@
# 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 runner
from .common import load_doctests
load_tests = load_doctests(runner)

View file

@ -0,0 +1,48 @@
# 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")

View file

@ -0,0 +1,40 @@
# 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

View file

@ -0,0 +1,58 @@
<?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>

View file

@ -0,0 +1,58 @@
<?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>

View file

@ -0,0 +1,34 @@
<?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>

View file

@ -0,0 +1,330 @@
<?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" />
<field name="state" />
<field name="date_created" />
<field
name="eta"
widget="remaining_days"
string="Remaining days to execute"
optional="hide"
/>
<field name="date_done" />
<field name="exec_time" />
<field name="exc_name" />
<field name="exc_message" />
<field name="uuid" />
<field name="channel" />
<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="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="last_24_hours"
string="Last 24 hours"
domain="[('date_created', '&gt;=', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d'))]"
/>
<filter
name="last_7_days"
string="Last 7 days"
domain="[('date_created', '&gt;=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"
/>
<filter
name="last_30_days"
string="Last 30 days"
domain="[('date_created', '&gt;=', (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_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>

View file

@ -0,0 +1,3 @@
from . import queue_requeue_job
from . import queue_jobs_to_done
from . import queue_jobs_to_cancelled

View file

@ -0,0 +1,17 @@
# 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"}

View file

@ -0,0 +1,34 @@
<?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>

View file

@ -0,0 +1,15 @@
# 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"}

View file

@ -0,0 +1,34 @@
<?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>

View file

@ -0,0 +1,25 @@
# 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"}

View file

@ -0,0 +1,34 @@
<?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>