It will be automatically deleted the {}.
\n"
-" It will be automatically deleted the ${object.expiration_date}.
\n"
+" \n"
+"Language-Team: none\n"
+"Language: da\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 4.3.2\n"
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/xml/base.xml:0
+#, python-format
+msgid "(You will receive the export by email)"
+msgstr "(Du vil modtage eksporten via mail)"
+
+#. module: base_export_async
+#: model:mail.template,body_html:base_export_async.delay_export_mail_template
+msgid ""
+"Your export is available here .
\n"
+" It will be automatically deleted the ${object."
+"expiration_date}.
\n"
+" \n"
+" This is an automated message "
+"please do not reply.
\n"
+" "
+msgstr ""
+
+#. module: base_export_async
+#: model:ir.model,name:base_export_async.model_delay_export
+msgid "Asynchronous Export"
+msgstr "Asynkron export"
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/xml/base.xml:0
+#, python-format
+msgid "Asynchronous export"
+msgstr "Asynkron export"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__create_uid
+msgid "Created by"
+msgstr "Oprettet af"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__create_date
+msgid "Created on"
+msgstr "Oprettet den"
+
+#. module: base_export_async
+#: model:ir.actions.server,name:base_export_async.to_delete_attachment_ir_actions_server
+#: model:ir.cron,cron_name:base_export_async.to_delete_attachment
+#: model:ir.cron,name:base_export_async.to_delete_attachment
+msgid "Delete Generated Exports"
+msgstr "Slet genererede eksporter"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__display_name
+msgid "Display Name"
+msgstr "Vist navn"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__expiration_date
+msgid "Expiration Date"
+msgstr ""
+
+#. module: base_export_async
+#: model:mail.template,subject:base_export_async.delay_export_mail_template
+msgid "Export ${object.model_description} ${datetime.date.today()}"
+msgstr ""
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "External ID"
+msgstr "External ID"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__id
+msgid "ID"
+msgstr "ID"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export____last_update
+msgid "Last Modified on"
+msgstr "Senest rettet den"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__write_uid
+msgid "Last Updated by"
+msgstr "Senest rettet af"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__write_date
+msgid "Last Updated on"
+msgstr "Senest rettet den"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__model_description
+msgid "Model Description"
+msgstr ""
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "Please select fields to export..."
+msgstr "Vælg venligst felter til eksporten..."
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__url
+msgid "Url"
+msgstr ""
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__user_ids
+msgid "Users"
+msgstr "Brugere"
+
+#. module: base_export_async
+#: code:addons/base_export_async/models/delay_export.py:0
+#, python-format
+msgid "You must set an email address to your user."
+msgstr "Du skal have en email adresse på dit brugeropsæt."
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "You will receive the export file by email as soon as it is finished."
+msgstr "Du vil modtage et link til eksporten så snart den er færdig."
+
+#, python-format
+#~ msgid ""
+#~ "\n"
+#~ " Your export is available here .
\n"
+#~ " It will be automatically deleted the {}.
\n"
+#~ "
\n"
+#~ " \n"
+#~ " This is an automated message please do not reply.\n"
+#~ "
\n"
+#~ " "
+#~ msgstr ""
+#~ "\n"
+#~ " Din export er tilgængelig her .
\n"
+#~ " Den vil atutomatisk blive slettet den {}.
\n"
+#~ "
\n"
+#~ " \n"
+#~ " Dette er en automatisk besked. Undlad venlisgt at "
+#~ "besvare.\n"
+#~ "
\n"
+#~ " "
+
+#, python-format
+#~ msgid "Export {} {}"
+#~ msgstr "Eksport {} {}"
diff --git a/base_export_async/i18n/de.po b/base_export_async/i18n/de.po
index 72d0bdfbc3..1d6505f262 100644
--- a/base_export_async/i18n/de.po
+++ b/base_export_async/i18n/de.po
@@ -1,6 +1,6 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
-# * base_export_async
+# * base_export_async
#
msgid ""
msgstr ""
@@ -16,43 +16,34 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.7.1\n"
-#. module: base_export_async
-#: code:addons/base_export_async/models/delay_export.py:112
-#, python-format
-msgid "\n"
-" Your export is available here .
\n"
-" It will be automatically deleted the {}.
\n"
-"
\n"
-" \n"
-" This is an automated message please do not reply.\n"
-"
\n"
-" "
-msgstr ""
-"\n"
-" Der Export ist hier verfügbar.
\n"
-" Das {} wird automatisch gelöscht.
\n"
-"
\n"
-" \n"
-" Dies ist eine automatisch erstellte Nachricht, bitte nicht "
-"darauf antworten.\n"
-"
\n"
-" "
-
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/xml/base.xml:9
+#: code:addons/base_export_async/static/src/xml/base.xml:0
#, python-format
msgid "(You will receive the export by email)"
msgstr "(Der Export wird per Mail bereitgestellt.)"
+#. module: base_export_async
+#: model:mail.template,body_html:base_export_async.delay_export_mail_template
+msgid ""
+"Your export is available here .
\n"
+" It will be automatically deleted the ${object."
+"expiration_date}.
\n"
+" \n"
+" This is an automated message "
+"please do not reply.
\n"
+" "
+msgstr ""
+
#. module: base_export_async
#: model:ir.model,name:base_export_async.model_delay_export
-msgid "Allow to delay the export"
-msgstr "Verzögerung des Exports erlauben"
+#, fuzzy
+msgid "Asynchronous Export"
+msgstr "Asynchroner Export"
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/xml/base.xml:9
+#: code:addons/base_export_async/static/src/xml/base.xml:0
#, python-format
msgid "Asynchronous export"
msgstr "Asynchroner Export"
@@ -80,14 +71,18 @@ msgid "Display Name"
msgstr "Anzeigename"
#. module: base_export_async
-#: code:addons/base_export_async/models/delay_export.py:110
-#, python-format
-msgid "Export {} {}"
-msgstr "Export {} {}"
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__expiration_date
+msgid "Expiration Date"
+msgstr ""
+
+#. module: base_export_async
+#: model:mail.template,subject:base_export_async.delay_export_mail_template
+msgid "Export ${object.model_description} ${datetime.date.today()}"
+msgstr ""
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/js/data_export.js:47
+#: code:addons/base_export_async/static/src/js/data_export.js:0
#, python-format
msgid "External ID"
msgstr "Externe ID"
@@ -112,26 +107,30 @@ msgstr "Zuletzt aktualisiert von"
msgid "Last Updated on"
msgstr "Zuletzt aktualisiert am"
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__model_description
+msgid "Model Description"
+msgstr ""
+
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/js/data_export.js:39
+#: code:addons/base_export_async/static/src/js/data_export.js:0
#, python-format
msgid "Please select fields to export..."
msgstr "Bitte Felder für den Export auswählen..."
#. module: base_export_async
-#: code:addons/base_export_async/models/delay_export.py:42
-#, python-format
-msgid "The user doesn't have an email address."
-msgstr "Der Benutzer hat keine Email-Adresse."
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__url
+msgid "Url"
+msgstr ""
#. module: base_export_async
-#: model:ir.model.fields,field_description:base_export_async.field_delay_export__user_id
-msgid "User"
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__user_ids
+msgid "Users"
msgstr "Benutzer"
#. module: base_export_async
-#: code:addons/base_export_async/models/delay_export.py:29
+#: code:addons/base_export_async/models/delay_export.py:0
#, python-format
msgid "You must set an email address to your user."
msgstr ""
@@ -139,8 +138,39 @@ msgstr ""
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/js/data_export.js:91
+#: code:addons/base_export_async/static/src/js/data_export.js:0
#, python-format
msgid "You will receive the export file by email as soon as it is finished."
msgstr ""
"Die Export-Datei wird per Email versendet, sobald der Export beendet ist."
+
+#, python-format
+#~ msgid ""
+#~ "\n"
+#~ " Your export is available here .
\n"
+#~ " It will be automatically deleted the {}.
\n"
+#~ "
\n"
+#~ " \n"
+#~ " This is an automated message please do not reply.\n"
+#~ "
\n"
+#~ " "
+#~ msgstr ""
+#~ "\n"
+#~ " Der Export ist hier verfügbar.
\n"
+#~ " Das {} wird automatisch gelöscht.
\n"
+#~ "
\n"
+#~ " \n"
+#~ " Dies ist eine automatisch erstellte Nachricht, bitte "
+#~ "nicht darauf antworten.\n"
+#~ "
\n"
+#~ " "
+
+#, python-format
+#~ msgid "Export {} {}"
+#~ msgstr "Export {} {}"
+
+#~ msgid "Allow to delay the export"
+#~ msgstr "Verzögerung des Exports erlauben"
+
+#~ msgid "The user doesn't have an email address."
+#~ msgstr "Der Benutzer hat keine Email-Adresse."
diff --git a/base_export_async/i18n/fr.po b/base_export_async/i18n/fr.po
new file mode 100644
index 0000000000..9bc60621b7
--- /dev/null
+++ b/base_export_async/i18n/fr.po
@@ -0,0 +1,168 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * base_export_async
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2020-11-20 16:54+0000\n"
+"Last-Translator: Yann Papouin \n"
+"Language-Team: none\n"
+"Language: fr\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 3.10\n"
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/xml/base.xml:0
+#, python-format
+msgid "(You will receive the export by email)"
+msgstr "(Vous recevrez cet export par courriel)"
+
+#. module: base_export_async
+#: model:mail.template,body_html:base_export_async.delay_export_mail_template
+msgid ""
+"Your export is available here .
\n"
+" It will be automatically deleted the ${object."
+"expiration_date}.
\n"
+" \n"
+" This is an automated message "
+"please do not reply.
\n"
+" "
+msgstr ""
+
+#. module: base_export_async
+#: model:ir.model,name:base_export_async.model_delay_export
+msgid "Asynchronous Export"
+msgstr "Export asynchrone"
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/xml/base.xml:0
+#, python-format
+msgid "Asynchronous export"
+msgstr "Export asynchrone"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__create_uid
+msgid "Created by"
+msgstr "Créé par"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__create_date
+msgid "Created on"
+msgstr "Créé le"
+
+#. module: base_export_async
+#: model:ir.actions.server,name:base_export_async.to_delete_attachment_ir_actions_server
+#: model:ir.cron,cron_name:base_export_async.to_delete_attachment
+#: model:ir.cron,name:base_export_async.to_delete_attachment
+msgid "Delete Generated Exports"
+msgstr "Supprimer les exports générés"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__display_name
+msgid "Display Name"
+msgstr "Nom affiché"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__expiration_date
+msgid "Expiration Date"
+msgstr ""
+
+#. module: base_export_async
+#: model:mail.template,subject:base_export_async.delay_export_mail_template
+msgid "Export ${object.model_description} ${datetime.date.today()}"
+msgstr ""
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "External ID"
+msgstr "Identifiant externe"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__id
+msgid "ID"
+msgstr "ID"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export____last_update
+msgid "Last Modified on"
+msgstr "Dernière modification le"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__write_uid
+msgid "Last Updated by"
+msgstr "Dernière mise à jour par"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__write_date
+msgid "Last Updated on"
+msgstr "Dernière mise à jour le"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__model_description
+msgid "Model Description"
+msgstr ""
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "Please select fields to export..."
+msgstr "Veuillez choisir les champs à exporter..."
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__url
+msgid "Url"
+msgstr ""
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__user_ids
+msgid "Users"
+msgstr "Utilisateurs"
+
+#. module: base_export_async
+#: code:addons/base_export_async/models/delay_export.py:0
+#, python-format
+msgid "You must set an email address to your user."
+msgstr "Vous devez définir une adresse e-mail pour votre utilisateur."
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "You will receive the export file by email as soon as it is finished."
+msgstr "Vous recevrez le fichier d'export par courriel dès qu'il sera terminé."
+
+#, python-format
+#~ msgid ""
+#~ "\n"
+#~ " Your export is available here .
\n"
+#~ " It will be automatically deleted the {}.
\n"
+#~ "
\n"
+#~ " \n"
+#~ " This is an automated message please do not reply.\n"
+#~ "
\n"
+#~ " "
+#~ msgstr ""
+#~ "\n"
+#~ " Votre export est disponible ici ."
+#~ "p>\n"
+#~ "
Il sera automatiquement supprimé le {}.
\n"
+#~ "
\n"
+#~ " \n"
+#~ " Ceci est un message automatisé, merci de ne pas "
+#~ "répondre.\n"
+#~ "
\n"
+#~ " "
+
+#, python-format
+#~ msgid "Export {} {}"
+#~ msgstr "Export {} {}"
diff --git a/base_export_async/i18n/it.po b/base_export_async/i18n/it.po
new file mode 100644
index 0000000000..2283e06398
--- /dev/null
+++ b/base_export_async/i18n/it.po
@@ -0,0 +1,150 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * base_export_async
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2025-01-16 16:06+0000\n"
+"Last-Translator: mymage \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: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/xml/base.xml:0
+#, python-format
+msgid "(You will receive the export by email)"
+msgstr "(Verrà inviata l'esportazione vie e-mail)"
+
+#. module: base_export_async
+#: model:mail.template,body_html:base_export_async.delay_export_mail_template
+msgid ""
+"Your export is available here .
\n"
+" It will be automatically deleted the ${object."
+"expiration_date}.
\n"
+" \n"
+" This is an automated message "
+"please do not reply.
\n"
+" "
+msgstr ""
+"La tua esportazione è disponibile here .
"
+"\n"
+" Verrà eliminata automaticamente il ${object."
+"expiration_date}.
\n"
+" \n"
+" Questo è un messaggio "
+"automatico, non rispondere.
\n"
+" "
+
+#. module: base_export_async
+#: model:ir.model,name:base_export_async.model_delay_export
+msgid "Asynchronous Export"
+msgstr "Esportazione asincrona"
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/xml/base.xml:0
+#, python-format
+msgid "Asynchronous export"
+msgstr "Esportazione asincrona"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__create_uid
+msgid "Created by"
+msgstr "Creato da"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__create_date
+msgid "Created on"
+msgstr "Creato il"
+
+#. module: base_export_async
+#: model:ir.actions.server,name:base_export_async.to_delete_attachment_ir_actions_server
+#: model:ir.cron,cron_name:base_export_async.to_delete_attachment
+#: model:ir.cron,name:base_export_async.to_delete_attachment
+msgid "Delete Generated Exports"
+msgstr "Cancella esportazioni generate"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__display_name
+msgid "Display Name"
+msgstr "Nome visualizzato"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__expiration_date
+msgid "Expiration Date"
+msgstr "Data di scadenza"
+
+#. module: base_export_async
+#: model:mail.template,subject:base_export_async.delay_export_mail_template
+msgid "Export ${object.model_description} ${datetime.date.today()}"
+msgstr "Esportazione ${object.model_description} ${datetime.date.today()}"
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "External ID"
+msgstr "ID esterno"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__id
+msgid "ID"
+msgstr "ID"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export____last_update
+msgid "Last Modified on"
+msgstr "Ultima modifica il"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__write_uid
+msgid "Last Updated by"
+msgstr "Ultimo aggiornamento di"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__write_date
+msgid "Last Updated on"
+msgstr "Ultimo aggiornamento il"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__model_description
+msgid "Model Description"
+msgstr "Descrizione modello"
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "Please select fields to export..."
+msgstr "Selezionare i campi da esportare..."
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__url
+msgid "Url"
+msgstr "URL"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__user_ids
+msgid "Users"
+msgstr "Utenti"
+
+#. module: base_export_async
+#: code:addons/base_export_async/models/delay_export.py:0
+#, python-format
+msgid "You must set an email address to your user."
+msgstr "Bisogna impostare una e-mail nel proprio utente."
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "You will receive the export file by email as soon as it is finished."
+msgstr "Verrà inviato via e-mail il file esportazione appena sarà completato."
diff --git a/base_export_async/i18n/pt.po b/base_export_async/i18n/pt.po
new file mode 100644
index 0000000000..88ab925247
--- /dev/null
+++ b/base_export_async/i18n/pt.po
@@ -0,0 +1,168 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * base_export_async
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2021-03-04 16:45+0000\n"
+"Last-Translator: Pedro Castro Silva \n"
+"Language-Team: none\n"
+"Language: pt\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 4.3.2\n"
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/xml/base.xml:0
+#, python-format
+msgid "(You will receive the export by email)"
+msgstr "(Receberá a exportação por email)"
+
+#. module: base_export_async
+#: model:mail.template,body_html:base_export_async.delay_export_mail_template
+msgid ""
+"Your export is available here .
\n"
+" It will be automatically deleted the ${object."
+"expiration_date}.
\n"
+" \n"
+" This is an automated message "
+"please do not reply.
\n"
+" "
+msgstr ""
+
+#. module: base_export_async
+#: model:ir.model,name:base_export_async.model_delay_export
+msgid "Asynchronous Export"
+msgstr "Exportação Assíncrona"
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/xml/base.xml:0
+#, python-format
+msgid "Asynchronous export"
+msgstr "Exportação assíncrona"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__create_uid
+msgid "Created by"
+msgstr "Criada por"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__create_date
+msgid "Created on"
+msgstr "Criado em"
+
+#. module: base_export_async
+#: model:ir.actions.server,name:base_export_async.to_delete_attachment_ir_actions_server
+#: model:ir.cron,cron_name:base_export_async.to_delete_attachment
+#: model:ir.cron,name:base_export_async.to_delete_attachment
+msgid "Delete Generated Exports"
+msgstr "Eliminar Exportações Geradas"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__display_name
+msgid "Display Name"
+msgstr "Nome a Apresentar"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__expiration_date
+msgid "Expiration Date"
+msgstr ""
+
+#. module: base_export_async
+#: model:mail.template,subject:base_export_async.delay_export_mail_template
+msgid "Export ${object.model_description} ${datetime.date.today()}"
+msgstr ""
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "External ID"
+msgstr "ID Externo"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__id
+msgid "ID"
+msgstr ""
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export____last_update
+msgid "Last Modified on"
+msgstr "Última Modificação em"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__write_uid
+msgid "Last Updated by"
+msgstr "Última Atualização por"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__write_date
+msgid "Last Updated on"
+msgstr "Última Atualização em"
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__model_description
+msgid "Model Description"
+msgstr ""
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "Please select fields to export..."
+msgstr "Por favor, selecione os campos a exportar..."
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__url
+msgid "Url"
+msgstr ""
+
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__user_ids
+msgid "Users"
+msgstr "Utilizadores"
+
+#. module: base_export_async
+#: code:addons/base_export_async/models/delay_export.py:0
+#, python-format
+msgid "You must set an email address to your user."
+msgstr "Tem que atribuir um email ao seu utilizador."
+
+#. module: base_export_async
+#. openerp-web
+#: code:addons/base_export_async/static/src/js/data_export.js:0
+#, python-format
+msgid "You will receive the export file by email as soon as it is finished."
+msgstr ""
+"Receberá o ficheiro de exportação por email assim que este estiver terminado."
+
+#, python-format
+#~ msgid ""
+#~ "\n"
+#~ " Your export is available here .
\n"
+#~ " It will be automatically deleted the {}.
\n"
+#~ "
\n"
+#~ " \n"
+#~ " This is an automated message please do not reply.\n"
+#~ "
\n"
+#~ " "
+#~ msgstr ""
+#~ "\n"
+#~ " A sua exportação está disponível aqui"
+#~ "a>.
\n"
+#~ " Será automaticamente eliminada em {}.
\n"
+#~ "
\n"
+#~ " \n"
+#~ " Esta é uma mensagem automática. Por favor, não responda.\n"
+#~ "
\n"
+#~ " "
+
+#, python-format
+#~ msgid "Export {} {}"
+#~ msgstr "Exportar {} {}"
diff --git a/base_export_async/i18n/zh_CN.po b/base_export_async/i18n/zh_CN.po
index 356e6aac6a..43c7ece307 100644
--- a/base_export_async/i18n/zh_CN.po
+++ b/base_export_async/i18n/zh_CN.po
@@ -1,6 +1,6 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
-# * base_export_async
+# * base_export_async
#
msgid ""
msgstr ""
@@ -16,42 +16,34 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 3.7.1\n"
-#. module: base_export_async
-#: code:addons/base_export_async/models/delay_export.py:112
-#, python-format
-msgid "\n"
-" Your export is available here .
\n"
-" It will be automatically deleted the {}.
\n"
-"
\n"
-" \n"
-" This is an automated message please do not reply.\n"
-"
\n"
-" "
-msgstr ""
-"\n"
-" 你的导出可以用 这里 .
\n"
-" 它将自动删除 {}。
\n"
-"
\n"
-" \n"
-" 这是一条自动消息,请不要回复。\n"
-"
\n"
-" "
-
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/xml/base.xml:9
+#: code:addons/base_export_async/static/src/xml/base.xml:0
#, python-format
msgid "(You will receive the export by email)"
msgstr "(您将通过电子邮件收到导出)"
+#. module: base_export_async
+#: model:mail.template,body_html:base_export_async.delay_export_mail_template
+msgid ""
+"Your export is available here .
\n"
+" It will be automatically deleted the ${object."
+"expiration_date}.
\n"
+" \n"
+" This is an automated message "
+"please do not reply.
\n"
+" "
+msgstr ""
+
#. module: base_export_async
#: model:ir.model,name:base_export_async.model_delay_export
-msgid "Allow to delay the export"
-msgstr "允许延迟导出"
+#, fuzzy
+msgid "Asynchronous Export"
+msgstr "异步导出"
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/xml/base.xml:9
+#: code:addons/base_export_async/static/src/xml/base.xml:0
#, python-format
msgid "Asynchronous export"
msgstr "异步导出"
@@ -79,14 +71,18 @@ msgid "Display Name"
msgstr "显示名称"
#. module: base_export_async
-#: code:addons/base_export_async/models/delay_export.py:110
-#, python-format
-msgid "Export {} {}"
-msgstr "导出{} {}"
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__expiration_date
+msgid "Expiration Date"
+msgstr ""
+
+#. module: base_export_async
+#: model:mail.template,subject:base_export_async.delay_export_mail_template
+msgid "Export ${object.model_description} ${datetime.date.today()}"
+msgstr ""
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/js/data_export.js:47
+#: code:addons/base_export_async/static/src/js/data_export.js:0
#, python-format
msgid "External ID"
msgstr "外部ID"
@@ -111,33 +107,67 @@ msgstr "最后更新者"
msgid "Last Updated on"
msgstr "最后更新时间"
+#. module: base_export_async
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__model_description
+msgid "Model Description"
+msgstr ""
+
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/js/data_export.js:39
+#: code:addons/base_export_async/static/src/js/data_export.js:0
#, python-format
msgid "Please select fields to export..."
msgstr "请选择要导出的字段..."
#. module: base_export_async
-#: code:addons/base_export_async/models/delay_export.py:42
-#, python-format
-msgid "The user doesn't have an email address."
-msgstr "用户没有电子邮件地址。"
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__url
+msgid "Url"
+msgstr ""
#. module: base_export_async
-#: model:ir.model.fields,field_description:base_export_async.field_delay_export__user_id
-msgid "User"
+#: model:ir.model.fields,field_description:base_export_async.field_delay_export__user_ids
+msgid "Users"
msgstr "用户"
#. module: base_export_async
-#: code:addons/base_export_async/models/delay_export.py:29
+#: code:addons/base_export_async/models/delay_export.py:0
#, python-format
msgid "You must set an email address to your user."
msgstr "您必须为您的用户设置电子邮件地址。"
#. module: base_export_async
#. openerp-web
-#: code:addons/base_export_async/static/src/js/data_export.js:91
+#: code:addons/base_export_async/static/src/js/data_export.js:0
#, python-format
msgid "You will receive the export file by email as soon as it is finished."
msgstr "完成后,您将通过电子邮件收到导出文件。"
+
+#, python-format
+#~ msgid ""
+#~ "\n"
+#~ " Your export is available here .
\n"
+#~ " It will be automatically deleted the {}.
\n"
+#~ "
\n"
+#~ " \n"
+#~ " This is an automated message please do not reply.\n"
+#~ "
\n"
+#~ " "
+#~ msgstr ""
+#~ "\n"
+#~ " 你的导出可以用 这里 .
\n"
+#~ " 它将自动删除 {}。
\n"
+#~ "
\n"
+#~ " \n"
+#~ " 这是一条自动消息,请不要回复。\n"
+#~ "
\n"
+#~ " "
+
+#, python-format
+#~ msgid "Export {} {}"
+#~ msgstr "导出{} {}"
+
+#~ msgid "Allow to delay the export"
+#~ msgstr "允许延迟导出"
+
+#~ msgid "The user doesn't have an email address."
+#~ msgstr "用户没有电子邮件地址。"
diff --git a/base_export_async/models/delay_export.py b/base_export_async/models/delay_export.py
index 7881af001b..4f83ed1c0b 100644
--- a/base_export_async/models/delay_export.py
+++ b/base_export_async/models/delay_export.py
@@ -3,27 +3,29 @@
import base64
import json
-import logging
import operator
from dateutil.relativedelta import relativedelta
+
from odoo import _, api, fields, models
-from odoo.addons.queue_job.job import job
-from odoo.addons.web.controllers.main import CSVExport, ExcelExport
from odoo.exceptions import UserError
-_logger = logging.getLogger(__name__)
+from odoo.addons.web.controllers.main import CSVExport, ExcelExport
class DelayExport(models.Model):
_name = "delay.export"
- _description = "Allow to delay the export"
+ _description = "Asynchronous Export"
- user_id = fields.Many2one("res.users", string="User", index=True)
+ user_ids = fields.Many2many("res.users", string="Users", index=True)
+ model_description = fields.Char()
+ url = fields.Char()
+ expiration_date = fields.Date()
@api.model
def delay_export(self, data):
+ """Delay the export, called from js"""
params = json.loads(data.get("data"))
if not self.env.user.email:
raise UserError(_("You must set an email address to your user."))
@@ -32,14 +34,11 @@ def delay_export(self, data):
@api.model
def _get_file_content(self, params):
export_format = params.get("format")
- raw_data = export_format != "csv"
- item_names = ("model", "fields", "ids", "domain", "import_compat", "context")
- items = operator.itemgetter(item_names)(params)
- model_name, fields_name, ids, domain, import_compat, context = items
- user = self.env["res.users"].browse([context.get("uid")])
- if not user or not user.email:
- raise UserError(_("The user doesn't have an email address."))
+ items = operator.itemgetter(
+ "model", "fields", "ids", "domain", "import_compat", "context", "user_ids"
+ )(params)
+ (model_name, fields_name, ids, domain, import_compat, context, user_ids) = items
model = self.env[model_name].with_context(
import_compat=import_compat, **context
@@ -52,7 +51,7 @@ def _get_file_content(self, params):
fields_name = [field for field in fields_name if field["name"] != "id"]
field_names = [f["name"] for f in fields_name]
- import_data = records.export_data(field_names, raw_data).get("datas", [])
+ import_data = records.export_data(field_names).get("datas", [])
if import_compat:
columns_headers = field_names
@@ -67,23 +66,34 @@ def _get_file_content(self, params):
return xls.from_data(columns_headers, import_data)
@api.model
- @job
def export(self, params):
+ """Delayed export of a file sent by email
+
+ The ``params`` is a dict of parameters, contains:
+
+ * format: csv/excel
+ * model: model to export
+ * fields: list of fields to export, a list of dict:
+ [{'label': '', 'name': ''}]
+ * ids: list of ids to export
+ * domain: domain for the export
+ * context: context for the export (language, ...)
+ * import_compat: if the export is export/import compatible (boolean)
+ * user_ids: optional list of user ids who receive the file
+ """
content = self._get_file_content(params)
- model_name, context, export_format = operator.itemgetter(
- "model", "context", "format"
- )(params)
- user = self.env["res.users"].browse([context.get("uid")])
+ items = operator.itemgetter("model", "context", "format", "user_ids")(params)
+ model_name, context, export_format, user_ids = items
+ users = self.env["res.users"].browse(user_ids)
- export_record = self.sudo().create({"user_id": user.id})
+ export_record = self.sudo().create({"user_ids": [(6, 0, users.ids)]})
name = "{}.{}".format(model_name, export_format)
attachment = self.env["ir.attachment"].create(
{
"name": name,
"datas": base64.b64encode(content),
- "datas_fname": name,
"type": "binary",
"res_model": self._name,
"res_id": export_record.id,
@@ -96,6 +106,10 @@ def export(self, params):
attachment.name,
)
+ if any(user.has_group("base.group_portal") for user in users):
+ attachment.generate_access_token()
+ url += f"&access_token={attachment.access_token}"
+
time_to_live = (
self.env["ir.config_parameter"].sudo().get_param("attachment.ttl", 7)
)
@@ -104,30 +118,24 @@ def export(self, params):
date_today + relativedelta(days=+int(time_to_live))
)
- # TODO : move to email template
odoo_bot = self.sudo().env.ref("base.partner_root")
email_from = odoo_bot.email
model_description = self.env[model_name]._description
- self.env["mail.mail"].create(
+ export_record.write(
{
+ "url": url,
+ "expiration_date": expiration_date,
+ "model_description": model_description,
+ }
+ )
+
+ self.env.ref("base_export_async.delay_export_mail_template").send_mail(
+ export_record.id,
+ email_values={
"email_from": email_from,
"reply_to": email_from,
- "email_to": user.email,
- "subject": _("Export {} {}").format(
- model_description, fields.Date.to_string(fields.Date.today())
- ),
- "body_html": _(
- """
- Your export is available here .
- It will be automatically deleted the {}.
-
-
- This is an automated message please do not reply.
-
- """
- ).format(url, expiration_date),
- "auto_delete": True,
- }
+ "recipient_ids": [(6, 0, users.mapped("partner_id").ids)],
+ },
)
@api.model
diff --git a/base_export_async/readme/CONTRIBUTORS.rst b/base_export_async/readme/CONTRIBUTORS.rst
index ede63fcc0d..b1a90f9127 100644
--- a/base_export_async/readme/CONTRIBUTORS.rst
+++ b/base_export_async/readme/CONTRIBUTORS.rst
@@ -1 +1,2 @@
-Arnaud Pineux (ACSONE SA/NV) authored the initial prototype.
+* Arnaud Pineux (ACSONE SA/NV) authored the initial prototype.
+* Guewen Baconnier (Camptocamp)
diff --git a/base_export_async/security/ir_rule.xml b/base_export_async/security/ir_rule.xml
index 7ed488ed37..d7934f09ae 100644
--- a/base_export_async/security/ir_rule.xml
+++ b/base_export_async/security/ir_rule.xml
@@ -1,13 +1,13 @@
-
-
+
+
Only user can read delay.export
-
-
-
-
-
-
- [('user_id', '=', user.id)]
+
+
+
+
+
+
+ [('user_ids', 'in', user.id)]
diff --git a/base_export_async/static/description/index.html b/base_export_async/static/description/index.html
index 7620c7e958..a392bd01b6 100644
--- a/base_export_async/static/description/index.html
+++ b/base_export_async/static/description/index.html
@@ -1,20 +1,20 @@
-
-
-Base Export Async
+
+README.rst
-
-
Base Export Async
+
+
+
+
+
+
+
Base Export Async
-
+
Standard Export can be delayed in asynchronous jobs executed in the background and then send by email to the user.
Table of contents
-
+
The user is presented with a new checkbox “Asynchronous export”
in the export screen. When selected, the export is delayed in a
background job.
@@ -391,36 +398,42 @@
to the user who execute the export.
-
+
Bugs are tracked on GitHub Issues .
In case of trouble, please check there if your issue has already been reported.
-If you spotted it first, help us smashing it by providing a detailed and welcomed
-feedback .
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+
feedback .
Do not contact contributors directly about support or help with technical issues.
-
+
-
-
Arnaud Pineux (ACSONE SA/NV) authored the initial prototype.
+
+
+Arnaud Pineux (ACSONE SA/NV) authored the initial prototype.
+Guewen Baconnier (Camptocamp)
+
-
+
This module is maintained by the OCA.
-
+
+
+
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.
-
This module is part of the OCA/queue project on GitHub.
+
This module is part of the OCA/queue project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute .
+
diff --git a/base_export_async/static/src/js/data_export.js b/base_export_async/static/src/js/data_export.js
index 06a7af02a0..29f4aea6c5 100644
--- a/base_export_async/static/src/js/data_export.js
+++ b/base_export_async/static/src/js/data_export.js
@@ -1,11 +1,11 @@
-odoo.define('base_export_async.DataExport', function(require) {
+odoo.define("base_export_async.DataExport", function (require) {
"use strict";
- var core = require('web.core');
- var DataExport = require('web.DataExport');
- var framework = require('web.framework');
- var pyUtils = require('web.py_utils');
- var Dialog = require('web.Dialog');
+ var core = require("web.core");
+ var DataExport = require("web.DataExport");
+ var framework = require("web.framework");
+ var pyUtils = require("web.py_utils");
+ var Dialog = require("web.Dialog");
var _t = core._t;
DataExport.include({
@@ -14,83 +14,54 @@ odoo.define('base_export_async.DataExport', function(require) {
A flag (checkbox) Async is added and if checked, call the
delay export instead of the standard export.
*/
- start: function() {
+ start: function () {
this._super.apply(this, arguments);
- this.async = this.$('#async_export');
+ this.async = this.$("#async_export");
},
- export_data: function() {
- var self = this;
- if (self.async.is(":checked")) {
+ _exportData(exportedFields, exportFormat, idsToExport) {
+ if (this.async && this.async.is(":checked")) {
/*
Checks from the standard method
*/
- var exported_fields = this.$(
- '.o_fields_list option').map(
- function() {
- return {
- name: (self.records[this.value] ||
- this).value,
- label: this.textContent ||
- this.innerText
- };
- }).get();
-
- if (_.isEmpty(exported_fields)) {
- Dialog.alert(this, _t(
- "Please select fields to export..."
- ));
+ if (_.isEmpty(exportedFields)) {
+ Dialog.alert(this, _t("Please select fields to export..."));
return;
}
- if (!this.isCompatibleMode) {
- exported_fields.unshift({
- name: 'id',
- label: _t('External ID')
- });
+ if (this.isCompatibleMode) {
+ exportedFields.unshift({name: "id", label: _t("External ID")});
}
- var export_format = this.$export_format_inputs
- .filter(':checked').val();
-
/*
Call the delay export if Async is checked
*/
framework.blockUI();
this._rpc({
- model: 'delay.export',
- method: 'delay_export',
- args: [{
- data: JSON.stringify({
- format: export_format,
- model: this
- .record
- .model,
- fields: exported_fields,
- ids: this
- .ids_to_export,
- domain: this
- .domain,
- context: pyUtils
- .eval(
- 'contexts', [
- this
- .record
- .getContext()
- ]
- ),
- import_compat:
- !!
- this
- .$import_compat_radios
- .filter(
- ':checked'
- ).val(),
- })
- }],
- }).then(function(result) {
+ model: "delay.export",
+ method: "delay_export",
+ args: [
+ {
+ data: JSON.stringify({
+ format: exportFormat,
+ model: this.record.model,
+ fields: exportedFields,
+ ids: idsToExport,
+ domain: this.domain,
+ context: pyUtils.eval("contexts", [
+ this.record.getContext(),
+ ]),
+ import_compat: this.isCompatibleMode,
+ user_ids: [this.record.context.uid],
+ }),
+ },
+ ],
+ }).then(function () {
framework.unblockUI();
- Dialog.alert(this, _t(
- "You will receive the export file by email as soon as it is finished."
- ));
+ Dialog.alert(
+ this,
+ _t(
+ "You will receive the export file by email as soon as it is finished."
+ )
+ );
});
} else {
/*
diff --git a/base_export_async/static/src/xml/base.xml b/base_export_async/static/src/xml/base.xml
index 24f53a000f..97b04cfeb1 100644
--- a/base_export_async/static/src/xml/base.xml
+++ b/base_export_async/static/src/xml/base.xml
@@ -1,12 +1,18 @@
-
+
-
-
-
-
- Asynchronous export (You will receive the export by email)
+
+
+
+
+ Asynchronous export (You will receive the export by email)
diff --git a/base_export_async/tests/test_base_export_async.py b/base_export_async/tests/test_base_export_async.py
index fae4b6264d..e93bce58c0 100644
--- a/base_export_async/tests/test_base_export_async.py
+++ b/base_export_async/tests/test_base_export_async.py
@@ -2,10 +2,14 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import json
+from unittest import mock
-import odoo.tests.common as common
+import freezegun
from dateutil.relativedelta import relativedelta
+
+import odoo.tests.common as common
from odoo import fields
+from odoo.http import _request_stack
data_csv = {
"data": """{"format": "csv", "model": "res.partner",
@@ -16,7 +20,9 @@
"ids": false,
"domain": [],
"context": {"lang": "en_US", "tz": "Europe/Brussels", "uid": 2},
- "import_compat": false}"""
+ "import_compat": false,
+ "user_ids": [2]
+ }"""
}
data_xls = {
@@ -28,25 +34,33 @@
"ids": false,
"domain": [],
"context": {"lang": "en_US", "tz": "Europe/Brussels", "uid": 2},
- "import_compat": false}"""
+ "import_compat": false,
+ "user_ids": [2]
+ }"""
}
class TestBaseExportAsync(common.TransactionCase):
def setUp(self):
- super(TestBaseExportAsync, self).setUp()
+ super().setUp()
self.delay_export_obj = self.env["delay.export"]
self.job_obj = self.env["queue.job"]
+ _request_stack.push(
+ mock.Mock(
+ env=self.env,
+ )
+ )
+ self.addCleanup(_request_stack.pop)
def test_delay_export(self):
- """ Check that the call create a new JOB"""
+ """Check that the call create a new JOB"""
nbr_job = len(self.job_obj.search([]))
self.delay_export_obj.delay_export(data_csv)
new_nbr_job = len(self.job_obj.search([]))
self.assertEqual(new_nbr_job, nbr_job + 1)
def test_export_csv(self):
- """ Check that the export generate an attachment and email"""
+ """Check that the export generate an attachment and email"""
params = json.loads(data_csv.get("data"))
mails = self.env["mail.mail"].search([])
attachments = self.env["ir.attachment"].search([])
@@ -54,10 +68,10 @@ def test_export_csv(self):
new_mail = self.env["mail.mail"].search([]) - mails
new_attachment = self.env["ir.attachment"].search([]) - attachments
self.assertEqual(len(new_mail), 1)
- self.assertEqual(new_attachment.datas_fname, "res.partner.csv")
+ self.assertEqual(new_attachment.name, "res.partner.csv")
def test_export_xls(self):
- """ Check that the export generate an attachment and email"""
+ """Check that the export generate an attachment and email"""
params = json.loads(data_xls.get("data"))
mails = self.env["mail.mail"].search([])
attachments = self.env["ir.attachment"].search([])
@@ -65,10 +79,10 @@ def test_export_xls(self):
new_mail = self.env["mail.mail"].search([]) - mails
new_attachment = self.env["ir.attachment"].search([]) - attachments
self.assertEqual(len(new_mail), 1)
- self.assertEqual(new_attachment.datas_fname, "res.partner.xls")
+ self.assertEqual(new_attachment.name, "res.partner.xls")
def test_cron_delete(self):
- """ Check that cron delete attachment after TTL"""
+ """Check that cron delete attachment after TTL"""
params = json.loads(data_csv.get("data"))
attachments = self.env["ir.attachment"].search([])
self.delay_export_obj.export(params)
@@ -76,10 +90,29 @@ def test_cron_delete(self):
time_to_live = (
self.env["ir.config_parameter"].sudo().get_param("attachment.ttl", 7)
)
- date_today = fields.Date.today()
- date_to_delete = date_today + relativedelta(days=-int(time_to_live))
- # Update create_date with today - TTL
- self.delay_export_obj.search([]).write({"create_date": date_to_delete})
- self.delay_export_obj.sudo().cron_delete()
+ date_today = fields.Datetime.now()
+ date_past_ttl = date_today + relativedelta(days=int(time_to_live))
+ with freezegun.freeze_time(date_past_ttl):
+ self.delay_export_obj.cron_delete()
+
# The attachment must be deleted
self.assertFalse(new_attachment.exists())
+
+ def test_portal_export(self):
+ """Check that we make attachments externally accessible for portal users"""
+ portal_user = self.env["res.users"].create(
+ {
+ "login": "base_export_async_portal_user",
+ "name": "base_export_async_portal_user",
+ "groups_id": self.env.ref("base.group_portal").ids,
+ }
+ )
+ params = json.loads(data_csv.get("data"))
+ params["user_ids"] = portal_user.ids
+ attachments = self.env["ir.attachment"].search([])
+ mails = self.env["mail.mail"].search([])
+ self.delay_export_obj.export(params)
+ new_attachment = self.env["ir.attachment"].search([]) - attachments
+ self.assertTrue(new_attachment.access_token)
+ new_mail = self.env["mail.mail"].search([]) - mails
+ self.assertIn("&access_token=", new_mail.body)
diff --git a/base_export_async/views/assets.xml b/base_export_async/views/assets.xml
index f0a778851c..24f276ba7e 100644
--- a/base_export_async/views/assets.xml
+++ b/base_export_async/views/assets.xml
@@ -1,8 +1,15 @@
-
+
-
+
-
+
diff --git a/base_import_async/README.rst b/base_import_async/README.rst
index 28b4eaaf89..603e106012 100644
--- a/base_import_async/README.rst
+++ b/base_import_async/README.rst
@@ -2,10 +2,13 @@
Asynchronous Import
===================
-.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:8bfdc88778ec5afb71ddb0d5d1c9f31e0cdceeef1003c5e13644b734e3b377be
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
:target: https://odoo-community.org/page/development-status
@@ -14,16 +17,16 @@ Asynchronous Import
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fqueue-lightgray.png?logo=github
- :target: https://github.com/OCA/queue/tree/13.0/base_import_async
+ :target: https://github.com/OCA/queue/tree/14.0/base_import_async
:alt: OCA/queue
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
- :target: https://translation.odoo-community.org/projects/queue-13-0/queue-13-0-base_import_async
+ :target: https://translation.odoo-community.org/projects/queue-14-0/queue-14-0-base_import_async
:alt: Translate me on Weblate
-.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
- :target: https://runbot.odoo-community.org/runbot/230/13.0
- :alt: Try me on Runbot
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=14.0
+ :alt: Try me on Runboat
-|badge1| |badge2| |badge3| |badge4| |badge5|
+|badge1| |badge2| |badge3| |badge4| |badge5|
This module extends the standard CSV import functionality
to import files in the background using the OCA/queue
@@ -88,8 +91,8 @@ Bug Tracker
Bugs are tracked on `GitHub Issues `_.
In case of trouble, please check there if your issue has already been reported.
-If you spotted it first, help us smashing it by providing a detailed and welcomed
-`feedback `_.
+If you spotted it first, help us to smash it by providing a detailed and welcomed
+`feedback `_.
Do not contact contributors directly about support or help with technical issues.
@@ -133,6 +136,6 @@ 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.
-This module is part of the `OCA/queue `_ project on GitHub.
+This module is part of the `OCA/queue `_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/base_import_async/__manifest__.py b/base_import_async/__manifest__.py
index 35901ecc46..e33c4c7f14 100644
--- a/base_import_async/__manifest__.py
+++ b/base_import_async/__manifest__.py
@@ -5,7 +5,7 @@
{
"name": "Asynchronous Import",
"summary": "Import CSV files in the background",
- "version": "13.0.2.0.0",
+ "version": "14.0.1.0.2",
"author": "Akretion, ACSONE SA/NV, Odoo Community Association (OCA)",
"license": "AGPL-3",
"website": "https://github.com/OCA/queue",
@@ -13,6 +13,6 @@
"depends": ["base_import", "queue_job"],
"data": ["data/queue_job_function_data.xml", "views/base_import_async.xml"],
"qweb": ["static/src/xml/import.xml"],
- "installable": False,
+ "installable": True,
"development_status": "Production/Stable",
}
diff --git a/base_import_async/data/queue_job_function_data.xml b/base_import_async/data/queue_job_function_data.xml
index 22cc8dbab0..fb04a63613 100644
--- a/base_import_async/data/queue_job_function_data.xml
+++ b/base_import_async/data/queue_job_function_data.xml
@@ -1,7 +1,13 @@
+
+ base_import
+
+
+
_split_file
+
_import_one_chunk
+
\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.10.4\n"
+
+#. module: base_import_async
+#: code:addons/base_import_async/models/queue_job.py:0
+#, python-format
+msgid "Attachment"
+msgstr ""
+
+#. module: base_import_async
+#: model:ir.model,name:base_import_async.model_base_import_import
+msgid "Base Import"
+msgstr ""
+
+#. module: base_import_async
+#: model:ir.model.fields,field_description:base_import_async.field_base_import_import__display_name
+#: model:ir.model.fields,field_description:base_import_async.field_queue_job__display_name
+msgid "Display Name"
+msgstr "Nome visualizzato"
+
+#. module: base_import_async
+#: model:ir.model.fields,field_description:base_import_async.field_base_import_import__id
+#: model:ir.model.fields,field_description:base_import_async.field_queue_job__id
+msgid "ID"
+msgstr ""
+
+#. module: base_import_async
+#: code:addons/base_import_async/models/base_import_import.py:0
+#, python-format
+msgid "Import %s from file %s"
+msgstr ""
+
+#. module: base_import_async
+#: code:addons/base_import_async/models/base_import_import.py:0
+#, python-format
+msgid "Import %s from file %s - #%s - lines %s to %s"
+msgstr ""
+
+#. module: base_import_async
+#. openerp-web
+#: code:addons/base_import_async/static/src/xml/import.xml:0
+#, python-format
+msgid "Import in the background"
+msgstr "Importa con le code"
+
+#. module: base_import_async
+#: model:ir.model.fields,field_description:base_import_async.field_base_import_import____last_update
+#: model:ir.model.fields,field_description:base_import_async.field_queue_job____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: base_import_async
+#: model:ir.model,name:base_import_async.model_queue_job
+msgid "Queue Job"
+msgstr ""
+
+#. module: base_import_async
+#. openerp-web
+#: code:addons/base_import_async/static/src/xml/import.xml:0
+#, python-format
+msgid ""
+"When checked, the import will be executed as a background job, after "
+"splitting your file in small chunks that will be processed independently. "
+"Use this to import very large files."
+msgstr ""
+
+#. module: base_import_async
+#. openerp-web
+#: code:addons/base_import_async/static/src/js/import.js:0
+#, python-format
+msgid "You can check the status of this job in menu 'Queue / Jobs'."
+msgstr ""
+
+#. module: base_import_async
+#. openerp-web
+#: code:addons/base_import_async/static/src/js/import.js:0
+#, python-format
+msgid "Your request is being processed"
+msgstr ""
diff --git a/base_import_async/i18n/ro.po b/base_import_async/i18n/ro.po
new file mode 100644
index 0000000000..54a5702e95
--- /dev/null
+++ b/base_import_async/i18n/ro.po
@@ -0,0 +1,93 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * base_import_async
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 14.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ro\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < "
+"20)) ? 1 : 2;\n"
+
+#. module: base_import_async
+#: code:addons/base_import_async/models/queue_job.py:0
+#, python-format
+msgid "Attachment"
+msgstr ""
+
+#. module: base_import_async
+#: model:ir.model,name:base_import_async.model_base_import_import
+msgid "Base Import"
+msgstr ""
+
+#. module: base_import_async
+#: model:ir.model.fields,field_description:base_import_async.field_base_import_import__display_name
+#: model:ir.model.fields,field_description:base_import_async.field_queue_job__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: base_import_async
+#: model:ir.model.fields,field_description:base_import_async.field_base_import_import__id
+#: model:ir.model.fields,field_description:base_import_async.field_queue_job__id
+msgid "ID"
+msgstr ""
+
+#. module: base_import_async
+#: code:addons/base_import_async/models/base_import_import.py:0
+#, python-format
+msgid "Import %s from file %s"
+msgstr ""
+
+#. module: base_import_async
+#: code:addons/base_import_async/models/base_import_import.py:0
+#, python-format
+msgid "Import %s from file %s - #%s - lines %s to %s"
+msgstr ""
+
+#. module: base_import_async
+#. openerp-web
+#: code:addons/base_import_async/static/src/xml/import.xml:0
+#, python-format
+msgid "Import in the background"
+msgstr ""
+
+#. module: base_import_async
+#: model:ir.model.fields,field_description:base_import_async.field_base_import_import____last_update
+#: model:ir.model.fields,field_description:base_import_async.field_queue_job____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: base_import_async
+#: model:ir.model,name:base_import_async.model_queue_job
+msgid "Queue Job"
+msgstr ""
+
+#. module: base_import_async
+#. openerp-web
+#: code:addons/base_import_async/static/src/xml/import.xml:0
+#, python-format
+msgid ""
+"When checked, the import will be executed as a background job, after "
+"splitting your file in small chunks that will be processed independently. "
+"Use this to import very large files."
+msgstr ""
+
+#. module: base_import_async
+#. openerp-web
+#: code:addons/base_import_async/static/src/js/import.js:0
+#, python-format
+msgid "You can check the status of this job in menu 'Queue / Jobs'."
+msgstr ""
+
+#. module: base_import_async
+#. openerp-web
+#: code:addons/base_import_async/static/src/js/import.js:0
+#, python-format
+msgid "Your request is being processed"
+msgstr ""
diff --git a/base_import_async/models/base_import_import.py b/base_import_async/models/base_import_import.py
index 4db93532bf..ff84a4d560 100644
--- a/base_import_async/models/base_import_import.py
+++ b/base_import_async/models/base_import_import.py
@@ -111,8 +111,7 @@ def _read_csv_attachment(self, attachment, options):
@staticmethod
def _extract_chunks(model_obj, fields, data, chunk_size):
- """ Split the data on record boundaries,
- in chunks of minimum chunk_size """
+ """Split the data on record boundaries, in chunks of minimum chunk_size"""
fields = list(map(fix_import_export_id_paths, fields))
row_from = 0
for rows in model_obj._extract_records(fields, data):
@@ -131,7 +130,7 @@ def _split_file(
options,
file_name="file.csv",
):
- """ Split a CSV attachment in smaller import jobs """
+ """Split a CSV attachment in smaller import jobs"""
model_obj = self.env[model_name]
fields, data = self._read_csv_attachment(attachment, options)
padding = len(str(len(data)))
diff --git a/base_import_async/models/queue_job.py b/base_import_async/models/queue_job.py
index fd1faec28e..e3594f3568 100644
--- a/base_import_async/models/queue_job.py
+++ b/base_import_async/models/queue_job.py
@@ -5,7 +5,7 @@
class QueueJob(models.Model):
- """ Job status and result """
+ """Job status and result"""
_inherit = "queue.job"
diff --git a/base_import_async/static/description/index.html b/base_import_async/static/description/index.html
index 4cebaf7dcc..fa11481572 100644
--- a/base_import_async/static/description/index.html
+++ b/base_import_async/static/description/index.html
@@ -1,20 +1,20 @@
-
+
-
+
Asynchronous Import
+
+
+
+
Scheduled Asynchronous Export
+
+
+
+
Add a new Automation feature: Scheduled Exports.
+Based on an export list and a domain, an email is sent every X
+hours/days/weeks/months to a selection of users.
+
Table of contents
+
+
+
+
The configuration of a scheduled export is based on export lists.
+
To create an export list:
+
+open the list view of the model to export
+select at least one record, and open “Action → Export”
+select the fields to export and save using “Save fields list”.
+
+
To configure a scheduled export:
+
+open “Settings → Technical → Automation → Scheduled Exports”
+create a scheduled export by filling the form
+
+
A Scheduled Action named “Send Scheduled Exports” checks every hour
+if Scheduled Exports have to be executed.
+
+
+
+
When the configuration of a Scheduled Export is done, their execution
+is automatic.
+
Users will receive an email containing a link to download the exported file at
+the specified frequency. The attachments stay in the database for 7 days by
+default (it can be changed with the system parameter attachment.ttl .
+
+
+
+
+We could configure a custom TTL (time-to-live) for each scheduled export
+
+
+
+
+
Bugs are tracked on GitHub 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 .
+
Do not contact contributors directly about support or help with technical issues.
+
+
+
+
+
+
+
+Guewen Baconnier (Camptocamp)
+Komit :
+
+
+
+
+
+
The migration of this module from 13.0 to 14.0 was financially supported by:
+
+
+
+
+
This module is maintained by the OCA.
+
+
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.
+
Current maintainer :
+
+
This module is part of the OCA/queue project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute .
+
+
+
+
+
diff --git a/export_async_schedule/tests/__init__.py b/export_async_schedule/tests/__init__.py
new file mode 100644
index 0000000000..69f905f9cf
--- /dev/null
+++ b/export_async_schedule/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_export_async_schedule
diff --git a/export_async_schedule/tests/test_export_async_schedule.py b/export_async_schedule/tests/test_export_async_schedule.py
new file mode 100644
index 0000000000..da4a03995a
--- /dev/null
+++ b/export_async_schedule/tests/test_export_async_schedule.py
@@ -0,0 +1,189 @@
+# Copyright 2019 Camptocamp
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from datetime import datetime
+
+from dateutil.relativedelta import relativedelta
+
+from odoo.tests import common
+
+from odoo.addons.queue_job.tests.common import mock_with_delay
+
+
+class TestExportAsyncSchedule(common.SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.Schedule = cls.env["export.async.schedule"]
+ cls._create_schedule()
+
+ @classmethod
+ def _create_schedule(cls):
+ cls.ir_export = cls.env["ir.exports"].create(
+ {
+ "name": "test",
+ "resource": "res.partner",
+ "export_fields": [
+ (0, 0, {"name": "display_name"}),
+ (0, 0, {"name": "email"}),
+ (0, 0, {"name": "phone"}),
+ (0, 0, {"name": "title/shortcut"}),
+ ],
+ }
+ )
+ model = cls.env["ir.model"].search([("model", "=", "res.partner")])
+ user = cls.env.ref("base.user_admin")
+ cls.schedule = cls.Schedule.create(
+ {
+ "model_id": model.id,
+ "user_ids": [(4, user.id)],
+ "domain": '[("is_company", "=", True)]',
+ "ir_export_id": cls.ir_export.id,
+ "export_format": "csv",
+ "import_compat": True,
+ "lang": "en_US",
+ }
+ )
+
+ def test_fields_with_labels(self):
+ export_fields = [
+ "display_name",
+ "email",
+ "phone",
+ "title/shortcut",
+ "parent_id/company_id/name",
+ ]
+ result = self.env["export.async.schedule"]._get_fields_with_labels(
+ "res.partner", export_fields
+ )
+ expected = [
+ {"label": "Display Name", "name": "display_name"},
+ {"label": "Email", "name": "email"},
+ {"label": "Phone", "name": "phone"},
+ {"label": "Title/Abbreviation", "name": "title/shortcut"},
+ {
+ "label": "Related Company/Company/Company Name",
+ "name": "parent_id/company_id/name",
+ },
+ ]
+ self.assertEqual(result, expected)
+
+ def test_prepare_export_params_compatible(self):
+ prepared = self.schedule._prepare_export_params()
+ expected = {
+ "context": {},
+ "domain": [("is_company", "=", True)],
+ # in 'import compatible' mode, the header (label)
+ # is equal to the field name
+ "fields": [
+ {"label": "display_name", "name": "display_name"},
+ {"label": "email", "name": "email"},
+ {"label": "phone", "name": "phone"},
+ {"label": "title/shortcut", "name": "title/shortcut"},
+ ],
+ "format": "csv",
+ "ids": False,
+ "import_compat": True,
+ "model": "res.partner",
+ "user_ids": [self.env.ref("base.user_admin").id],
+ }
+ self.assertDictEqual(prepared, expected)
+
+ def test_prepare_export_params_friendly(self):
+ self.schedule.import_compat = False
+ prepared = self.schedule._prepare_export_params()
+ expected = {
+ "context": {},
+ "domain": [("is_company", "=", True)],
+ # in 'import compatible' mode, the header (label)
+ # is equal to the field name
+ "fields": [
+ {"label": "Display Name", "name": "display_name"},
+ {"label": "Email", "name": "email"},
+ {"label": "Phone", "name": "phone"},
+ {"label": "Title/Abbreviation", "name": "title/shortcut"},
+ ],
+ "format": "csv",
+ "ids": False,
+ "import_compat": False,
+ "model": "res.partner",
+ "user_ids": [self.env.ref("base.user_admin").id],
+ }
+ self.assertDictEqual(prepared, expected)
+
+ def test_schedule_next_date(self):
+ start_date = datetime.now() + relativedelta(hours=1)
+
+ def assert_next_schedule(interval, unit, expected):
+ self.schedule.next_execution = start_date
+ self.schedule.interval = interval
+ self.schedule.interval_unit = unit
+
+ self.assertEqual(self.schedule._compute_next_date(), expected)
+
+ assert_next_schedule(1, "hours", start_date + relativedelta(hours=1))
+ assert_next_schedule(2, "hours", start_date + relativedelta(hours=2))
+ assert_next_schedule(1, "days", start_date + relativedelta(days=1))
+ assert_next_schedule(2, "days", start_date + relativedelta(days=2))
+ assert_next_schedule(1, "weeks", start_date + relativedelta(weeks=1))
+ assert_next_schedule(2, "weeks", start_date + relativedelta(weeks=2))
+ assert_next_schedule(1, "months", start_date + relativedelta(months=1))
+ assert_next_schedule(2, "months", start_date + relativedelta(months=2))
+
+ self.schedule.end_of_month = True
+ assert_next_schedule(
+ 1,
+ "months",
+ start_date + relativedelta(months=1, day=31, hour=23, minute=59, second=59),
+ )
+ assert_next_schedule(
+ 2,
+ "months",
+ start_date + relativedelta(months=2, day=31, hour=23, minute=59, second=59),
+ )
+
+ def test_run_schedule(self):
+ in_future = datetime.now() + relativedelta(minutes=1)
+ self.schedule.next_execution = in_future
+ self.schedule.run_schedule()
+ # nothing happened because we have not reached the next execution
+ self.assertEqual(self.schedule.next_execution, in_future)
+
+ in_past = datetime.now() - relativedelta(minutes=1)
+ self.schedule.next_execution = in_past
+ self.schedule.run_schedule()
+ # it has been executed and the date changed to the next execution
+ self.assertGreater(self.schedule.next_execution, in_past)
+
+ def test_delay_job(self):
+ with mock_with_delay() as (delayable_cls, delayable):
+ self.schedule.action_export()
+
+ # check 'with_delay()' part:
+ self.assertEqual(delayable_cls.call_count, 1)
+ # arguments passed in 'with_delay()'
+ delay_args, __ = delayable_cls.call_args
+ self.assertEqual((self.env["delay.export"],), delay_args)
+
+ # check what's passed to the job method 'export'
+ self.assertEqual(delayable.export.call_count, 1)
+ delay_args, delay_kwargs = delayable.export.call_args
+ expected_params = (
+ {
+ "context": {"lang": "en_US"},
+ "domain": [("is_company", "=", True)],
+ "fields": [
+ {"label": "display_name", "name": "display_name"},
+ {"label": "email", "name": "email"},
+ {"label": "phone", "name": "phone"},
+ {"label": "title/shortcut", "name": "title/shortcut"},
+ ],
+ "format": "csv",
+ "ids": False,
+ "import_compat": True,
+ "model": "res.partner",
+ "user_ids": [2],
+ },
+ )
+
+ self.assertEqual(delay_args, expected_params)
diff --git a/export_async_schedule/views/export_async_schedule_views.xml b/export_async_schedule/views/export_async_schedule_views.xml
new file mode 100644
index 0000000000..23a1700539
--- /dev/null
+++ b/export_async_schedule/views/export_async_schedule_views.xml
@@ -0,0 +1,127 @@
+
+
+
+
+ export.async.schedule.tree
+ export.async.schedule
+
+
+
+
+
+
+
+
+
+
+
+
+ export.async.schedule.form
+ export.async.schedule
+
+
+
+
+
+
+ export.async.schedule.search
+ export.async.schedule
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Scheduled Exports
+ ir.actions.act_window
+ export.async.schedule
+ Schedule Exports to send by email
+
+
+
+
+
diff --git a/queue_job/README.rst b/queue_job/README.rst
index 0532d27ba6..875c2a48cd 100644
--- a/queue_job/README.rst
+++ b/queue_job/README.rst
@@ -1,16 +1,23 @@
+.. 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:d7d006c41953034faf1dfee702a1887c0908c92b5744d6d3e48dfe52a7f2461b
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/licence-LGPL--3-blue.png
+.. |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
@@ -19,11 +26,11 @@ Job Queue
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/queue-14-0/queue-14-0-queue_job
:alt: Translate me on Weblate
-.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
- :target: https://runbot.odoo-community.org/runbot/230/14.0
- :alt: Try me on Runbot
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=14.0
+ :alt: Try me on Runboat
-|badge1| |badge2| |badge3| |badge4| |badge5|
+|badge1| |badge2| |badge3| |badge4| |badge5|
This addon adds an integrated Job Queue to Odoo.
@@ -139,7 +146,156 @@ To use this module, you need to:
Developers
~~~~~~~~~~
-**Configure default options for jobs**
+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``
@@ -159,7 +315,7 @@ Example of job function:
.. code-block:: XML
-
+
action_done
@@ -177,6 +333,13 @@ 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
+````
+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``.
@@ -252,18 +415,42 @@ Based on this configuration, we can tell that:
* 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 empty for backward compatibility. The allow-list can
+be customized in ``Base._job_prepare_context_before_enqueue_keys``.
+
+Example:
+
+.. code-block:: python
+
+ class Base(models.AbstractModel):
+
+ _inherit = "base"
+
+ @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")
+
**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 `TEST_QUEUE_JOB_NO_DELAY=1` in your enviroment.
+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 `test_queue_job_no_delay=True` in the context.
+you can set `queue_job__no_delay=True` in the context.
Tip: you can do this at test case level like this
@@ -274,12 +461,157 @@ Tip: you can do this at test case level like this
super().setUpClass()
cls.env = cls.env(context=dict(
cls.env.context,
- test_queue_job_no_delay=True, # no jobs thanks
+ 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
======================
@@ -329,7 +661,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues `_.
In case of trouble, please check there if your issue has already been reported.
-If you spotted it first, help us smashing it by providing a detailed and welcomed
+If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback `_.
Do not contact contributors directly about support or help with technical issues.
@@ -357,6 +689,7 @@ Contributors
* Tatiana Deribina
* Souheil Bejaoui
* Eric Antones
+* Simone Orsi
Maintainers
~~~~~~~~~~~
diff --git a/queue_job/__init__.py b/queue_job/__init__.py
index 75f80cf5aa..19ffa60c25 100644
--- a/queue_job/__init__.py
+++ b/queue_job/__init__.py
@@ -3,7 +3,7 @@
from . import models
from . import wizards
from . import jobrunner
-from .hooks.post_init_hook import post_init_hook
+from .post_init_hook import post_init_hook
# shortcuts
from .job import identity_exact
diff --git a/queue_job/__manifest__.py b/queue_job/__manifest__.py
index 41dd5bfe66..e25e4742e9 100644
--- a/queue_job/__manifest__.py
+++ b/queue_job/__manifest__.py
@@ -1,22 +1,23 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
-
{
"name": "Job Queue",
- "version": "14.0.1.0.0",
+ "version": "14.0.3.15.1",
"author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/queue",
"license": "LGPL-3",
"category": "Generic Modules",
- "depends": ["mail"],
+ "depends": ["mail", "base_sparse_field"],
"external_dependencies": {"python": ["requests"]},
"data": [
"security/security.xml",
"security/ir.model.access.csv",
+ "views/queue_job_assets.xml",
"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",
diff --git a/queue_job/controllers/main.py b/queue_job/controllers/main.py
index a0814bbbd5..ddec4d95ca 100644
--- a/queue_job/controllers/main.py
+++ b/queue_job/controllers/main.py
@@ -3,16 +3,19 @@
# 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
-from werkzeug.exceptions import Forbidden
+from psycopg2 import OperationalError, errorcodes
+from werkzeug.exceptions import BadRequest, Forbidden
import odoo
from odoo import _, http, 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
@@ -20,6 +23,8 @@
PG_RETRY = 5 # seconds
+DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE = 5
+
class RunJobController(http.Controller):
def _try_perform_job(self, env, job):
@@ -30,12 +35,44 @@ def _try_perform_job(self, env, job):
_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["base"].flush()
job.set_done()
job.store()
env["base"].flush()
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
@@ -57,7 +94,7 @@ def retry_postpone(job, message, seconds=None):
(job_uuid, ENQUEUED),
)
if not env.cr.fetchone():
- _logger.warn(
+ _logger.warning(
"was requested to run job %s, but it does not exist, "
"or is not in state %s",
job_uuid,
@@ -77,10 +114,10 @@ def retry_postpone(job, message, seconds=None):
if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
raise
- retry_postpone(
- job, tools.ustr(err.pgerror, errors="replace"), seconds=PG_RETRY
- )
_logger.debug("%s OperationalError, postponed", job)
+ raise RetryableJobError(
+ tools.ustr(err.pgerror, errors="replace"), seconds=PG_RETRY
+ )
except NothingToDoJob as err:
if str(err):
@@ -95,29 +132,84 @@ def retry_postpone(job, message, seconds=None):
# 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):
+ except (FailedJobError, Exception) as orig_exception:
buff = StringIO()
traceback.print_exc(file=buff)
- _logger.error(buff.getvalue())
+ traceback_txt = buff.getvalue()
+ _logger.error(traceback_txt)
job.env.clear()
with odoo.api.Environment.manage():
with odoo.registry(job.env.cr.dbname).cursor() as new_cr:
job.env = job.env(cr=new_cr)
- job.set_failed(exc_info=buff.getvalue())
+ vals = self._get_failure_values(job, traceback_txt, orig_exception)
+ job.set_failed(**vals)
job.store()
new_cr.commit()
+ 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,
+ }
+
@http.route("/queue_job/create_test_job", type="http", auth="user")
def create_test_job(
- self, priority=None, max_retries=None, channel="root", description="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)
@@ -130,6 +222,35 @@ def create_test_job(
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(
@@ -138,7 +259,57 @@ def create_test_job(
channel=channel,
description=description,
)
- ._test_job()
+ ._test_job(failure_rate=failure_rate)
)
+ return "job uuid: %s" % (delayed.db_record().uuid,)
- return delayed.db_record().uuid
+ TEST_GRAPH_MAX_PER_GROUP = 5
+
+ # flake8: noqa: C901
+ 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,
+ )
diff --git a/queue_job/delay.py b/queue_job/delay.py
new file mode 100644
index 0000000000..d00736fbe8
--- /dev/null
+++ b/queue_job/delay.py
@@ -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):
+ job_method = ""
+ if self._job_method:
+ job_method = self._job_method.__name__
+ return "Delayable({}.{}({}, {}))".format(
+ self.recordset, job_method, 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__
diff --git a/queue_job/fields.py b/queue_job/fields.py
index 50183993d8..8b440fc456 100644
--- a/queue_job/fields.py
+++ b/queue_job/fields.py
@@ -69,6 +69,9 @@ def convert_to_record(self, value, record):
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 {
@@ -77,6 +80,7 @@ def default(self, obj):
"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()}
@@ -107,7 +111,8 @@ def object_hook(self, 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"])
diff --git a/queue_job/i18n/de.po b/queue_job/i18n/de.po
index 0ce7ee2439..10ddd4452a 100644
--- a/queue_job/i18n/de.po
+++ b/queue_job/i18n/de.po
@@ -44,7 +44,6 @@ msgstr "Aktivitäten"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
#, fuzzy
-#| msgid "Exception Information"
msgid "Activity Exception Decoration"
msgstr "Exception-Information"
@@ -53,6 +52,11 @@ msgstr "Exception-Information"
msgid "Activity State"
msgstr "Aktivitätsstatus"
+#. 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"
@@ -77,18 +81,47 @@ msgstr "Basis"
#. 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 "Abbrechen"
+#. 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
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
+msgid "Cancelled by %s"
+msgstr ""
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job_channel.py:0
+#, python-format
msgid "Cannot change the root channel"
msgstr "Der Root-Kanal kann nicht geändert werden"
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: code:addons/queue_job/models/queue_job_channel.py:0
#, python-format
msgid "Cannot remove the root channel"
msgstr "Der Root-Kanal kann nicht entfernt werden"
@@ -101,11 +134,6 @@ msgstr "Der Root-Kanal kann nicht entfernt werden"
msgid "Channel"
msgstr "Kanal"
-#. module: queue_job
-#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
-msgid "Channel Method Name"
-msgstr "Kanal-Methodenname"
-
#. module: queue_job
#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
msgid "Channel complete name must be unique"
@@ -124,6 +152,11 @@ msgstr "Kanäle"
msgid "Company"
msgstr "Unternehmen"
+#. 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
@@ -137,13 +170,20 @@ msgstr "Erstellt am"
#. 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_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 "Erstellt von"
+#. 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_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"
@@ -159,20 +199,38 @@ msgstr "Aktueller Versuch"
msgid "Current try / max. retries"
msgstr "Aktueller Versuch / max. Anzahl der Wiederholung"
+#. 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 "Erledigt am"
+#. 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 "Beschreibung"
#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__display_name
#: 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_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"
@@ -195,6 +253,12 @@ msgstr "Zeit der Einreihung"
msgid "Enqueued"
msgstr "Eingereiht"
+#. 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"
@@ -205,11 +269,31 @@ msgstr "Exception-Info"
msgid "Exception Information"
msgstr "Exception-Information"
+#. 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 "Erst ausführen nach"
+#. 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
@@ -241,6 +325,31 @@ msgstr "Abonnenten (Kanäle)"
msgid "Followers (Partners)"
msgstr "Abonnenten (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 ""
+
+#. 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
@@ -248,9 +357,11 @@ msgid "Group By"
msgstr "Gruppieren nach"
#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__id
#: 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_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"
@@ -274,7 +385,6 @@ msgstr "Identitätsschlüssel"
#. module: queue_job
#: code:addons/queue_job/models/queue_job.py:0
#, fuzzy, python-format
-#| msgid "The selected jobs will be requeued."
msgid "If both parameters are 0, ALL jobs will be requeued!"
msgstr "Die ausgewählten Jobs werden erneut eingereiht."
@@ -286,13 +396,12 @@ msgstr "Wenn es gesetzt ist, erfordern neue Nachrichten Ihre Aufmerksamkeit."
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
-#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_sms_error
msgid "If checked, some messages have a delivery error."
msgstr ""
"Wenn es gesetzt ist, gibt es einige Nachrichten mit einem Übertragungsfehler."
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: code:addons/queue_job/models/queue_job_function.py:0
#, python-format
msgid "Invalid job function: {}"
msgstr ""
@@ -337,7 +446,6 @@ msgstr "Job-Warteschlangenverwalter"
#. module: queue_job
#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
#, fuzzy
-#| msgid "Job failed"
msgid "Job Serialized"
msgstr "Job ist fehlgeschlagen"
@@ -354,10 +462,13 @@ msgstr "Job unterbrochen und als Erledigt markiert: Es ist nicht zu tun."
#. 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 "Jobs"
@@ -369,15 +480,38 @@ msgstr "Jobs"
msgid "Jobs Garbage Collector"
msgstr ""
+#. module: queue_job
+#: 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 "Kwargs"
#. 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_ir_model_fields____last_update
#: 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_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"
@@ -385,6 +519,7 @@ msgstr "Zuletzt geändert am"
#. 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_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"
@@ -392,6 +527,7 @@ msgstr "Zuletzt aktualisiert von"
#. 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_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"
@@ -426,7 +562,6 @@ msgstr "Nachrichten"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
#, fuzzy
-#| msgid "Method Name"
msgid "Method"
msgstr "Methodenname"
@@ -438,15 +573,21 @@ msgstr "Methodenname"
#. 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 "Modell"
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: 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
@@ -505,18 +646,13 @@ msgstr "Das ist die Anzahl von Nachrichten mit Übermittlungsfehler"
msgid "Number of unread messages"
msgstr "Das ist die Anzahl von ungelesenen Nachrichten"
-#. module: queue_job
-#: model:ir.model.fields,field_description:queue_job.field_queue_job__override_channel
-msgid "Override Channel"
-msgstr "Kanal überschreiben"
-
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
msgid "Parent Channel"
msgstr "Übergeordneter Kanal"
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: code:addons/queue_job/models/queue_job_channel.py:0
#, python-format
msgid "Parent channel required."
msgstr "Es ist ein übergeordneter Kanal notwendig."
@@ -525,8 +661,11 @@ msgstr "Es ist ein übergeordneter Kanal notwendig."
#: 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.\n"
+"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 ""
@@ -538,6 +677,7 @@ msgstr "Ausstehend"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Priority"
msgstr "Priorität"
@@ -554,7 +694,7 @@ msgstr "Job einreihen"
#. module: queue_job
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
-msgid "Queue jobs must created by calling 'with_delay()'."
+msgid "Queue jobs must be created by calling 'with_delay()'."
msgstr ""
#. module: queue_job
@@ -565,7 +705,6 @@ msgstr "Datensatz"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
#, fuzzy
-#| msgid "Record"
msgid "Record(s)"
msgstr "Datensatz"
@@ -577,7 +716,6 @@ msgstr "Zugehörige Aktion anzeigen"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
#, fuzzy
-#| msgid "Related Record"
msgid "Related Action"
msgstr "Zugehöriger Datensatz"
@@ -598,6 +736,11 @@ msgstr "Zugehöriger Datensatz"
msgid "Related Records"
msgstr "Zugehörige Datensätze"
+#. 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"
@@ -630,6 +773,11 @@ msgstr "Verantwortlicher Benutzer"
msgid "Result"
msgstr "Ergebnis"
+#. 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"
@@ -640,11 +788,6 @@ msgstr ""
msgid "Retry Pattern (serialized)"
msgstr ""
-#. module: queue_job
-#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_sms_error
-msgid "SMS Delivery error"
-msgstr "Fehler bei der SMS Nachrichtenübermittlung"
-
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_jobs_to_done
msgid "Set all selected jobs to done"
@@ -670,6 +813,11 @@ msgstr "Als Erledigt markieren"
msgid "Set to done"
msgstr "Als Erledigt markieren"
+#. 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
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
@@ -735,6 +883,11 @@ msgstr ""
"Wenn Letzteres nicht gesetzt ist, werden unendlich viele Versuche "
"unternommen."
+#. 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."
@@ -745,6 +898,21 @@ msgstr "Die ausgewählten Jobs werden erneut eingereiht."
msgid "The selected jobs will be set to done."
msgstr "Die ausgewählten Jobs werden als Erledigt markiert."
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
+msgid "Time required to execute this job in seconds. Average when grouped."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr ""
+
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
msgid "Type of the exception activity on record."
@@ -756,22 +924,23 @@ msgid "UUID"
msgstr "UUID"
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: 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}}}}"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
msgstr ""
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: code:addons/queue_job/models/queue_job_function.py:0
#, python-format
msgid ""
"Unexpected format of Retry Pattern for {}.\n"
-"Example of valid format:\n"
-"{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+"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
@@ -789,6 +958,12 @@ msgstr "Zähler für ungelesene Nachrichten"
msgid "User ID"
msgstr "Benutzer"
+#. 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.fields,field_description:queue_job.field_queue_job__website_message_ids
msgid "Website Messages"
@@ -809,6 +984,15 @@ msgstr "Assistent zur erneuten Einreihung einer Job-Auswahl"
msgid "Worker Pid"
msgstr ""
+#~ msgid "SMS Delivery error"
+#~ msgstr "Fehler bei der SMS Nachrichtenübermittlung"
+
+#~ msgid "Channel Method Name"
+#~ msgstr "Kanal-Methodenname"
+
+#~ msgid "Override Channel"
+#~ msgstr "Kanal überschreiben"
+
#~ msgid "If checked new messages require your attention."
#~ msgstr ""
#~ "Wenn es gesetzt ist, erfordern neue Nachrichten Ihre Aufmerksamkeit."
diff --git a/queue_job/i18n/es.po b/queue_job/i18n/es.po
new file mode 100644
index 0000000000..bbe3a4b314
--- /dev/null
+++ b/queue_job/i18n/es.po
@@ -0,0 +1,965 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 14.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: es\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"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid ""
+" If the max. retries is 0, the number of "
+"retries is infinite. "
+msgstr ""
+
+#. module: queue_job
+#: 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
+#: model:ir.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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "Cancelled by %s"
+msgstr ""
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job_channel.py:0
+#, python-format
+msgid "Cannot change the root channel"
+msgstr ""
+
+#. module: queue_job
+#: 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_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_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_ir_model_fields__display_name
+#: 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_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_channel_ids
+msgid "Followers (Channels)"
+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_ir_model_fields__id
+#: 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_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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "If both parameters are 0, ALL jobs will be requeued!"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_unread
+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
+#: 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
+#: 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
+#: model:ir.actions.server,name:queue_job.ir_cron_queue_job_garbage_collector_ir_actions_server
+#: model:ir.cron,cron_name:queue_job.ir_cron_queue_job_garbage_collector
+#: model:ir.cron,name:queue_job.ir_cron_queue_job_garbage_collector
+msgid "Jobs Garbage Collector"
+msgstr ""
+
+#. module: queue_job
+#: 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_ir_model_fields____last_update
+#: 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_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_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_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
+#: 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
+#: 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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "No action available for this job"
+msgstr ""
+
+#. module: queue_job
+#: 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 which requires an 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,help:queue_job.field_queue_job__message_unread_counter
+msgid "Number of unread messages"
+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
+#: code:addons/queue_job/models/queue_job_channel.py:0
+#, python-format
+msgid "Parent channel required."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
+msgid ""
+"Pattern expressing from the count of retries on retryable errors, the number "
+"of of seconds to postpone the next execution. Setting the number of seconds "
+"to a 2-element tuple or list will randomize the retry interval between the 2 "
+"values.\n"
+"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"See the module description for details."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Pending"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Priority"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+msgid "Queue Job"
+msgstr ""
+
+#. module: queue_job
+#: 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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "Related Record"
+msgstr ""
+
+#. module: queue_job
+#: 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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid ""
+"Something bad happened during the execution of the job. More details in the "
+"'Exception Information' section."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
+msgid "Start Date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Started"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "State"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
+msgid ""
+"Status based on activities\n"
+"Overdue: Due date is already passed\n"
+"Today: Activity date is today\n"
+"Planned: Future activities."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
+msgid "Task"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action
+msgid ""
+"The action when the button *Related Action* is used on a job. The default "
+"action is to open the view of the record related to the job. Configured as a "
+"dictionary with optional keys: enable, func_name, kwargs.\n"
+"See the module description for details."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
+msgid ""
+"The job will fail if the number of tries reach the max. retries.\n"
+"Retries are infinite when empty."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "The selected jobs will be cancelled."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "The selected jobs will be requeued."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "The selected jobs will be set to done."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
+msgid "Time required to execute this job in seconds. Average when grouped."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
+msgid "UUID"
+msgstr ""
+
+#. module: queue_job
+#: 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
+#: 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__message_unread
+msgid "Unread Messages"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_unread_counter
+msgid "Unread Messages Counter"
+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.fields,field_description:queue_job.field_queue_job__website_message_ids
+msgid "Website Messages"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__website_message_ids
+msgid "Website communication history"
+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 ""
diff --git a/queue_job/i18n/it.po b/queue_job/i18n/it.po
new file mode 100644
index 0000000000..32fc75b128
--- /dev/null
+++ b/queue_job/i18n/it.po
@@ -0,0 +1,969 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 14.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2025-08-04 14:25+0000\n"
+"Last-Translator: mymage \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.10.4\n"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid ""
+" If the max. retries is 0, the number of "
+"retries is infinite. "
+msgstr ""
+" Se il massimo dei tentativi è 0, il "
+"numero di tentativi è infinito. "
+
+#. module: queue_job
+#: 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 ""
+
+#. 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 ""
+
+#. 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
+#: model:ir.cron,name:queue_job.ir_cron_autovacuum_queue_jobs
+msgid "AutoVacuum Job Queue"
+msgstr "Pulizia automatica Job Queue"
+
+#. 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 job selezionati"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Cancel job"
+msgstr "Annulla job"
+
+#. 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 jobs"
+
+#. 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 "Annullato"
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "Cancelled by %s"
+msgstr "Annullato da %s"
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job_channel.py:0
+#, python-format
+msgid "Cannot change the root channel"
+msgstr "Impossibile modificare il canale root"
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job_channel.py:0
+#, python-format
+msgid "Cannot remove the root channel"
+msgstr "Impossibile rimuovere il canale 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 "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 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 ""
+
+#. 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_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 ""
+
+#. 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_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 corrente"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr "Tentativo corrente / max. tentativi"
+
+#. 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 "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 ""
+
+#. 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_ir_model_fields__display_name
+#: 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_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 "Completato"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr "Tempo di 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 "Info Eccezione"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception Information"
+msgstr "Info 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 "Esegui solo dopo"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
+msgid "Execution Time (avg)"
+msgstr "Tempo di esecuzione (media)"
+
+#. 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_channel_ids
+msgid "Followers (Channels)"
+msgstr "Seguito da (canali)"
+
+#. 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 ""
+
+#. 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 ""
+
+#. 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 "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_ir_model_fields__id
+#: 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_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à di eccezione."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr "Chiave d'identità"
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "If both parameters are 0, ALL jobs will be requeued!"
+msgstr "Se entrambi i parametri sono 0, TUTTI i jobs verranno rimessi in coda!"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_unread
+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
+#: code:addons/queue_job/models/queue_job_function.py:0
+#, python-format
+msgid "Invalid job function: {}"
+msgstr "Funzione Job 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 Job"
+
+#. 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 ""
+
+#. 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
+#: 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
+#: model:ir.actions.server,name:queue_job.ir_cron_queue_job_garbage_collector_ir_actions_server
+#: model:ir.cron,cron_name:queue_job.ir_cron_queue_job_garbage_collector
+#: model:ir.cron,name:queue_job.ir_cron_queue_job_garbage_collector
+msgid "Jobs Garbage Collector"
+msgstr ""
+
+#. module: queue_job
+#: 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_ir_model_fields____last_update
+#: 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_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_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_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 ""
+
+#. module: queue_job
+#: 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 "Modello"
+
+#. module: queue_job
+#: 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 ""
+
+#. 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 ""
+
+#. 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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "No action available for this job"
+msgstr ""
+
+#. module: queue_job
+#: 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 which requires an 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,help:queue_job.field_queue_job__message_unread_counter
+msgid "Number of unread messages"
+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
+#: 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 "In attesa"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Priority"
+msgstr "Priorità"
+
+#. 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
+msgid "Queue Job"
+msgstr ""
+
+#. module: queue_job
+#: 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 "Correlato"
+
+#. 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 ""
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "Related Record"
+msgstr "Record collegato"
+
+#. module: queue_job
+#: 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 per l'esecuzione"
+
+#. 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 "Rimetti in coda"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr "Rimetti in coda il Job"
+
+#. 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 "Rimetti in coda i Jobs"
+
+#. 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 ""
+
+#. 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 "Imposta tutti i jobs selezionati a completato"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr "Imposta i jobs 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 a '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 ""
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid ""
+"Something bad happened during the execution of the job. 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 "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 ""
+
+#. 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 "Durata (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 ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
+msgid "UUID"
+msgstr "UUID"
+
+#. module: queue_job
+#: 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
+#: 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__message_unread
+msgid "Unread Messages"
+msgstr "Messaggi non letti"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_unread_counter
+msgid "Unread Messages Counter"
+msgstr "Contatore messaggi non letti"
+
+#. 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.fields,field_description:queue_job.field_queue_job__website_message_ids
+msgid "Website Messages"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__website_message_ids
+msgid "Website communication history"
+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 ""
diff --git a/queue_job/i18n/queue_job.pot b/queue_job/i18n/queue_job.pot
index f18f9d9456..b85a205ddd 100644
--- a/queue_job/i18n/queue_job.pot
+++ b/queue_job/i18n/queue_job.pot
@@ -75,10 +75,39 @@ 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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "Cancelled by %s"
+msgstr ""
+
#. module: queue_job
#: code:addons/queue_job/models/queue_job_channel.py:0
#, python-format
@@ -99,11 +128,6 @@ msgstr ""
msgid "Channel"
msgstr ""
-#. module: queue_job
-#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
-msgid "Channel Method Name"
-msgstr ""
-
#. module: queue_job
#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
msgid "Channel complete name must be unique"
@@ -122,6 +146,11 @@ msgstr ""
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
@@ -135,13 +164,20 @@ 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_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_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"
@@ -157,11 +193,27 @@ msgstr ""
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"
@@ -172,6 +224,7 @@ msgstr ""
#: 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_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"
@@ -194,6 +247,12 @@ msgstr ""
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"
@@ -204,11 +263,31 @@ msgstr ""
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
@@ -245,6 +324,26 @@ msgstr ""
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
@@ -256,6 +355,7 @@ msgstr ""
#: 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_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"
@@ -290,7 +390,6 @@ msgstr ""
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
-#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_sms_error
msgid "If checked, some messages have a delivery error."
msgstr ""
@@ -355,10 +454,13 @@ 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 ""
@@ -370,16 +472,38 @@ msgstr ""
msgid "Jobs Garbage Collector"
msgstr ""
+#. module: queue_job
+#: 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_ir_model_fields____last_update
#: 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_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"
@@ -387,6 +511,7 @@ 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_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"
@@ -394,6 +519,7 @@ 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_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"
@@ -438,6 +564,7 @@ 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 ""
@@ -447,6 +574,11 @@ msgstr ""
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
@@ -505,11 +637,6 @@ msgstr ""
msgid "Number of unread messages"
msgstr ""
-#. module: queue_job
-#: model:ir.model.fields,field_description:queue_job.field_queue_job__override_channel
-msgid "Override Channel"
-msgstr ""
-
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
msgid "Parent Channel"
@@ -524,8 +651,9 @@ 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.\n"
+"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 ""
@@ -537,6 +665,7 @@ msgstr ""
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Priority"
msgstr ""
@@ -553,7 +682,7 @@ msgstr ""
#. module: queue_job
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
-msgid "Queue jobs must created by calling 'with_delay()'."
+msgid "Queue jobs must be created by calling 'with_delay()'."
msgstr ""
#. module: queue_job
@@ -593,6 +722,11 @@ msgstr ""
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"
@@ -625,6 +759,11 @@ msgstr ""
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"
@@ -635,11 +774,6 @@ msgstr ""
msgid "Retry Pattern (serialized)"
msgstr ""
-#. module: queue_job
-#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_sms_error
-msgid "SMS Delivery error"
-msgstr ""
-
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_jobs_to_done
msgid "Set all selected jobs to done"
@@ -665,6 +799,11 @@ msgstr ""
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
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
@@ -718,6 +857,11 @@ msgid ""
"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."
@@ -728,6 +872,21 @@ msgstr ""
msgid "The selected jobs will be set to done."
msgstr ""
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
+msgid "Time required to execute this job in seconds. Average when grouped."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr ""
+
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
msgid "Type of the exception activity on record."
@@ -752,8 +911,9 @@ msgstr ""
#, python-format
msgid ""
"Unexpected format of Retry Pattern for {}.\n"
-"Example of valid format:\n"
-"{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+"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
@@ -771,6 +931,22 @@ msgstr ""
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.fields,field_description:queue_job.field_queue_job__website_message_ids
+msgid "Website Messages"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__website_message_ids
+msgid "Website communication history"
+msgstr ""
+
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_requeue_job
msgid "Wizard to requeue a selection of jobs"
diff --git a/queue_job/i18n/ro.po b/queue_job/i18n/ro.po
new file mode 100644
index 0000000000..b96a9cd281
--- /dev/null
+++ b/queue_job/i18n/ro.po
@@ -0,0 +1,968 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 14.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2022-11-23 01:46+0000\n"
+"Last-Translator: Dorin Hongu \n"
+"Language-Team: none\n"
+"Language: ro\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < "
+"20)) ? 1 : 2;\n"
+"X-Generator: Weblate 4.14.1\n"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid ""
+" If the max. retries is 0, the number of "
+"retries is infinite. "
+msgstr ""
+
+#. module: queue_job
+#: 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
+#: model:ir.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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "Cancelled by %s"
+msgstr ""
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job_channel.py:0
+#, python-format
+msgid "Cannot change the root channel"
+msgstr ""
+
+#. module: queue_job
+#: 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 "Companie"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
+msgid "Complete Method Name"
+msgstr "Nume complet"
+
+#. 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_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_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 "Descriere"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__display_name
+#: 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_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 "Efectuat"
+
+#. 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 "Excepție"
+
+#. 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_channel_ids
+msgid "Followers (Channels)"
+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_ir_model_fields__id
+#: 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_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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "If both parameters are 0, ALL jobs will be requeued!"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_unread
+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
+#: 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 "Coadă sarcini"
+
+#. 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
+#: 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
+#: model:ir.actions.server,name:queue_job.ir_cron_queue_job_garbage_collector_ir_actions_server
+#: model:ir.cron,cron_name:queue_job.ir_cron_queue_job_garbage_collector
+#: model:ir.cron,name:queue_job.ir_cron_queue_job_garbage_collector
+msgid "Jobs Garbage Collector"
+msgstr ""
+
+#. module: queue_job
+#: 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_ir_model_fields____last_update
+#: 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_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_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_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
+#: 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
+#: 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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "No action available for this job"
+msgstr ""
+
+#. module: queue_job
+#: 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 which requires an 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,help:queue_job.field_queue_job__message_unread_counter
+msgid "Number of unread messages"
+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
+#: code:addons/queue_job/models/queue_job_channel.py:0
+#, python-format
+msgid "Parent channel required."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
+msgid ""
+"Pattern expressing from the count of retries on retryable errors, the number "
+"of of seconds to postpone the next execution. Setting the number of seconds "
+"to a 2-element tuple or list will randomize the retry interval between the 2 "
+"values.\n"
+"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"See the module description for details."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Pending"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Priority"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+msgid "Queue Job"
+msgstr ""
+
+#. module: queue_job
+#: 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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid "Related Record"
+msgstr ""
+
+#. module: queue_job
+#: 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
+#: code:addons/queue_job/models/queue_job.py:0
+#, python-format
+msgid ""
+"Something bad happened during the execution of the job. More details in the "
+"'Exception Information' section."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
+msgid "Start Date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Started"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "State"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
+msgid ""
+"Status based on activities\n"
+"Overdue: Due date is already passed\n"
+"Today: Activity date is today\n"
+"Planned: Future activities."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
+msgid "Task"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action
+msgid ""
+"The action when the button *Related Action* is used on a job. The default "
+"action is to open the view of the record related to the job. Configured as a "
+"dictionary with optional keys: enable, func_name, kwargs.\n"
+"See the module description for details."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
+msgid ""
+"The job will fail if the number of tries reach the max. retries.\n"
+"Retries are infinite when empty."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "The selected jobs will be cancelled."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "The selected jobs will be requeued."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "The selected jobs will be set to done."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
+msgid "Time required to execute this job in seconds. Average when grouped."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
+msgid "UUID"
+msgstr ""
+
+#. module: queue_job
+#: 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
+#: 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__message_unread
+msgid "Unread Messages"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_unread_counter
+msgid "Unread Messages Counter"
+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.fields,field_description:queue_job.field_queue_job__website_message_ids
+msgid "Website Messages"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__website_message_ids
+msgid "Website communication history"
+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 ""
diff --git a/queue_job/i18n/zh_CN.po b/queue_job/i18n/zh_CN.po
index 2117912c34..f4f8007f42 100644
--- a/queue_job/i18n/zh_CN.po
+++ b/queue_job/i18n/zh_CN.po
@@ -51,6 +51,11 @@ msgstr "活动异常装饰"
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"
@@ -75,18 +80,47 @@ 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
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
+msgid "Cancelled by %s"
+msgstr ""
+
+#. module: queue_job
+#: code:addons/queue_job/models/queue_job_channel.py:0
+#, python-format
msgid "Cannot change the root channel"
msgstr "无法更改root频道"
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: code:addons/queue_job/models/queue_job_channel.py:0
#, python-format
msgid "Cannot remove the root channel"
msgstr "无法删除root频道"
@@ -99,11 +133,6 @@ msgstr "无法删除root频道"
msgid "Channel"
msgstr "频道"
-#. module: queue_job
-#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
-msgid "Channel Method Name"
-msgstr "频道方法名称"
-
#. module: queue_job
#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
msgid "Channel complete name must be unique"
@@ -122,6 +151,11 @@ msgstr "频道"
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
@@ -135,13 +169,20 @@ 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_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_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"
@@ -157,20 +198,38 @@ msgstr "当前尝试"
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_ir_model_fields__display_name
#: 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_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"
@@ -193,6 +252,12 @@ msgstr "排队时间"
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"
@@ -203,11 +268,31 @@ msgstr "异常信息"
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
@@ -239,6 +324,31 @@ msgstr "关注者(频道)"
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
@@ -246,9 +356,11 @@ msgid "Group By"
msgstr "分组"
#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__id
#: 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_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"
@@ -272,7 +384,6 @@ msgstr "身份密钥"
#. module: queue_job
#: code:addons/queue_job/models/queue_job.py:0
#, fuzzy, python-format
-#| msgid "The selected jobs will be requeued."
msgid "If both parameters are 0, ALL jobs will be requeued!"
msgstr "所选作业将重新排队。"
@@ -284,12 +395,11 @@ msgstr "确认后, 出现提示消息。"
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
-#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_sms_error
msgid "If checked, some messages have a delivery error."
msgstr "如果勾选此项, 某些消息将会产生传递错误。"
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: code:addons/queue_job/models/queue_job_function.py:0
#, python-format
msgid "Invalid job function: {}"
msgstr ""
@@ -334,7 +444,6 @@ msgstr "作业队列管理员"
#. module: queue_job
#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
#, fuzzy
-#| msgid "Job failed"
msgid "Job Serialized"
msgstr "作业失败"
@@ -351,10 +460,13 @@ 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 "作业"
@@ -366,15 +478,38 @@ msgstr "作业"
msgid "Jobs Garbage Collector"
msgstr ""
+#. module: queue_job
+#: 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_ir_model_fields____last_update
#: 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_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"
@@ -382,6 +517,7 @@ 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_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"
@@ -389,6 +525,7 @@ 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_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"
@@ -423,7 +560,6 @@ msgstr "消息"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
#, fuzzy
-#| msgid "Method Name"
msgid "Method"
msgstr "方法名称"
@@ -435,15 +571,21 @@ 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
-#: code:addons/queue_job/models/queue_job.py:0
+#: 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
@@ -502,18 +644,13 @@ msgstr "递送错误消息数量"
msgid "Number of unread messages"
msgstr "未读消息数量"
-#. module: queue_job
-#: model:ir.model.fields,field_description:queue_job.field_queue_job__override_channel
-msgid "Override Channel"
-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
-#: code:addons/queue_job/models/queue_job.py:0
+#: code:addons/queue_job/models/queue_job_channel.py:0
#, python-format
msgid "Parent channel required."
msgstr "父频道必填。"
@@ -522,8 +659,11 @@ msgstr "父频道必填。"
#: 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.\n"
+"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 ""
@@ -535,6 +675,7 @@ msgstr "等待"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
msgid "Priority"
msgstr "优先级"
@@ -551,7 +692,7 @@ msgstr "队列作业"
#. module: queue_job
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
-msgid "Queue jobs must created by calling 'with_delay()'."
+msgid "Queue jobs must be created by calling 'with_delay()'."
msgstr ""
#. module: queue_job
@@ -562,7 +703,6 @@ msgstr "记录"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
#, fuzzy
-#| msgid "Record"
msgid "Record(s)"
msgstr "记录"
@@ -574,7 +714,6 @@ msgstr "相关的"
#. module: queue_job
#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
#, fuzzy
-#| msgid "Related Record"
msgid "Related Action"
msgstr "相关记录"
@@ -595,6 +734,11 @@ msgstr "相关记录"
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"
@@ -627,6 +771,11 @@ msgstr "负责的用户"
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"
@@ -637,11 +786,6 @@ msgstr ""
msgid "Retry Pattern (serialized)"
msgstr ""
-#. module: queue_job
-#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_sms_error
-msgid "SMS Delivery error"
-msgstr "短信传递错误"
-
#. module: queue_job
#: model:ir.model,name:queue_job.model_queue_jobs_to_done
msgid "Set all selected jobs to done"
@@ -667,6 +811,11 @@ msgstr "设置为“完成”"
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
#: code:addons/queue_job/models/queue_job.py:0
#, python-format
@@ -729,6 +878,11 @@ 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."
@@ -739,6 +893,21 @@ msgstr "所选作业将重新排队。"
msgid "The selected jobs will be set to done."
msgstr "所选作业将设置为完成。"
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
+msgid "Time required to execute this job in seconds. Average when grouped."
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr ""
+
#. module: queue_job
#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
msgid "Type of the exception activity on record."
@@ -750,22 +919,23 @@ msgid "UUID"
msgstr "UUID"
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: 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}}}}"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
msgstr ""
#. module: queue_job
-#: code:addons/queue_job/models/queue_job.py:0
+#: code:addons/queue_job/models/queue_job_function.py:0
#, python-format
msgid ""
"Unexpected format of Retry Pattern for {}.\n"
-"Example of valid format:\n"
-"{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+"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
@@ -783,6 +953,12 @@ msgstr "未读消息计数器"
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.fields,field_description:queue_job.field_queue_job__website_message_ids
msgid "Website Messages"
@@ -803,6 +979,15 @@ msgstr "重新排队向导所选的作业"
msgid "Worker Pid"
msgstr ""
+#~ msgid "SMS Delivery error"
+#~ msgstr "短信传递错误"
+
+#~ msgid "Channel Method Name"
+#~ msgstr "频道方法名称"
+
+#~ msgid "Override Channel"
+#~ msgstr "覆盖频道"
+
#~ msgid "If checked new messages require your attention."
#~ msgstr "查看是否有需要留意的新消息。"
diff --git a/queue_job/job.py b/queue_job/job.py
index 06eb6c0516..e486d1f001 100644
--- a/queue_job/job.py
+++ b/queue_job/job.py
@@ -7,23 +7,30 @@
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"),
]
@@ -34,69 +41,17 @@
_logger = logging.getLogger(__name__)
-class DelayableRecordset(object):
- """Allow to delay a method for a recordset
+# 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
- 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`
- """
-
- def __init__(
- self,
- recordset,
- priority=None,
- eta=None,
- max_retries=None,
- description=None,
- channel=None,
- identity_key=None,
- ):
- 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
-
- def __getattr__(self, name):
- if name in self.recordset:
- raise AttributeError(
- "only methods can be delayed ({} called on {})".format(
- name, self.recordset
- )
- )
- recordset_method = getattr(self.recordset, name)
-
- def delay(*args, **kwargs):
- return Job.enqueue(
- recordset_method,
- args=args,
- kwargs=kwargs,
- priority=self.priority,
- max_retries=self.max_retries,
- eta=self.eta,
- description=self.description,
- channel=self.channel,
- identity_key=self.identity_key,
- )
-
- return delay
-
- def __str__(self):
- return "DelayableRecordset({}{})".format(
- self.recordset._name, getattr(self.recordset, "_ids", "")
- )
-
- __repr__ = __str__
+ _logger.debug(
+ "DelayableRecordset moved from the queue_job.job"
+ " to the queue_job.delay python module"
+ )
+ return dr(*args, **kwargs)
def identity_exact(job_):
@@ -134,17 +89,23 @@ def identity_example(job_):
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
- return hasher.hexdigest()
-
-class Job(object):
+@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
@@ -154,6 +115,10 @@ class Job(object):
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.
@@ -213,6 +178,14 @@ class Job(object):
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.
@@ -245,14 +218,26 @@ class Job(object):
@classmethod
def load(cls, env, job_uuid):
- """Read a job from the Database"""
- stored = cls.db_record_from_uuid(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}
+
@classmethod
def _load_from_db_record(cls, job_db_record):
stored = job_db_record
@@ -292,7 +277,11 @@ def _load_from_db_record(cls, job_db_record):
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
@@ -301,6 +290,11 @@ def _load_from_db_record(cls, job_db_record):
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):
@@ -311,13 +305,14 @@ def job_record_with_same_identity_key(self):
.search(
[
("identity_key", "=", self.identity_key),
- ("state", "in", [PENDING, ENQUEUED]),
+ ("state", "in", [WAIT_DEPENDENCIES, PENDING, ENQUEUED]),
],
limit=1,
)
)
return existing
+ # TODO to deprecate (not called anymore)
@classmethod
def enqueue(
cls,
@@ -351,32 +346,42 @@ def enqueue(
channel=channel,
identity_key=identity_key,
)
- if new_job.identity_key:
- existing = new_job.job_record_with_same_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",
- new_job.identity_key,
+ self.identity_key,
existing.uuid,
)
return Job._load_from_db_record(existing)
- new_job.store()
+ self.store()
_logger.debug(
"enqueued %s:%s(*%r, **%r) with uuid: %s",
- new_job.recordset,
- new_job.method_name,
- new_job.args,
- new_job.kwargs,
- new_job.uuid,
+ self.recordset,
+ self.method_name,
+ self.args,
+ self.kwargs,
+ self.uuid,
)
- return new_job
+ 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", "=", job_uuid)], limit=1)
- return record.with_env(env)
+ record = model.search([("uuid", "in", tuple(job_uuids))])
+ return record.with_env(env).sudo()
def __init__(
self,
@@ -414,6 +419,7 @@ def __init__(
:param identity_key: A hash to uniquely identify a job, or a function
that returns this hash (the function takes the job
as argument)
+ :param graph_uuid: Shared UUID of the job's graph
:param env: Odoo Environment
:type env: :class:`odoo.api.Environment`
"""
@@ -440,13 +446,7 @@ def __init__(
self.job_model_name = "queue.job"
self.job_config = (
- self.env["queue.job.function"]
- .sudo()
- .job_config(
- self.env["queue.job.function"].job_function_name(
- self.model_name, self.method_name
- )
- )
+ self.env["queue.job.function"].sudo().job_config(self.job_function_name)
)
self.state = PENDING
@@ -458,10 +458,16 @@ def __init__(
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
@@ -481,8 +487,11 @@ def __init__(
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:
@@ -495,6 +504,17 @@ def __init__(
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.
@@ -519,24 +539,86 @@ def perform(self):
)
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_cache(["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_cache(["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:
@@ -545,49 +627,99 @@ def store(self):
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
- 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
+ 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
- db_record = self.db_record()
- if db_record:
- db_record.with_context(_job_edit_sentinel=edit_sentinel).write(vals)
- else:
- date_created = self.date_created
- # The following values must never be modified after the
- # creation of the job
+ 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,
- "date_created": date_created,
+ "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,
}
)
- # it the channel is not specified, lets the job_model compute
- # the right one to use
- if self.channel:
- vals.update({"channel": self.channel})
- job_model.with_context(_job_edit_sentinel=edit_sentinel).sudo().create(vals)
+ 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_record_from_uuid(self.env, self.uuid)
+ 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:
@@ -606,6 +738,20 @@ def identity_key(self, value):
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:
@@ -617,7 +763,7 @@ def description(self):
@property
def uuid(self):
- """Job ID, this is an UUID """
+ """Job ID, this is an UUID"""
if self._uuid is None:
self._uuid = str(uuid.uuid4())
return self._uuid
@@ -645,11 +791,30 @@ def eta(self, 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):
- self.state = PENDING
+ 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:
@@ -668,15 +833,23 @@ def set_started(self):
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_failed(self, exc_info=None):
+ 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
- if exc_info is not None:
- self.exc_info = exc_info
+ for k, v in kw.items():
+ if v is not None:
+ setattr(self, k, v)
def __repr__(self):
return "" % (self.uuid, self.priority)
@@ -694,6 +867,8 @@ def _get_retry_seconds(self, seconds=None):
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):
@@ -705,6 +880,7 @@ def postpone(self, result=None, seconds=None):
"""
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
diff --git a/queue_job/jobrunner/__init__.py b/queue_job/jobrunner/__init__.py
index 7ad479ff33..688edb7149 100644
--- a/queue_job/jobrunner/__init__.py
+++ b/queue_job/jobrunner/__init__.py
@@ -48,12 +48,13 @@ def stop(self):
class WorkerJobRunner(server.Worker):
- """ Jobrunner workers """
+ """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
@@ -64,10 +65,23 @@ def signal_handler(self, 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
diff --git a/queue_job/jobrunner/__main__.py b/queue_job/jobrunner/__main__.py
new file mode 100644
index 0000000000..8f9628c5db
--- /dev/null
+++ b/queue_job/jobrunner/__main__.py
@@ -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()
diff --git a/queue_job/jobrunner/channels.py b/queue_job/jobrunner/channels.py
index 1f42a10cc9..54188db11f 100644
--- a/queue_job/jobrunner/channels.py
+++ b/queue_job/jobrunner/channels.py
@@ -2,19 +2,21 @@
# Copyright 2015-2016 Camptocamp SA
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
import logging
+from collections import namedtuple
from functools import total_ordering
from heapq import heappop, heappush
from weakref import WeakValueDictionary
from ..exception import ChannelNotFound
-from ..job import DONE, ENQUEUED, FAILED, PENDING, STARTED
+from ..job import CANCELLED, DONE, ENQUEUED, FAILED, PENDING, STARTED, WAIT_DEPENDENCIES
-NOT_DONE = (PENDING, ENQUEUED, STARTED, FAILED)
+NOT_DONE = (WAIT_DEPENDENCIES, PENDING, ENQUEUED, STARTED, FAILED)
+JobSortingKey = namedtuple("SortingKey", "eta priority date_created seq")
_logger = logging.getLogger(__name__)
-class PriorityQueue(object):
+class PriorityQueue:
"""A priority queue that supports removing arbitrary objects.
Adding an object already in the queue is a no op.
@@ -75,8 +77,7 @@ def __contains__(self, o):
def add(self, o):
if o is None:
raise ValueError()
- if o in self._removed:
- self._removed.remove(o)
+ self._removed.discard(o)
if o in self._known:
return
self._known.add(o)
@@ -87,8 +88,7 @@ def remove(self, o):
raise ValueError()
if o not in self._known:
return
- if o not in self._removed:
- self._removed.add(o)
+ self._removed.add(o)
def pop(self):
while True:
@@ -104,31 +104,13 @@ def pop(self):
return o
-class SafeSet(set):
- """A set that does not raise KeyError when removing non-existent items.
-
- >>> s = SafeSet()
- >>> s.remove(1)
- >>> len(s)
- 0
- >>> s.remove(1)
- """
-
- def remove(self, o):
- # pylint: disable=missing-return,except-pass
- try:
- super().remove(o)
- except KeyError:
- pass
-
-
@total_ordering
-class ChannelJob(object):
+class ChannelJob:
"""A channel job is attached to a channel and holds the properties of a
job that are necessary to prioritise them.
Channel jobs are comparable according to the following rules:
- * jobs with an eta come before all other jobs
+ * jobs with an eta cannot be compared with jobs without
* then jobs with a smaller eta come first
* then jobs with a smaller priority come first
* then jobs with a smaller creation time come first
@@ -155,14 +137,18 @@ class ChannelJob(object):
>>> j3 < j1
True
- j4 and j5 comes even before j3, because they have an eta
+ j4 and j5 have an eta, they cannot be compared with j3
>>> j4 = ChannelJob(None, None, 4,
... seq=0, date_created=4, priority=9, eta=9)
>>> j5 = ChannelJob(None, None, 5,
... seq=0, date_created=5, priority=9, eta=9)
- >>> j4 < j5 < j3
+ >>> j4 < j5
True
+ >>> j4 < j3
+ Traceback (most recent call last):
+ ...
+ TypeError: '<' not supported between instances of 'int' and 'NoneType'
j6 has same date_created and priority as j5 but a smaller eta
@@ -173,7 +159,7 @@ class ChannelJob(object):
Here is the complete suite:
- >>> j6 < j4 < j5 < j3 < j1 < j2
+ >>> j6 < j4 < j5 and j3 < j1 < j2
True
j0 has the same properties as j1 but they are not considered
@@ -193,14 +179,13 @@ class ChannelJob(object):
"""
+ __slots__ = ("db_name", "channel", "uuid", "_sorting_key", "__weakref__")
+
def __init__(self, db_name, channel, uuid, seq, date_created, priority, eta):
self.db_name = db_name
self.channel = channel
self.uuid = uuid
- self.seq = seq
- self.date_created = date_created
- self.priority = priority
- self.eta = eta
+ self._sorting_key = JobSortingKey(eta, priority, date_created, seq)
def __repr__(self):
return "" % self.uuid
@@ -211,21 +196,39 @@ def __eq__(self, other):
def __hash__(self):
return id(self)
+ def set_no_eta(self):
+ self._sorting_key = JobSortingKey(None, *self._sorting_key[1:])
+
+ @property
+ def seq(self):
+ return self._sorting_key.seq
+
+ @property
+ def date_created(self):
+ return self._sorting_key.date_created
+
+ @property
+ def priority(self):
+ return self._sorting_key.priority
+
+ @property
+ def eta(self):
+ return self._sorting_key.eta
+
def sorting_key(self):
- return self.eta, self.priority, self.date_created, self.seq
+ # DEPRECATED
+ return self._sorting_key
def sorting_key_ignoring_eta(self):
- return self.priority, self.date_created, self.seq
+ return self._sorting_key[1:]
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()
+ # Do not compare job where ETA is set with job where it is not
+ # If one job 'eta' is set, and the other is None, it raises TypeError
+ return self._sorting_key < other._sorting_key
-class ChannelQueue(object):
+class ChannelQueue:
"""A channel queue is a priority queue for jobs.
Jobs with an eta are set aside until their eta is past due, at
@@ -332,7 +335,7 @@ def remove(self, job):
def pop(self, now):
while self._eta_queue and self._eta_queue[0].eta <= now:
eta_job = self._eta_queue.pop()
- eta_job.eta = None
+ eta_job.set_no_eta()
self._queue.add(eta_job)
if self.sequential and self._eta_queue and self._queue:
eta_job = self._eta_queue[0]
@@ -354,7 +357,7 @@ def get_wakeup_time(self, wakeup_time=0):
return wakeup_time
-class Channel(object):
+class Channel:
"""A channel for jobs, with a maximum capacity.
When jobs are created by queue_job modules, they may be associated
@@ -408,8 +411,8 @@ def __init__(self, name, parent, capacity=None, sequential=False, throttle=0):
self.parent.children[name] = self
self.children = {}
self._queue = ChannelQueue()
- self._running = SafeSet()
- self._failed = SafeSet()
+ self._running = set()
+ self._failed = set()
self._pause_until = 0 # utc seconds since the epoch
self.capacity = capacity
self.throttle = throttle # seconds
@@ -463,8 +466,8 @@ def __str__(self):
def remove(self, job):
"""Remove a job from the channel."""
self._queue.remove(job)
- self._running.remove(job)
- self._failed.remove(job)
+ self._running.discard(job)
+ self._failed.discard(job)
if self.parent:
self.parent.remove(job)
@@ -484,8 +487,8 @@ def set_pending(self, job):
"""
if job not in self._queue:
self._queue.add(job)
- self._running.remove(job)
- self._failed.remove(job)
+ self._running.discard(job)
+ self._failed.discard(job)
if self.parent:
self.parent.remove(job)
_logger.debug("job %s marked pending in channel %s", job.uuid, self)
@@ -498,16 +501,16 @@ def set_running(self, job):
if job not in self._running:
self._queue.remove(job)
self._running.add(job)
- self._failed.remove(job)
+ self._failed.discard(job)
if self.parent:
self.parent.set_running(job)
_logger.debug("job %s marked running in channel %s", job.uuid, self)
def set_failed(self, job):
- """Mark the job as failed. """
+ """Mark the job as failed."""
if job not in self._failed:
self._queue.remove(job)
- self._running.remove(job)
+ self._running.discard(job)
self._failed.add(job)
if self.parent:
self.parent.remove(job)
@@ -601,7 +604,7 @@ def split_strip(s, sep, maxsplit=-1):
return [x.strip() for x in s.split(sep, maxsplit)]
-class ChannelManager(object):
+class ChannelManager:
"""High level interface for channels
This class handles:
@@ -942,7 +945,9 @@ def get_channel_from_config(self, config):
_logger.info("Configured channel: %s", channel)
return channel
- def get_channel_by_name(self, channel_name, autocreate=False):
+ def get_channel_by_name(
+ self, channel_name, autocreate=False, parent_fallback=False
+ ):
"""Return a Channel object by its name.
If it does not exist and autocreate is True, it is created
@@ -980,6 +985,9 @@ def get_channel_by_name(self, channel_name, autocreate=False):
>>> c = cm.get_channel_by_name('sub')
>>> c.fullname
'root.sub'
+ >>> c = cm.get_channel_by_name('root.sub.not.configured', parent_fallback=True)
+ >>> c.fullname
+ 'root.sub.sub.not.configured'
"""
if not channel_name or channel_name == self._root_channel.name:
return self._root_channel
@@ -987,9 +995,26 @@ def get_channel_by_name(self, channel_name, autocreate=False):
channel_name = self._root_channel.name + "." + channel_name
if channel_name in self._channels_by_name:
return self._channels_by_name[channel_name]
- if not autocreate:
+ if not autocreate and not parent_fallback:
raise ChannelNotFound("Channel %s not found" % channel_name)
parent = self._root_channel
+ if parent_fallback:
+ # Look for first direct parent w/ config.
+ # Eg: `root.edi.foo.baz` will falback on `root.edi.foo`
+ # or `root.edi` or `root` in sequence
+ parent_name = channel_name
+ while True:
+ parent_name = parent_name.rsplit(".", 1)[:-1][0]
+ if parent_name == self._root_channel.name:
+ break
+ if parent_name in self._channels_by_name:
+ parent = self._channels_by_name[parent_name]
+ _logger.debug(
+ "%s has no specific configuration: using %s",
+ channel_name,
+ parent_name,
+ )
+ break
for subchannel_name in channel_name.split(".")[1:]:
subchannel = parent.get_subchannel_by_name(subchannel_name)
if not subchannel:
@@ -1001,13 +1026,7 @@ def get_channel_by_name(self, channel_name, autocreate=False):
def notify(
self, db_name, channel_name, uuid, seq, date_created, priority, eta, state
):
- try:
- channel = self.get_channel_by_name(channel_name)
- except ChannelNotFound:
- _logger.warning(
- "unknown channel %s, using root channel for job %s", channel_name, uuid
- )
- channel = self._root_channel
+ channel = self.get_channel_by_name(channel_name, parent_fallback=True)
job = self._jobs_by_uuid.get(uuid)
if job:
# db_name is invariant
@@ -1030,7 +1049,7 @@ def notify(
job = ChannelJob(db_name, channel, uuid, seq, date_created, priority, eta)
self._jobs_by_uuid[uuid] = job
# state transitions
- if not state or state == DONE:
+ if not state or state in (DONE, CANCELLED):
job.channel.set_done(job)
elif state == PENDING:
job.channel.set_pending(job)
@@ -1038,6 +1057,9 @@ def notify(
job.channel.set_running(job)
elif state == FAILED:
job.channel.set_failed(job)
+ elif state == WAIT_DEPENDENCIES:
+ # wait until all parent jobs are done
+ pass
else:
_logger.error("unexpected state %s for job %s", state, job)
diff --git a/queue_job/jobrunner/runner.py b/queue_job/jobrunner/runner.py
index 8dbe39d97c..d4dcabceb2 100644
--- a/queue_job/jobrunner/runner.py
+++ b/queue_job/jobrunner/runner.py
@@ -35,6 +35,10 @@
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:
@@ -50,6 +54,8 @@
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:
@@ -136,7 +142,7 @@
import datetime
import logging
import os
-import select
+import selectors
import threading
import time
from contextlib import closing, contextmanager
@@ -156,6 +162,8 @@
_logger = logging.getLogger(__name__)
+select = selectors.DefaultSelector
+
# Unfortunately, it is not possible to extend the Odoo
# server command line arguments, so we resort to environment variables
@@ -187,7 +195,7 @@ def _odoo_now():
def _connection_info_for(db_name):
db_or_uri, connection_info = odoo.sql_db.connection_info_for(db_name)
- for p in ("host", "port"):
+ 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)
@@ -251,7 +259,7 @@ def urlopen():
thread.start()
-class Database(object):
+class Database:
def __init__(self, db_name):
self.db_name = db_name
connection_info = _connection_info_for(db_name)
@@ -336,7 +344,7 @@ def set_job_enqueued(self, uuid):
)
-class QueueJobRunner(object):
+class QueueJobRunner:
def __init__(
self,
scheme="http",
@@ -359,6 +367,16 @@ def __init__(
self._stop = False
self._stop_pipe = os.pipe()
+ def __del__(self):
+ try:
+ os.close(self._stop_pipe[0])
+ except OSError:
+ pass
+ try:
+ os.close(self._stop_pipe[1])
+ except OSError:
+ pass
+
@classmethod
def from_environ_or_config(cls):
scheme = os.environ.get("ODOO_QUEUE_JOB_SCHEME") or queue_job_config.get(
@@ -393,7 +411,7 @@ def get_db_names(self):
if config["db_name"]:
db_names = config["db_name"].split(",")
else:
- db_names = odoo.service.db.exp_list(True)
+ db_names = odoo.service.db.list_dbs(True)
return db_names
def close_databases(self, remove_jobs=True):
@@ -479,10 +497,16 @@ def wait_notification(self):
# probably a bug
_logger.debug("select() timeout: %.2f sec", timeout)
if timeout > 0:
- conns, _, _ = select.select(conns, [], [], timeout)
if conns and not self._stop:
- for conn in conns:
- conn.poll()
+ 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")
diff --git a/queue_job/migrations/14.0.1.4.0/post-migration.py b/queue_job/migrations/14.0.1.4.0/post-migration.py
new file mode 100644
index 0000000000..f6eff72707
--- /dev/null
+++ b/queue_job/migrations/14.0.1.4.0/post-migration.py
@@ -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
diff --git a/queue_job/migrations/14.0.1.4.0/pre-migration.py b/queue_job/migrations/14.0.1.4.0/pre-migration.py
new file mode 100644
index 0000000000..8ae6cb3a5f
--- /dev/null
+++ b/queue_job/migrations/14.0.1.4.0/pre-migration.py
@@ -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;
+ """
+ )
diff --git a/queue_job/migrations/14.0.3.5.2/pre-migration.py b/queue_job/migrations/14.0.3.5.2/pre-migration.py
new file mode 100644
index 0000000000..53d9690caa
--- /dev/null
+++ b/queue_job/migrations/14.0.3.5.2/pre-migration.py
@@ -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;")
diff --git a/queue_job/models/base.py b/queue_job/models/base.py
index a05da489b3..4a6fa4753a 100644
--- a/queue_job/models/base.py
+++ b/queue_job/models/base.py
@@ -1,14 +1,13 @@
# Copyright 2016 Camptocamp
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
-import logging
-import os
+import functools
-from odoo import models
+from odoo import api, models
+from ..delay import Delayable
from ..job import DelayableRecordset
-
-_logger = logging.getLogger(__name__)
+from ..utils import must_run_without_delay
class Base(models.AbstractModel):
@@ -31,23 +30,88 @@ def with_delay(
):
"""Return a ``DelayableRecordset``
- The returned instance allows to enqueue any method of the recordset's
- Model.
-
- Usage::
+ It is a shortcut for the longer form as shown below::
- self.env['res.users'].with_delay().write({'name': 'test'})
+ 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::
- delayable = env['a.model'].with_delay(priority=30, eta=60*60*5)
+ 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
@@ -65,26 +129,11 @@ def with_delay(
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`).
- :return: instance of a DelayableRecordset
- :rtype: :class:`odoo.addons.queue_job.job.DelayableRecordset`
-
- Note for developers: if you want to run tests or simply disable
- jobs queueing for debugging purposes, you can:
-
- a. set the env var `TEST_QUEUE_JOB_NO_DELAY=1`
- b. pass a ctx key `test_queue_job_no_delay=1`
-
- In tests you'll have to mute the logger like:
-
- @mute_logger('odoo.addons.queue_job.models.base')
+ the new job will not be added.
+ :return: instance of a Delayable
+ :rtype: :class:`odoo.addons.queue_job.job.Delayable`
"""
- if os.getenv("TEST_QUEUE_JOB_NO_DELAY"):
- _logger.warn("`TEST_QUEUE_JOB_NO_DELAY` env var found. NO JOB scheduled.")
- return self
- if self.env.context.get("test_queue_job_no_delay"):
- _logger.warn("`test_queue_job_no_delay` ctx key found. NO JOB scheduled.")
- return self
- return DelayableRecordset(
+ return Delayable(
self,
priority=priority,
eta=eta,
@@ -93,3 +142,128 @@ def with_delay(
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.
+ """
+ # TODO: when migrating to 16.0, active the base context keys:
+ # return ("tz", "lang", "allowed_company_ids", "force_company", "active_test")
+ return ()
+
+ 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()
+ }
diff --git a/queue_job/models/queue_job.py b/queue_job/models/queue_job.py
index 9376fab07d..5c69655181 100644
--- a/queue_job/models/queue_job.py
+++ b/queue_job/models/queue_job.py
@@ -2,13 +2,28 @@
# 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.osv import expression
+from odoo.tools import config, html_escape, index_exists
+from odoo.addons.base_sparse_field.models.fields import Serialized
+
+from ..delay import Graph
+from ..exception import JobError
from ..fields import JobSerialized
-from ..job import DONE, PENDING, STATES, Job
+from ..job import (
+ CANCELLED,
+ DONE,
+ FAILED,
+ PENDING,
+ STARTED,
+ STATES,
+ WAIT_DEPENDENCIES,
+ Job,
+)
_logger = logging.getLogger(__name__)
@@ -37,27 +52,28 @@ class QueueJob(models.Model):
"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)
- user_id = fields.Many2one(
- comodel_name="res.users",
- string="User ID",
- compute="_compute_user_id",
- inverse="_inverse_user_id",
- store=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", compute="_compute_model_name", store=True, 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
@@ -67,14 +83,18 @@ class QueueJob(models.Model):
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", compute="_compute_func_string", readonly=True, store=True
- )
+ func_string = fields.Char(string="Task", readonly=True)
state = fields.Selection(STATES, readonly=True, required=True, index=True)
- priority = fields.Integer()
+ priority = fields.Integer(group_operator=False)
+ exc_name = fields.Char(string="Exception", readonly=True)
+ exc_message = fields.Char(string="Exception Message", readonly=True, tracking=True)
exc_info = fields.Text(string="Exception Info", readonly=True)
result = fields.Text(readonly=True)
@@ -82,6 +102,12 @@ class QueueJob(models.Model):
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")
@@ -91,88 +117,119 @@ class QueueJob(models.Model):
"max. retries.\n"
"Retries are infinite when empty.",
)
- channel_method_name = fields.Char(
- readonly=True, compute="_compute_job_function", store=True
- )
+ # 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",
- compute="_compute_job_function",
string="Job Function",
readonly=True,
- store=True,
)
- override_channel = fields.Char()
- channel = fields.Char(
- compute="_compute_channel", inverse="_inverse_channel", store=True, index=True
- )
+ channel = fields.Char(index=True)
- identity_key = fields.Char()
- worker_pid = fields.Integer()
+ 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():
+ index_1 = "queue_job_identity_key_state_partial_index"
+ index_2 = "queue_job_channel_date_done_date_created_index"
+ if not index_exists(self._cr, index_1):
+ # Used by Job.job_record_with_same_identity_key
self._cr.execute(
"CREATE INDEX queue_job_identity_key_state_partial_index "
"ON queue_job (identity_key) WHERE state in ('pending', "
- "'enqueued') AND identity_key IS NOT NULL;"
+ "'enqueued', 'wait_dependencies') AND identity_key IS NOT NULL;"
+ )
+ if not index_exists(self._cr, index_2):
+ # Used by .autovacuum
+ self._cr.execute(
+ "CREATE INDEX queue_job_channel_date_done_date_created_index "
+ "ON queue_job (channel, date_done, date_created);"
)
-
- @api.depends("records")
- def _compute_user_id(self):
- for record in self:
- record.user_id = record.records.env.uid
-
- def _inverse_user_id(self):
- for record in self.with_context(_job_edit_sentinel=self.EDIT_SENTINEL):
- record.records = record.records.with_user(record.user_id.id)
-
- @api.depends("records")
- def _compute_model_name(self):
- for record in self:
- record.model_name = record.records._name
@api.depends("records")
def _compute_record_ids(self):
for record in self:
record.record_ids = record.records.ids
- def _inverse_channel(self):
- for record in self:
- record.override_channel = record.channel
-
- @api.depends("job_function_id.channel_id")
- def _compute_channel(self):
- for record in self:
- channel = (
- record.override_channel or record.job_function_id.channel or "root"
- )
- if record.channel != channel:
- record.channel = channel
-
- @api.depends("model_name", "method_name", "job_function_id.channel_id")
- def _compute_job_function(self):
+ @api.depends("dependencies")
+ def _compute_dependency_graph(self):
+ graph_uuids = [uuid for uuid in self.mapped("graph_uuid") if uuid]
+ jobs_groups = self.env["queue.job"].read_group(
+ [("graph_uuid", "in", graph_uuids)],
+ ["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:
- func_model = self.env["queue.job.function"]
- channel_method_name = func_model.job_function_name(
- record.model_name, record.method_name
- )
- function = func_model.search([("name", "=", channel_method_name)], limit=1)
- record.channel_method_name = channel_method_name
- record.job_function_id = function
+ 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": "%s %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,
+ }
- @api.depends("model_name", "method_name", "records", "args", "kwargs")
- def _compute_func_string(self):
+ def _compute_graph_jobs_count(self):
+ graph_uuids = [uuid for uuid in self.mapped("graph_uuid") if uuid]
+ jobs_groups = self.env["queue.job"].read_group(
+ [("graph_uuid", "in", graph_uuids)], ["graph_uuid"], ["graph_uuid"]
+ )
+ count_per_graph_uuid = {
+ group["graph_uuid"]: group["graph_uuid_count"] for group in jobs_groups
+ }
for record in self:
- model = repr(record.records)
- args = [repr(arg) for arg in record.args]
- kwargs = ["{}={!r}".format(key, val) for key, val in record.kwargs.items()]
- all_args = ", ".join(args + kwargs)
- record.func_string = "{}.{}({})".format(model, record.method_name, all_args)
+ record.graph_jobs_count = count_per_graph_uuid.get(record.graph_uuid) or 0
@api.model_create_multi
def create(self, vals_list):
@@ -180,9 +237,12 @@ def create(self, vals_list):
# Prevent to create a queue.job record "raw" from RPC.
# ``with_delay()`` must be used.
raise exceptions.AccessError(
- _("Queue jobs must created by calling 'with_delay()'.")
+ _("Queue jobs must be created by calling 'with_delay()'.")
)
- return super().create(vals_list)
+ 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:
@@ -196,10 +256,25 @@ def write(self, vals):
)
)
+ 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()
- return super().write(vals)
+ 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"""
@@ -210,6 +285,23 @@ def open_related_action(self):
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
@@ -220,28 +312,43 @@ def _change_job_state(self, state, result=None):
job_ = Job.load(record.env, record.uuid)
if state == DONE:
job_.set_done(result=result)
+ job_.store()
+ record.env["queue.job"].flush()
+ 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()
+ job_.cancel_dependent_jobs()
else:
raise ValueError("State not supported: %s" % state)
- job_.store()
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):
- self._change_job_state(PENDING)
+ jobs_to_requeue = self.filtered(lambda job_: job_.state != WAIT_DEPENDENCIES)
+ jobs_to_requeue._change_job_state(PENDING)
return True
def _message_post_on_failure(self):
# subscribe the users now to avoid to subscribe them
# at every job creation
domain = self._subscribe_users_domain()
- users = self.env["res.users"].search(domain)
- self.message_subscribe(partner_ids=users.mapped("partner_id").ids)
+ 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")
@@ -286,11 +393,23 @@ def autovacuum(self):
"""
for channel in self.env["queue.job.channel"].search([]):
deadline = datetime.now() - timedelta(days=int(channel.removal_interval))
- jobs = self.search(
- [("date_done", "<=", deadline), ("channel", "=", channel.complete_name)]
- )
- if jobs:
- jobs.unlink()
+ while True:
+ jobs = self.search(
+ [
+ "|",
+ ("date_done", "<=", deadline),
+ ("date_cancelled", "<=", deadline),
+ ("channel", "=", channel.complete_name),
+ ],
+ order="date_done, date_created",
+ limit=1000,
+ )
+ if jobs:
+ jobs.unlink()
+ if not config["test_enable"]:
+ self.env.cr.commit() # pylint: disable=E8102
+ else:
+ break
return True
def requeue_stuck_jobs(self, enqueued_delta=5, started_delta=0):
@@ -375,5 +494,7 @@ def related_action_open_record(self):
)
return action
- def _test_job(self):
+ def _test_job(self, failure_rate=0):
_logger.info("Running test job.")
+ if random.random() <= failure_rate:
+ raise JobError("Job failed")
diff --git a/queue_job/models/queue_job_function.py b/queue_job/models/queue_job_function.py
index ef6c1b849d..d839708dcf 100644
--- a/queue_job/models/queue_job_function.py
+++ b/queue_job/models/queue_job_function.py
@@ -27,7 +27,8 @@ class QueueJobFunction(models.Model):
"retry_pattern "
"related_action_enable "
"related_action_func_name "
- "related_action_kwargs ",
+ "related_action_kwargs "
+ "job_function_id ",
)
def _default_channel(self):
@@ -60,8 +61,11 @@ def _default_channel(self):
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.\n"
+ " 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)
@@ -138,16 +142,19 @@ def job_default_config(self):
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 = {
- int(try_count): postpone_seconds
- for try_count, postpone_seconds in self.retry_pattern.items()
- }
+ 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,"
@@ -170,13 +177,15 @@ def job_config(self, name):
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 format:\n"
- "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+ "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")
@@ -189,12 +198,20 @@ def _check_retry_pattern(self):
all_values = list(retry_pattern) + list(retry_pattern.values())
for value in all_values:
try:
- int(value)
+ self._retry_value_type_check(value)
except ValueError:
raise exceptions.UserError(
record._retry_pattern_format_error_message()
)
+ 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"
diff --git a/queue_job/hooks/post_init_hook.py b/queue_job/post_init_hook.py
similarity index 100%
rename from queue_job/hooks/post_init_hook.py
rename to queue_job/post_init_hook.py
diff --git a/queue_job/readme/CONTRIBUTORS.rst b/queue_job/readme/CONTRIBUTORS.rst
index 0f8bb1a3b2..4b34823abe 100644
--- a/queue_job/readme/CONTRIBUTORS.rst
+++ b/queue_job/readme/CONTRIBUTORS.rst
@@ -9,3 +9,4 @@
* Tatiana Deribina
* Souheil Bejaoui
* Eric Antones
+* Simone Orsi
diff --git a/queue_job/readme/USAGE.rst b/queue_job/readme/USAGE.rst
index 6c472eccf9..b7614d2ab4 100644
--- a/queue_job/readme/USAGE.rst
+++ b/queue_job/readme/USAGE.rst
@@ -5,7 +5,156 @@ To use this module, you need to:
Developers
~~~~~~~~~~
-**Configure default options for jobs**
+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``
@@ -25,7 +174,7 @@ Example of job function:
.. code-block:: XML
-
+
action_done
@@ -43,6 +192,13 @@ 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
+````
+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``.
@@ -118,18 +274,42 @@ Based on this configuration, we can tell that:
* 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 empty for backward compatibility. The allow-list can
+be customized in ``Base._job_prepare_context_before_enqueue_keys``.
+
+Example:
+
+.. code-block:: python
+
+ class Base(models.AbstractModel):
+
+ _inherit = "base"
+
+ @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")
+
**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 `TEST_QUEUE_JOB_NO_DELAY=1` in your enviroment.
+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 `test_queue_job_no_delay=True` in the context.
+you can set `queue_job__no_delay=True` in the context.
Tip: you can do this at test case level like this
@@ -140,8 +320,153 @@ Tip: you can do this at test case level like this
super().setUpClass()
cls.env = cls.env(context=dict(
cls.env.context,
- test_queue_job_no_delay=True, # no jobs thanks
+ 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.
diff --git a/queue_job/security/ir.model.access.csv b/queue_job/security/ir.model.access.csv
index 9242305158..634daf8ede 100644
--- a/queue_job/security/ir.model.access.csv
+++ b/queue_job/security/ir.model.access.csv
@@ -4,3 +4,4 @@ access_queue_job_function_manager,queue job functions manager,queue_job.model_qu
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
diff --git a/queue_job/static/description/icon.png b/queue_job/static/description/icon.png
index c252a4c5d5..a518515a27 100644
Binary files a/queue_job/static/description/icon.png and b/queue_job/static/description/icon.png differ
diff --git a/queue_job/static/description/icon.svg b/queue_job/static/description/icon.svg
index fd5da3366b..b6861d681b 100644
--- a/queue_job/static/description/icon.svg
+++ b/queue_job/static/description/icon.svg
@@ -1,150 +1,220 @@
-
+
+
-
-
-
- image/svg+xml
-
-
-
-
-
-
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg8"
+ version="1.1"
+ viewBox="0 0 140 140"
+ height="140"
+ width="140"
+ inkscape:export-filename="./icon.png"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96"
+ sodipodi:docname="icon.svg"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)">
-
-
-
-
+ inkscape:window-maximized="1"
+ inkscape:window-y="28"
+ inkscape:window-x="0"
+ inkscape:window-height="1024"
+ inkscape:window-width="1920"
+ inkscape:snap-to-guides="true"
+ inkscape:snap-page="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-others="false"
+ inkscape:lockguides="false"
+ inkscape:guide-bbox="true"
+ showguides="true"
+ units="px"
+ showgrid="false"
+ inkscape:current-layer="layer2"
+ inkscape:document-units="px"
+ inkscape:cy="66.615172"
+ inkscape:cx="65.97056"
+ inkscape:zoom="4.5659091"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base">
+ inkscape:color="rgb(0,0,255)"
+ inkscape:label=""
+ inkscape:locked="false"
+ id="guide856"
+ orientation="1,0"
+ position="20,155.71926" />
+ inkscape:color="rgb(0,0,255)"
+ inkscape:label=""
+ inkscape:locked="false"
+ id="guide858"
+ orientation="1,0"
+ position="120,136.88402" />
+ inkscape:color="rgb(0,0,255)"
+ inkscape:label=""
+ inkscape:locked="false"
+ id="guide860"
+ orientation="0,1"
+ position="61.105027,115" />
-
-
+ inkscape:color="rgb(0,0,255)"
+ inkscape:label=""
+ inkscape:locked="false"
+ id="guide862"
+ orientation="0,1"
+ position="104.25087,25" />
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
-
+ inkscape:label="Icon"
+ id="layer2"
+ inkscape:groupmode="layer">
+
+
+
+
diff --git a/queue_job/static/description/index.html b/queue_job/static/description/index.html
index 973b5332c1..c53bb505a6 100644
--- a/queue_job/static/description/index.html
+++ b/queue_job/static/description/index.html
@@ -1,20 +1,20 @@
-
-
-Job Queue
+
+README.rst
-
-
Job Queue
+
+
+
+
+
+
+
Job Queue
-
+
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:
-from odoo import models , fields , api
+from odoo import models , fields , api
-class MyModel ( models . Model ):
- _name = 'my.model'
+ 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 )
+ 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'
+ class MyOtherModel ( models . Model ):
+ _name = 'my.other.model'
- def button_do_stuff ( self ):
- self . env [ 'my.model' ] . with_delay () . my_method ( 'a' , k = 2 )
+ 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
@@ -410,32 +417,40 @@
Job Queue
Table of contents
-
+
Be sure to have the requests library.
-
+
Using environment variables and command line:
Adjust environment variables (optional):
Start Odoo with --load=web,queue_job
-and --workers greater than 1.
+and --workers greater than 1.
Using the Odoo configuration file:
-[options]
-(...)
-workers = 6
-server_wide_modules = web,queue_job
-
-(...)
-[queue_job]
-channels = root:2
+[options]
+ (...)
+ workers = 6
+ server_wide_modules = web,queue_job
+
+ (...)
+ [queue_job]
+ channels = root:2
Confirm the runner is starting correctly by checking the odoo log file:
@@ -475,42 +490,172 @@
Tip: to enable debug logging for the queue job, use
--log-handler=odoo.addons.queue_job:DEBUG
-