"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_needaction
+msgid "Action Needed"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model,name:hr_docs_expiry.model_ir_attachment
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__doc_attachment_id
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_ir_attachment__doc_attach_rel
+msgid "Attachment"
+msgstr "مرفق"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_attachment_count
+msgid "Attachment Count"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_check_list_form_view
+msgid "Checklist"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__document_type
+msgid "Checklist Type"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__create_uid
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__create_date
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__description
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_form_view
+msgid "Description"
+msgstr "الوصف"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__display_name
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee__display_name
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__display_name
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_ir_attachment__display_name
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_res_users__display_name
+msgid "Display Name"
+msgstr "الاسم المعروض"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__document_name
+msgid "Document"
+msgstr "مستند"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__name
+msgid "Document Name"
+msgstr "اسم المستند"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__name
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_form_view
+msgid "Document Name"
+msgstr "اسم المستند"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__document_type
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_form_view
+msgid "Document Type"
+msgstr "نوع المستند"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "Documents"
+msgstr "المستندات"
+
+#. module: hr_docs_expiry
+#: model:ir.model,name:hr_docs_expiry.model_hr_employee
+msgid "Employee"
+msgstr "الموظف"
+
+#. module: hr_docs_expiry
+#: model:ir.actions.server,name:hr_docs_expiry.employee_docs_data_reminder_ir_actions_server
+#: model:ir.cron,cron_name:hr_docs_expiry.employee_docs_data_reminder
+#: model:ir.cron,name:hr_docs_expiry.employee_docs_data_reminder
+msgid "Employee Data Expiration"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model,name:hr_docs_expiry.model_employee_checklist
+#: model:ir.ui.menu,name:hr_docs_expiry.Employee_document_menu_item
+msgid "Employee Documents"
+msgstr "مستندات الموظف"
+
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__employee_ref
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_form_view
+msgid "Employee Name"
+msgstr "إسم الموظف"
+
+#. module: hr_docs_expiry
+#: model:ir.actions.act_window,name:hr_docs_expiry.employee_document_list_action
+msgid "Employee document"
+msgstr "مستندات الموظف"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields.selection,name:hr_docs_expiry.selection__employee_checklist__document_type__entry
+msgid "Entry Process"
+msgstr "مستند دخول"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields.selection,name:hr_docs_expiry.selection__employee_checklist__document_type__exit
+msgid "Exit Process"
+msgstr "مستند خروج"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee__expiry_license
+msgid "License Expiry Date"
+msgstr " تاريخ انتهاء الرخصة"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__file_examination
+msgid "File Examination"
+msgstr "ملف كشف طبي"
+
+#. module: hr_docs_expiry
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_form_view
+msgid "File Examination Name"
+msgstr "رقم الكشف/الإفصاح الطبي"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_follower_ids
+msgid "Followers"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_channel_ids
+msgid "Followers (Channels)"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_partner_ids
+msgid "Followers (Partners)"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.actions.server,name:hr_docs_expiry.employee_data_reminder_ir_actions_server
+#: model:ir.cron,cron_name:hr_docs_expiry.employee_data_reminder
+#: model:ir.cron,name:hr_docs_expiry.employee_data_reminder
+msgid "HR Employee Data Expiration"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model,name:hr_docs_expiry.model_hr_employee_document
+msgid "HR Employee Documents"
+msgstr "مستندات الموظف"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__id
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee__id
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__id
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_ir_attachment__id
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_res_users__id
+msgid "ID"
+msgstr "المُعرف"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,help:hr_docs_expiry.field_employee_checklist__message_needaction
+#: model:ir.model.fields,help:hr_docs_expiry.field_employee_checklist__message_unread
+msgid "If checked, new messages require your attention."
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,help:hr_docs_expiry.field_employee_checklist__message_has_error
+msgid "If checked, some messages have a delivery error."
+msgstr ""
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#: model:ir.model.fields.selection,name:hr_docs_expiry.selection__hr_employee_document__document_type__iqama
+#, python-format
+msgid "Identity"
+msgstr "الهوية"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#: model:ir.model.fields.selection,name:hr_docs_expiry.selection__hr_employee_document__document_type__professional_certificates
+#, python-format
+msgid "Professional Certificates"
+msgstr "شهادات مهنية"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__iqama_id
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_form_view
+msgid "Identity No"
+msgstr "رقم الهوية"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_is_follower
+msgid "Is Follower"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__issue_date
+msgid "Issue Date"
+msgstr "تاريخ الاصدار"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__job_id
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_form_view
+msgid "Job Position"
+msgstr "المهنة"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist____last_update
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee____last_update
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document____last_update
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_ir_attachment____last_update
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_res_users____last_update
+msgid "Last Modified on"
+msgstr "آخر تعديل في"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__write_uid
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__write_date
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#: model:ir.model.fields.selection,name:hr_docs_expiry.selection__hr_employee_document__document_type__license
+#, python-format
+msgid "License"
+msgstr "رخصة القيادة"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__license_id
+msgid "License ID"
+msgstr "رقم رخصة القيادة"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee__license_number_id
+msgid "License Number"
+msgstr "رقم رخصة القيادة"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_main_attachment_id
+msgid "Main Attachment"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_has_error
+msgid "Message Delivery error"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_ids
+msgid "Messages"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_needaction_counter
+msgid "Number of Actions"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_has_error_counter
+msgid "Number of errors"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,help:hr_docs_expiry.field_employee_checklist__message_needaction_counter
+msgid "Number of messages which requires an action"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,help:hr_docs_expiry.field_employee_checklist__message_has_error_counter
+msgid "Number of messages with delivery error"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,help:hr_docs_expiry.field_employee_checklist__message_unread_counter
+msgid "Number of unread messages"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#: model:ir.model.fields.selection,name:hr_docs_expiry.selection__employee_checklist__document_type__other
+#: model:ir.model.fields.selection,name:hr_docs_expiry.selection__hr_employee_document__document_type__other
+#, python-format
+msgid "Other"
+msgstr "اخرى"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee__passport_id
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_res_users__passport_id
+#: model:ir.model.fields.selection,name:hr_docs_expiry.selection__hr_employee_document__document_type__passport
+#, python-format
+msgid "Passport"
+msgstr "جواز السفر"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__passport_id
+msgid "Passport Number"
+msgstr "رقم الجواز"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__place_issue_id
+msgid "Place of Issue"
+msgstr "مكان استخراج المستند"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__reminder_before
+msgid "Reminder Before"
+msgstr "تذكير قبل (يوم)"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__saudi_id
+#: model:ir.model.fields.selection,name:hr_docs_expiry.selection__hr_employee_document__document_type__saudi
+#, python-format
+msgid "Saudi ID"
+msgstr "رقم الهوية الوطنية"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "This Identity Number already Exiting"
+msgstr "رقم الهوية هذا موجود بالفعل"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "This License Number already Exiting"
+msgstr "رقم رخصة القيادة هذا موجود بالفعل"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "This Medical Examination Number already Exiting"
+msgstr "رقم الفحص الطبي هذا موجود بالفعل"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "This Passport Number already Exiting"
+msgstr "رقم جواز السفر هذا موجود بالفعل"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "This Saudi Identity already Exiting"
+msgstr "رقم الهوية الوطنية هذا موجود بالفعل"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_unread
+msgid "Unread Messages"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_employee_checklist__message_unread_counter
+msgid "Unread Messages Counter"
+msgstr ""
+
+#. module: hr_docs_expiry
+#: model:ir.model,name:hr_docs_expiry.model_res_users
+msgid "Users"
+msgstr "المستخدمون"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,help:hr_docs_expiry.field_hr_employee_document__doc_attachment_id
+msgid "You can attach the copy of your document"
+msgstr "يجب ارفاق المستند للملف"
+
+#. module: hr_docs_expiry
+#: model:mail.template,subject:hr_docs_expiry.email_template_child_age_check
+msgid "Your Child ${object.name} has turned 18"
+msgstr "تاجوز عمر 18 سنة ${object.name} هذا الطفل"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__membership_type
+msgid "Membership Type"
+msgstr "نوع العضوية"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__specialization
+msgid "Specialization"
+msgstr "التخصص"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__category
+msgid "Category"
+msgstr "التصنيف"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "Saudi ID must be 10 digits"
+msgstr "رقم الهويه الوطنية يجب ان يكون 10 ارقام"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "The Saudi ID number should begin with 1"
+msgstr "رقم الهويه الوطنية يجب ان يبدأ ب 1"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "Identity must be 10 digits"
+msgstr "رقم الهويه يجب ان يكون 10 ارقام"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "Identity must begin with 2 or 3 or 4"
+msgstr "رقم الهويه يجب ان يبدأ ب 2 او 3 او 4"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "medical Examination"
+msgstr "الكشف/الإفصاح الطبي"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "Error, date of issue must be less than expiry date"
+msgstr "خطأ, تاريخ اصدار الهويه يجب ان تكون اقل من تاريخ النهاية"
+
+#. module: hr_docs_expiry
+#: code:addons/hr_docs_expiry/models/employee_documents.py:0
+#, python-format
+msgid "Error,the expiry date must be greater than the date of the day"
+msgstr "خطأ, تاريخ النهاية يجب ان يكون اكبر من تاريخ اليوم"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__company_id
+msgid "Company"
+msgstr "الشركة"
+
+#. module: hr_docs_expiry
+#: model:ir.model.fields,field_description:hr_docs_expiry.field_hr_employee_document__employee_number
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_form_view
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_inherit_view
+#: model_terms:ir.ui.view,arch_db:hr_docs_expiry.employee_document_tree_view
+msgid "Employee Number"
+msgstr "رقم الموظف"
+
+
+
diff --git a/odex30_base/hr_docs_expiry/models/__init__.py b/odex30_base/hr_docs_expiry/models/__init__.py
new file mode 100644
index 0000000..7997bd9
--- /dev/null
+++ b/odex30_base/hr_docs_expiry/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import employee_documents
+from . import employee_entry_exit_check_list
diff --git a/odex30_base/hr_docs_expiry/models/employee_documents.py b/odex30_base/hr_docs_expiry/models/employee_documents.py
new file mode 100644
index 0000000..9170853
--- /dev/null
+++ b/odex30_base/hr_docs_expiry/models/employee_documents.py
@@ -0,0 +1,268 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models
+from datetime import datetime, date, timedelta
+from odoo.tools.translate import _
+from odoo.exceptions import ValidationError
+import logging
+
+_logger = logging.getLogger(__name__)
+
+class HrEmployeeDocument(models.Model):
+ _name = "hr.employee.document"
+ _description = "HR Employee Documents"
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+
+ @api.model
+ def mail_reminder(self):
+ now = datetime.now() + timedelta(days=1)
+ date_now = now.date()
+ matches = self.search([])
+ for record in matches:
+ if record.expiry_date:
+ exp_date = fields.Date.from_string(record.expiry_date) - timedelta(days=record.reminder_before)
+ if date_now >= exp_date and record.employee_ref.state not in ['draft', 'out_of_service']:
+ template = self.env.ref('hr_docs_expiry.email_template_document_expiry_reminder', raise_if_not_found=False)
+ if template:
+ template.send_mail(record.id)
+
+ if record.employee_ref.state not in ['draft', 'out_of_service'] and record.employee_ref.employee_dependant:
+ for dependant in record.employee_ref.employee_dependant:
+ if dependant.relation == 'child' and dependant.age >= 18:
+ template2 = self.env.ref('hr_docs_expiry.email_template_child_age_check', raise_if_not_found=False)
+ if template2:
+ template2.send_mail(record.id)
+
+ @api.constrains('expiry_date', 'saudi_id', 'iqama_id', 'issue_date')
+ def check_expr_date(self):
+ for record in self:
+ if record.expiry_date:
+ exp_date = fields.Date.from_string(record.expiry_date)
+ if exp_date < date.today():
+ raise ValidationError('Your Document Is Expired.')
+
+ if record.saudi_id:
+ if len(record.saudi_id) != 10:
+ raise ValidationError(_('Saudi ID must be 10 digits'))
+ if record.saudi_id[0] != '1':
+ raise ValidationError(_('The Saudi ID number should begin with 1'))
+
+ if record.iqama_id:
+ if len(record.iqama_id) != 10:
+ raise ValidationError(_('Identity must be 10 digits'))
+ if record.iqama_id[0] not in ['2', '3', '4']:
+ raise ValidationError(_('Identity must begin with 2 or 3 or 4'))
+
+ if record.expiry_date and record.issue_date:
+ if record.expiry_date <= record.issue_date:
+ raise ValidationError(_('Error, date of issue must be less than expiry date'))
+ if date.today() >= record.expiry_date:
+ raise ValidationError(_("Error, the expiry date must be greater than today's date"))
+
+ saudi_id = fields.Char(string="Saudi ID")
+ license_id = fields.Char(string="License ID")
+ passport_id = fields.Char(string="Passport Number")
+ iqama_id = fields.Char(string="Identity No")
+ place_issue_id = fields.Char(string="Place of Issue")
+ name = fields.Char(string="Document Name", required=True, copy=False)
+ document_name = fields.Many2one(comodel_name="employee.checklist", string="Document")
+ description = fields.Text(string="Description", copy=False)
+ expiry_date = fields.Date(string="Expiry Date", tracking=True)
+ employee_ref = fields.Many2one('hr.employee', copy=False, string="Employee Name")
+ doc_attachment_id = fields.Many2many(
+ "ir.attachment",
+ "doc_attach_rel",
+ "doc_id",
+ "attach_id3",
+ string="Attachment",
+ help="You can attach the copy of your document",
+ copy=False
+ )
+ file_examination = fields.Char()
+ document_type = fields.Selection([
+ ("passport", _("Passport")),
+ ("license", _("License")),
+ ("Iqama", _("Identity")),
+ ("saudi", _("Saudi ID")),
+ ("medical_Examination", _("Medical Examination")),
+ ("professional_certificates", _("Professional Certificates")),
+ ("other", _("Other"))
+ ])
+ issue_date = fields.Date(
+ string="Issue Date",
+ default=fields.Date.context_today,
+ copy=False,
+ tracking=True
+ )
+ reminder_before = fields.Integer(default=0)
+ job_id = fields.Many2one("hr.job", "Job Position")
+ emp_iqama_job = fields.Char("Job Position")
+ membership_type = fields.Many2one(comodel_name="membership.types", string="Membership Type")
+ specialization = fields.Char(string="Specialization")
+ category = fields.Many2one(comodel_name="membership.categorys", string="Category")
+ company_id = fields.Many2one(
+ 'res.company',
+ string="Company",
+ default=lambda self: self.env.company
+ )
+ employee_number = fields.Char(
+ related="employee_ref.contract_id.name",
+ copy=False,
+ string='Employee Number'
+ )
+
+ def set_last_document(self):
+ self.ensure_one()
+ emp_id = self.env["hr.employee"].search([("id", "=", self.employee_ref.id)])
+ if emp_id:
+ if self.document_type == "passport":
+ emp_id.passport_id = self.id
+ elif self.document_type == "Iqama":
+ emp_id.iqama_number = self.id
+ elif self.document_type == "saudi":
+ emp_id.saudi_number = self.id
+ elif self.document_type == "license":
+ emp_id.license_number_id = self.id
+ elif self.document_type == "medical_Examination":
+ emp_id.copy_examination_file = self.id
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = super(HrEmployeeDocument, self).create(vals_list)
+ for record in records:
+ record.set_last_document()
+ return records
+
+ def name_get(self):
+ result = []
+ for record in self:
+ name = ""
+ if record.saudi_id:
+ name = record.saudi_id
+ elif record.passport_id:
+ name = record.passport_id
+ elif record.iqama_id:
+ name = record.iqama_id
+ elif record.license_id:
+ name = record.license_id
+ elif record.file_examination:
+ name = record.file_examination
+ elif record.document_type == "other":
+ name = record.name
+
+ result.append((record.id, name))
+ return result
+
+ @api.constrains("passport_id", "saudi_id", "iqama_id", "license_id", "file_examination")
+ def unique_fields(self):
+ for record in self:
+ domain = []
+ if record.document_type == "passport" and record.passport_id:
+ domain = [("passport_id", "=", record.passport_id), ("document_type", "=", record.document_type)]
+ elif record.document_type == "saudi" and record.saudi_id:
+ domain = [("saudi_id", "=", record.saudi_id), ("document_type", "=", record.document_type)]
+ elif record.document_type == "Iqama" and record.iqama_id:
+ domain = [("iqama_id", "=", record.iqama_id), ("document_type", "=", record.document_type)]
+ elif record.document_type == "license" and record.license_id:
+ domain = [("license_id", "=", record.license_id), ("document_type", "=", record.document_type)]
+ elif record.document_type == "medical_Examination" and record.file_examination:
+ domain = [("file_examination", "=", record.file_examination), ("document_type", "=", record.document_type)]
+
+ if domain:
+ duplicate_count = self.search_count(domain)
+ if duplicate_count > 1:
+ doc_type_names = {
+ "passport": "Passport",
+ "saudi": "Saudi Identity",
+ "Iqama": "Identity",
+ "license": "License",
+ "medical_Examination": "Medical Examination"
+ }
+ doc_name = doc_type_names.get(record.document_type, "Document")
+ raise ValidationError(_(f"This {doc_name} Number already exists"))
+
+
+class HrEmployee(models.Model):
+ _inherit = "hr.employee"
+
+ passport_id = fields.Many2one(
+ "hr.employee.document",
+ domain=[("document_type", "=", "passport")],
+ tracking=True,
+ )
+ expiry_license = fields.Date(
+ related="license_number_id.expiry_date",
+ readonly=True,
+ string="License Expiry Date"
+ )
+ license_number_id = fields.Many2one(
+ comodel_name="hr.employee.document",
+ domain="[('document_type','=','license')]"
+ )
+
+
+ document_ids = fields.One2many(
+ 'hr.employee.document',
+ 'employee_ref',
+ string="Documents"
+ )
+
+ document_count = fields.Integer(
+ compute="_compute_document_count",
+ string="# Documents"
+ )
+
+ @api.depends('document_ids')
+ def _compute_document_count(self):
+ for employee in self:
+ employee.document_count = len(employee.document_ids)
+
+ def document_view(self):
+ self.ensure_one()
+ return {
+ "name": _("Documents"),
+ "domain": [("employee_ref", "=", self.id)],
+ "res_model": "hr.employee.document",
+ "type": "ir.actions.act_window",
+ "view_mode": "tree,form",
+ "help": """
Click to Create for New Documents
""",
+ "limit": 80,
+ "context": {"default_employee_ref": self.id},
+ }
+
+
+
+class HrEmployeeAttachment(models.Model):
+ _inherit = "ir.attachment"
+
+ doc_attach_rel = fields.Many2many(
+ "hr.employee.document",
+ "doc_attachment_id",
+ "attach_id3",
+ "doc_id",
+ string="Attachment",
+ invisible=True
+ )
+
+
+class User(models.Model):
+ _inherit = "res.users"
+
+ passport_id = fields.Many2one(
+ "hr.employee.document",
+ related="employee_id.passport_id",
+ readonly=False
+ )
+
+
+class MembershipTypes(models.Model):
+ _name = 'membership.types'
+ _description = 'Membership Types'
+
+ name = fields.Char(required=True)
+
+
+class MembershipCategorys(models.Model):
+ _name = 'membership.categorys'
+ _description = 'Membership Categories'
+
+ name = fields.Char(required=True)
\ No newline at end of file
diff --git a/odex30_base/hr_docs_expiry/models/employee_entry_exit_check_list.py b/odex30_base/hr_docs_expiry/models/employee_entry_exit_check_list.py
new file mode 100644
index 0000000..7ae72e6
--- /dev/null
+++ b/odex30_base/hr_docs_expiry/models/employee_entry_exit_check_list.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+
+from odoo import fields, models
+
+
+class EmployeeEntryDocuments(models.Model):
+ _name = "employee.checklist"
+ _inherit = ["mail.thread", "mail.activity.mixin"]
+ _description = "Employee Documents"
+
+ name = fields.Char(string="Document Name", copy=False, required=True)
+ document_type = fields.Selection(
+ selection=[
+ ("entry", "Entry Process"),
+ ("exit", "Exit Process"),
+ ("other", "Other")
+ ],
+ string="Checklist Type",
+ required=True
+ )
+
+ def name_get(self):
+ result = []
+ for record in self:
+ name = record.name
+ if record.document_type == "entry":
+ name += "_en"
+ elif record.document_type == "exit":
+ name += "_ex"
+ elif record.document_type == "other":
+ name += "_ot"
+ result.append((record.id, name))
+ return result
\ No newline at end of file
diff --git a/odex30_base/hr_docs_expiry/models/res_users.py b/odex30_base/hr_docs_expiry/models/res_users.py
new file mode 100644
index 0000000..d87f560
--- /dev/null
+++ b/odex30_base/hr_docs_expiry/models/res_users.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+from odoo import fields, models
+
+
+class User(models.Model):
+ _inherit = "res.users"
+
+ passport_id = fields.Many2one(
+ "hr.employee.document",
+ related="employee_id.passport_id",
+ readonly=False
+ )
diff --git a/odex30_base/hr_docs_expiry/security/ir.model.access.csv b/odex30_base/hr_docs_expiry/security/ir.model.access.csv
new file mode 100644
index 0000000..f0676d2
--- /dev/null
+++ b/odex30_base/hr_docs_expiry/security/ir.model.access.csv
@@ -0,0 +1,10 @@
+id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
+access_hr_employee_checklist_user,employee.checklist.user,model_employee_checklist,hr.group_hr_user,1,1,1,1
+access_hr_employee_checklist_emp,employee.checklist.emp,model_employee_checklist,base.group_user,1,0,0,0
+access_hr_employee_document_employee,hr.employee.document_employee,model_hr_employee_document,base.group_user,1,0,0,0
+access_hr_employee_document_manager,hr.employee.document_manager,model_hr_employee_document,hr.group_hr_manager,1,1,1,1
+access_hr_employee_document_user,hr.employee.document_user,model_hr_employee_document,hr.group_hr_user,1,1,1,0
+access_membership_types_hr,Access.membership_types_hr,model_membership_types,hr.group_hr_user,1,1,1,1
+access_membership_types_emp,Access.membership_types_emp,model_membership_types,base.group_user,1,0,0,0
+access_membership_categorys_hr,Access.membership_categorys_hr,model_membership_categorys,hr.group_hr_user,1,1,1,1
+access_membership_categorys_emp,Access.membership_categorys_emp,model_membership_categorys,base.group_user,1,0,0,0
diff --git a/odex30_base/hr_docs_expiry/static/description/icon.png b/odex30_base/hr_docs_expiry/static/description/icon.png
new file mode 100644
index 0000000..4141f52
Binary files /dev/null and b/odex30_base/hr_docs_expiry/static/description/icon.png differ
diff --git a/odex30_base/hr_docs_expiry/static/src/css/website_rtl.css b/odex30_base/hr_docs_expiry/static/src/css/website_rtl.css
new file mode 100644
index 0000000..7a8ddfb
--- /dev/null
+++ b/odex30_base/hr_docs_expiry/static/src/css/website_rtl.css
@@ -0,0 +1,22 @@
+@media (min-width: 768px) {
+ .rtl .navbar-right {
+ float: left !important;
+ }
+ .rtl .navbar-right .dropdown .dropdown-menu {
+ right: auto !important;
+ left: 0 !important;
+ }
+ .rtl .navbar-left {
+ float: right !important;
+ }
+ .rtl .navbar-left .dropdown .dropdown-menu {
+ left: auto !important;
+ right: 0 !important;
+ }
+ .navbar-nav.navbar-right:last-child {
+ margin-left: auto;
+ }
+ .rtl .pull-left {
+ float: right !important;
+ }
+}
diff --git a/odex30_base/hr_docs_expiry/views/employee_check_list_view.xml b/odex30_base/hr_docs_expiry/views/employee_check_list_view.xml
new file mode 100644
index 0000000..b3d0caa
--- /dev/null
+++ b/odex30_base/hr_docs_expiry/views/employee_check_list_view.xml
@@ -0,0 +1,31 @@
+
+
+
+ employee.checklist.form
+ employee.checklist
+ form
+
+
+
+
+
+
+ employee.checklist.list
+ employee.checklist
+ list
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/odex30_base/hr_docs_expiry/views/employee_document_view.xml b/odex30_base/hr_docs_expiry/views/employee_document_view.xml
new file mode 100644
index 0000000..d0b8ce8
--- /dev/null
+++ b/odex30_base/hr_docs_expiry/views/employee_document_view.xml
@@ -0,0 +1,118 @@
+
+
+
+ Employee document
+ hr.employee.document
+ list,form
+
+
+
+ hr.employee.document.form
+ hr.employee.document
+ form
+
+
+
+
+
+
+
+ Employees Document inherit
+ hr.employee.document
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.employee.document.list
+ hr.employee.document
+ list
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/inbox_notif_email/__init__.py b/odex30_base/inbox_notif_email/__init__.py
similarity index 100%
rename from inbox_notif_email/__init__.py
rename to odex30_base/inbox_notif_email/__init__.py
diff --git a/inbox_notif_email/__manifest__.py b/odex30_base/inbox_notif_email/__manifest__.py
similarity index 100%
rename from inbox_notif_email/__manifest__.py
rename to odex30_base/inbox_notif_email/__manifest__.py
diff --git a/inbox_notif_email/controllers/__init__.py b/odex30_base/inbox_notif_email/controllers/__init__.py
similarity index 100%
rename from inbox_notif_email/controllers/__init__.py
rename to odex30_base/inbox_notif_email/controllers/__init__.py
diff --git a/inbox_notif_email/controllers/controllers.py b/odex30_base/inbox_notif_email/controllers/controllers.py
similarity index 100%
rename from inbox_notif_email/controllers/controllers.py
rename to odex30_base/inbox_notif_email/controllers/controllers.py
diff --git a/inbox_notif_email/demo/demo.xml b/odex30_base/inbox_notif_email/demo/demo.xml
similarity index 100%
rename from inbox_notif_email/demo/demo.xml
rename to odex30_base/inbox_notif_email/demo/demo.xml
diff --git a/inbox_notif_email/models/__init__.py b/odex30_base/inbox_notif_email/models/__init__.py
similarity index 100%
rename from inbox_notif_email/models/__init__.py
rename to odex30_base/inbox_notif_email/models/__init__.py
diff --git a/inbox_notif_email/models/models.py b/odex30_base/inbox_notif_email/models/models.py
similarity index 100%
rename from inbox_notif_email/models/models.py
rename to odex30_base/inbox_notif_email/models/models.py
diff --git a/inbox_notif_email/security/ir.model.access.csv b/odex30_base/inbox_notif_email/security/ir.model.access.csv
similarity index 100%
rename from inbox_notif_email/security/ir.model.access.csv
rename to odex30_base/inbox_notif_email/security/ir.model.access.csv
diff --git a/inbox_notif_email/static/description/banner.PNG b/odex30_base/inbox_notif_email/static/description/banner.PNG
similarity index 100%
rename from inbox_notif_email/static/description/banner.PNG
rename to odex30_base/inbox_notif_email/static/description/banner.PNG
diff --git a/inbox_notif_email/static/description/icon.png b/odex30_base/inbox_notif_email/static/description/icon.png
similarity index 100%
rename from inbox_notif_email/static/description/icon.png
rename to odex30_base/inbox_notif_email/static/description/icon.png
diff --git a/inbox_notif_email/static/description/image1.png b/odex30_base/inbox_notif_email/static/description/image1.png
similarity index 100%
rename from inbox_notif_email/static/description/image1.png
rename to odex30_base/inbox_notif_email/static/description/image1.png
diff --git a/inbox_notif_email/static/description/image10.png b/odex30_base/inbox_notif_email/static/description/image10.png
similarity index 100%
rename from inbox_notif_email/static/description/image10.png
rename to odex30_base/inbox_notif_email/static/description/image10.png
diff --git a/inbox_notif_email/static/description/image2.png b/odex30_base/inbox_notif_email/static/description/image2.png
similarity index 100%
rename from inbox_notif_email/static/description/image2.png
rename to odex30_base/inbox_notif_email/static/description/image2.png
diff --git a/inbox_notif_email/static/description/image3.png b/odex30_base/inbox_notif_email/static/description/image3.png
similarity index 100%
rename from inbox_notif_email/static/description/image3.png
rename to odex30_base/inbox_notif_email/static/description/image3.png
diff --git a/inbox_notif_email/static/description/image4.png b/odex30_base/inbox_notif_email/static/description/image4.png
similarity index 100%
rename from inbox_notif_email/static/description/image4.png
rename to odex30_base/inbox_notif_email/static/description/image4.png
diff --git a/inbox_notif_email/static/description/image5.png b/odex30_base/inbox_notif_email/static/description/image5.png
similarity index 100%
rename from inbox_notif_email/static/description/image5.png
rename to odex30_base/inbox_notif_email/static/description/image5.png
diff --git a/inbox_notif_email/static/description/image6.png b/odex30_base/inbox_notif_email/static/description/image6.png
similarity index 100%
rename from inbox_notif_email/static/description/image6.png
rename to odex30_base/inbox_notif_email/static/description/image6.png
diff --git a/inbox_notif_email/static/description/image7.png b/odex30_base/inbox_notif_email/static/description/image7.png
similarity index 100%
rename from inbox_notif_email/static/description/image7.png
rename to odex30_base/inbox_notif_email/static/description/image7.png
diff --git a/inbox_notif_email/static/description/image8.png b/odex30_base/inbox_notif_email/static/description/image8.png
similarity index 100%
rename from inbox_notif_email/static/description/image8.png
rename to odex30_base/inbox_notif_email/static/description/image8.png
diff --git a/inbox_notif_email/static/description/image9.png b/odex30_base/inbox_notif_email/static/description/image9.png
similarity index 100%
rename from inbox_notif_email/static/description/image9.png
rename to odex30_base/inbox_notif_email/static/description/image9.png
diff --git a/inbox_notif_email/static/description/index.html b/odex30_base/inbox_notif_email/static/description/index.html
similarity index 100%
rename from inbox_notif_email/static/description/index.html
rename to odex30_base/inbox_notif_email/static/description/index.html
diff --git a/inbox_notif_email/views/templates.xml b/odex30_base/inbox_notif_email/views/templates.xml
similarity index 100%
rename from inbox_notif_email/views/templates.xml
rename to odex30_base/inbox_notif_email/views/templates.xml
diff --git a/inbox_notif_email/views/views.xml b/odex30_base/inbox_notif_email/views/views.xml
similarity index 100%
rename from inbox_notif_email/views/views.xml
rename to odex30_base/inbox_notif_email/views/views.xml
diff --git a/odex30_base/queue_job/README.rst b/odex30_base/queue_job/README.rst
new file mode 100644
index 0000000..88b5a4d
--- /dev/null
+++ b/odex30_base/queue_job/README.rst
@@ -0,0 +1,725 @@
+.. 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:58f9182440bb316576671959b69148ea5454958f9ae8db75bccd30c89012676d
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Mature
+.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
+ :alt: License: LGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fqueue-lightgray.png?logo=github
+ :target: https://github.com/OCA/queue/tree/18.0/queue_job
+ :alt: OCA/queue
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/queue-18-0/queue-18-0-queue_job
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=18.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This addon adds an integrated Job Queue to Odoo.
+
+It allows to postpone method calls executed asynchronously.
+
+Jobs are executed in the background by a ``Jobrunner``, in their own
+transaction.
+
+Example:
+
+.. code:: python
+
+ from odoo import models, fields, api
+
+ class MyModel(models.Model):
+ _name = 'my.model'
+
+ def my_method(self, a, k=None):
+ _logger.info('executed with a: %s and k: %s', a, k)
+
+
+ class MyOtherModel(models.Model):
+ _name = 'my.other.model'
+
+ def button_do_stuff(self):
+ self.env['my.model'].with_delay().my_method('a', k=2)
+
+In the snippet of code above, when we call ``button_do_stuff``, a job
+**capturing the method and arguments** will be postponed. It will be
+executed as soon as the Jobrunner has a free bucket, which can be
+instantaneous if no other job is running.
+
+Features:
+
+- Views for jobs, jobs are stored in PostgreSQL
+- Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL's
+ NOTIFY
+- Channels: give a capacity for the root channel and its sub-channels
+ and segregate jobs in them. Allow for instance to restrict heavy jobs
+ to be executed one at a time while little ones are executed 4 at a
+ times.
+- Retries: Ability to retry jobs by raising a type of exception
+- Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next
+ tries, retry after 1 minutes, ...
+- Job properties: priorities, estimated time of arrival (ETA), custom
+ description, number of retries
+- Related Actions: link an action on the job view, such as open the
+ record concerned by the job
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Installation
+============
+
+Be sure to have the ``requests`` library.
+
+Configuration
+=============
+
+- Using environment variables and command line:
+
+ - Adjust environment variables (optional):
+
+ - ``ODOO_QUEUE_JOB_CHANNELS=root:4`` or any other channels
+ configuration. The default is ``root:1``
+ - if ``xmlrpc_port`` is not set: ``ODOO_QUEUE_JOB_PORT=8069``
+
+ - Start Odoo with ``--load=web,queue_job`` and ``--workers`` greater
+ than 1. [1]_
+
+- Using the Odoo configuration file:
+
+.. code:: ini
+
+ [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:
+
+::
+
+ ...INFO...queue_job.jobrunner.runner: starting
+ ...INFO...queue_job.jobrunner.runner: initializing database connections
+ ...INFO...queue_job.jobrunner.runner: queue job runner ready for db
+ ...INFO...queue_job.jobrunner.runner: database connections ready
+
+- Create jobs (eg using ``base_import_async``) and observe they start
+ immediately and in parallel.
+- Tip: to enable debug logging for the queue job, use
+ ``--log-handler=odoo.addons.queue_job:DEBUG``
+
+- Jobs that remain in ``enqueued`` or ``started`` state (because, for
+ instance, their worker has been killed) will be automatically
+ re-queued.
+
+.. [1]
+ It works with the threaded Odoo server too, although this way of
+ running Odoo is obviously not for production purposes.
+
+Usage
+=====
+
+To use this module, you need to:
+
+1. Go to ``Job Queue`` menu
+
+Developers
+----------
+
+Delaying jobs
+~~~~~~~~~~~~~
+
+The fast way to enqueue a job for a method is to use ``with_delay()`` on
+a record or model:
+
+.. code:: 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:: 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:: 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:: 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:: 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:: 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:: python
+
+ def button_increment_var(self):
+ (
+ self
+ .delayable()
+ .increment_counter()
+ .split(1, chain=True) # Will exceute the jobs one after the other
+ .delay()
+ )
+
+Enqueing Job Options
+~~~~~~~~~~~~~~~~~~~~
+
+- priority: default is 10, the closest it is to 0, the faster it will be
+ executed
+- eta: Estimated Time of Arrival of the job. It will not be executed
+ before this date/time
+- max_retries: default is 5, maximum number of retries before giving up
+ and set the job state to 'failed'. A value of 0 means infinite
+ retries.
+- description: human description of the job. If not set, description is
+ computed from the function doc or method name
+- channel: the complete name of the channel to use to process the
+ function. If specified it overrides the one defined on the function
+- identity_key: key uniquely identifying the job, if specified and a job
+ with the same key has not yet been run, the new job will not be
+ created
+
+Configure default options for jobs
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In earlier versions, jobs could be configured using the ``@job``
+decorator. This is now obsolete, they can be configured using optional
+``queue.job.function`` and ``queue.job.channel`` XML records.
+
+Example of channel:
+
+.. code:: XML
+
+
+ sale
+
+
+
+Example of job function:
+
+.. code:: XML
+
+
+
+ action_done
+
+
+
+
+
+The general form for the ``name`` is: ``.method``.
+
+The channel, related action and retry pattern options are optional, they
+are documented below.
+
+When writing modules, if 2+ modules add a job function or channel with
+the same name (and parent for channels), they'll be merged in the same
+record, even if they have different xmlids. On uninstall, the merged
+record is deleted when all the modules using it are uninstalled.
+
+**Job function: model**
+
+If the function is defined in an abstract model, you can not write
+````
+but you have to define a function for each model that inherits from the
+abstract model.
+
+**Job function: channel**
+
+The channel where the job will be delayed. The default channel is
+``root``.
+
+**Job function: related action**
+
+The *Related Action* appears as a button on the Job's view. The button
+will execute the defined action.
+
+The default one is to open the view of the record related to the job
+(form view when there is a single record, list view for several
+records). In many cases, the default related action is enough and
+doesn't need customization, but it can be customized by providing a
+dictionary on the job function:
+
+.. code:: python
+
+ {
+ "enable": False,
+ "func_name": "related_action_partner",
+ "kwargs": {"name": "Partner"},
+ }
+
+- ``enable``: when ``False``, the button has no effect (default:
+ ``True``)
+- ``func_name``: name of the method on ``queue.job`` that returns an
+ action
+- ``kwargs``: extra arguments to pass to the related action method
+
+Example of related action code:
+
+.. code:: python
+
+ class QueueJob(models.Model):
+ _inherit = 'queue.job'
+
+ def related_action_partner(self, name):
+ self.ensure_one()
+ model = self.model_name
+ partner = self.records
+ action = {
+ 'name': name,
+ 'type': 'ir.actions.act_window',
+ 'res_model': model,
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_id': partner.id,
+ }
+ return action
+
+**Job function: retry pattern**
+
+When a job fails with a retryable error type, it is automatically
+retried later. By default, the retry is always 10 minutes later.
+
+A retry pattern can be configured on the job function. What a pattern
+represents is "from X tries, postpone to Y seconds". It is expressed as
+a dictionary where keys are tries and values are seconds to postpone as
+integers:
+
+.. code:: python
+
+ {
+ 1: 10,
+ 5: 20,
+ 10: 30,
+ 15: 300,
+ }
+
+Based on this configuration, we can tell that:
+
+- 5 first retries are postponed 10 seconds later
+- retries 5 to 10 postponed 20 seconds later
+- retries 10 to 15 postponed 30 seconds later
+- all subsequent retries postponed 5 minutes later
+
+**Job Context**
+
+The context of the recordset of the job, or any recordset passed in
+arguments of a job, is transferred to the job according to an
+allow-list.
+
+The default allow-list is ("tz", "lang", "allowed_company_ids",
+"force_company", "active_test"). It can be customized in
+``Base._job_prepare_context_before_enqueue_keys``. **Bypass jobs on
+running Odoo**
+
+When you are developing (ie: connector modules) you might want to bypass
+the queue job and run your code immediately.
+
+To do so you can set QUEUE_JOB\__NO_DELAY=1 in your environment.
+
+**Bypass jobs in tests**
+
+When writing tests on job-related methods is always tricky to deal with
+delayed recordsets. To make your testing life easier you can set
+queue_job\__no_delay=True in the context.
+
+Tip: you can do this at test case level like this
+
+.. code:: python
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.env = cls.env(context=dict(
+ cls.env.context,
+ queue_job__no_delay=True, # no jobs thanks
+ ))
+
+Then all your tests execute the job methods synchronously without
+delaying any jobs.
+
+Testing
+~~~~~~~
+
+**Asserting enqueued jobs**
+
+The recommended way to test jobs, rather than running them directly and
+synchronously is to split the tests in two parts:
+
+ - one test where the job is mocked (trap jobs with ``trap_jobs()``
+ and the test only verifies that the job has been delayed with the
+ expected arguments
+ - one test that only calls the method of the job synchronously, to
+ validate the proper behavior of this method only
+
+Proceeding this way means that you can prove that jobs will be enqueued
+properly at runtime, and it ensures your code does not have a different
+behavior in tests and in production (because running your jobs
+synchronously may have a different behavior as they are in the same
+transaction / in the middle of the method). Additionally, it gives more
+control on the arguments you want to pass when calling the job's method
+(synchronously, this time, in the second type of tests), and it makes
+tests smaller.
+
+The best way to run such assertions on the enqueued jobs is to use
+``odoo.addons.queue_job.tests.common.trap_jobs()``.
+
+A very small example (more details in ``tests/common.py``):
+
+.. code:: 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:: 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:: python
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.env = cls.env(context=dict(
+ cls.env.context,
+ queue_job__no_delay=True, # no jobs thanks
+ ))
+
+Then all your tests execute the job methods synchronously without
+delaying any jobs.
+
+In tests you'll have to mute the logger like:
+
+ @mute_logger('odoo.addons.queue_job.models.base')
+
+Note
+
+in graphs of jobs, the ``queue_job__no_delay`` context key must be in at
+least one job's env of the graph for the whole graph to be executed
+synchronously
+
+Tips and tricks
+~~~~~~~~~~~~~~~
+
+- **Idempotency**
+ (https://www.restapitutorial.com/lessons/idempotency.html): The
+ queue_job should be idempotent so they can be retried several times
+ without impact on the data.
+- **The job should test at the very beginning its relevance**: the
+ moment the job will be executed is unknown by design. So the first
+ task of a job should be to check if the related work is still relevant
+ at the moment of the execution.
+
+Patterns
+~~~~~~~~
+
+Through the time, two main patterns emerged:
+
+1. For data exposed to users, a model should store the data and the
+ model should be the creator of the job. The job is kept hidden from
+ the users
+2. For technical data, that are not exposed to the users, it is
+ generally alright to create directly jobs with data passed as
+ arguments to the job, without intermediary models.
+
+Known issues / Roadmap
+======================
+
+- After creating a new database or installing ``queue_job`` on an
+ existing database, Odoo must be restarted for the runner to detect it.
+- When Odoo shuts down normally, it waits for running jobs to finish.
+ However, when the Odoo server crashes or is otherwise force-stopped,
+ running jobs are interrupted while the runner has no chance to know
+ they have been aborted. In such situations, jobs may remain in
+ ``started`` or ``enqueued`` state after the Odoo server is halted.
+ Since the runner has no way to know if they are actually running or
+ not, and does not know for sure if it is safe to restart the jobs, it
+ does not attempt to restart them automatically. Such stale jobs
+ therefore fill the running queue and prevent other jobs to start. You
+ must therefore requeue them manually, either from the Jobs view, or by
+ running the following SQL statement *before starting Odoo*:
+
+.. code:: sql
+
+ update queue_job set state='pending' where state in ('started', 'enqueued')
+
+Changelog
+=========
+
+Next
+----
+
+- [ADD] Run jobrunner as a worker process instead of a thread in the
+ main process (when running with --workers > 0)
+- [REF] ``@job`` and ``@related_action`` deprecated, any method can be
+ delayed, and configured using ``queue.job.function`` records
+- [MIGRATION] from 13.0 branched at rev. e24ff4b
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+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.
+
+Credits
+=======
+
+Authors
+-------
+
+* Camptocamp
+* ACSONE SA/NV
+
+Contributors
+------------
+
+- Guewen Baconnier
+- Stéphane Bidoul
+- Matthieu Dietrich
+- Jos De Graeve
+- David Lefever
+- Laurent Mignon
+- Laetitia Gangloff
+- Cédric Pigeon
+- Tatiana Deribina
+- Souheil Bejaoui
+- Eric Antones
+- Simone Orsi
+- Nguyen Minh Chien
+- Tran Quoc Duong
+- Vo Hong Thien
+
+Other credits
+-------------
+
+The migration of this module from 17.0 to 18.0 was financially supported
+by Camptocamp.
+
+Maintainers
+-----------
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+.. |maintainer-guewen| image:: https://github.com/guewen.png?size=40px
+ :target: https://github.com/guewen
+ :alt: guewen
+
+Current `maintainer `__:
+
+|maintainer-guewen|
+
+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/odex30_base/queue_job/__init__.py b/odex30_base/queue_job/__init__.py
new file mode 100644
index 0000000..6ca18c3
--- /dev/null
+++ b/odex30_base/queue_job/__init__.py
@@ -0,0 +1,10 @@
+from . import controllers
+from . import fields
+from . import models
+from . import wizards
+from . import jobrunner
+from .post_init_hook import post_init_hook
+from .post_load import post_load
+
+# shortcuts
+from .job import identity_exact
diff --git a/odex30_base/queue_job/__manifest__.py b/odex30_base/queue_job/__manifest__.py
new file mode 100644
index 0000000..69211ec
--- /dev/null
+++ b/odex30_base/queue_job/__manifest__.py
@@ -0,0 +1,35 @@
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+{
+ "name": "Job Queue",
+ "version": "18.0.2.0.2",
+ "author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/queue",
+ "license": "LGPL-3",
+ "category": "Generic Modules",
+ "depends": ["mail", "base_sparse_field", "web"],
+ "external_dependencies": {"python": ["requests"]},
+ "data": [
+ "security/security.xml",
+ "security/ir.model.access.csv",
+ "views/queue_job_views.xml",
+ "views/queue_job_channel_views.xml",
+ "views/queue_job_function_views.xml",
+ "wizards/queue_jobs_to_done_views.xml",
+ "wizards/queue_jobs_to_cancelled_views.xml",
+ "wizards/queue_requeue_job_views.xml",
+ "views/queue_job_menus.xml",
+ "data/queue_data.xml",
+ "data/queue_job_function_data.xml",
+ ],
+ "assets": {
+ "web.assets_backend": [
+ "/queue_job/static/src/views/**/*",
+ ],
+ },
+ "installable": True,
+ "development_status": "Mature",
+ "maintainers": ["guewen"],
+ "post_init_hook": "post_init_hook",
+ "post_load": "post_load",
+}
diff --git a/odex30_base/queue_job/controllers/__init__.py b/odex30_base/queue_job/controllers/__init__.py
new file mode 100644
index 0000000..12a7e52
--- /dev/null
+++ b/odex30_base/queue_job/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
diff --git a/odex30_base/queue_job/controllers/main.py b/odex30_base/queue_job/controllers/main.py
new file mode 100644
index 0000000..6365e6e
--- /dev/null
+++ b/odex30_base/queue_job/controllers/main.py
@@ -0,0 +1,300 @@
+# Copyright (c) 2015-2016 ACSONE SA/NV ()
+# Copyright 2013-2016 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+import logging
+import random
+import time
+import traceback
+from io import StringIO
+
+from psycopg2 import OperationalError, errorcodes
+from werkzeug.exceptions import BadRequest, Forbidden
+
+from odoo import SUPERUSER_ID, _, api, http
+from odoo.modules.registry import Registry
+from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY
+
+from ..delay import chain, group
+from ..exception import FailedJobError, RetryableJobError
+from ..job import ENQUEUED, Job
+
+_logger = logging.getLogger(__name__)
+
+PG_RETRY = 5 # seconds
+
+DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE = 5
+
+
+class RunJobController(http.Controller):
+ def _try_perform_job(self, env, job):
+ """Try to perform the job."""
+ job.set_started()
+ job.store()
+ env.cr.commit()
+ job.lock()
+
+ _logger.debug("%s started", job)
+
+ job.perform()
+ # Triggers any stored computed fields before calling 'set_done'
+ # so that will be part of the 'exec_time'
+ env.flush_all()
+ job.set_done()
+ job.store()
+ env.flush_all()
+ env.cr.commit()
+ _logger.debug("%s done", job)
+
+ def _enqueue_dependent_jobs(self, env, job):
+ tries = 0
+ while True:
+ try:
+ job.enqueue_waiting()
+ except OperationalError as err:
+ # Automatically retry the typical transaction serialization
+ # errors
+ if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
+ raise
+ if tries >= DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE:
+ _logger.info(
+ "%s, maximum number of tries reached to update dependencies",
+ errorcodes.lookup(err.pgcode),
+ )
+ raise
+ wait_time = random.uniform(0.0, 2**tries)
+ tries += 1
+ _logger.info(
+ "%s, retry %d/%d in %.04f sec...",
+ errorcodes.lookup(err.pgcode),
+ tries,
+ DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE,
+ wait_time,
+ )
+ time.sleep(wait_time)
+ else:
+ break
+
+ @http.route(
+ "/queue_job/runjob",
+ type="http",
+ auth="none",
+ save_session=False,
+ readonly=False,
+ )
+ def runjob(self, db, job_uuid, **kw):
+ http.request.session.db = db
+ env = http.request.env(user=SUPERUSER_ID)
+
+ def retry_postpone(job, message, seconds=None):
+ job.env.clear()
+ with Registry(job.env.cr.dbname).cursor() as new_cr:
+ job.env = api.Environment(new_cr, SUPERUSER_ID, {})
+ job.postpone(result=message, seconds=seconds)
+ job.set_pending(reset_retry=False)
+ job.store()
+
+ # ensure the job to run is in the correct state and lock the record
+ env.cr.execute(
+ "SELECT state FROM queue_job WHERE uuid=%s AND state=%s FOR UPDATE",
+ (job_uuid, ENQUEUED),
+ )
+ if not env.cr.fetchone():
+ _logger.warning(
+ "was requested to run job %s, but it does not exist, "
+ "or is not in state %s",
+ job_uuid,
+ ENQUEUED,
+ )
+ return ""
+
+ job = Job.load(env, job_uuid)
+ assert job and job.state == ENQUEUED
+
+ try:
+ try:
+ self._try_perform_job(env, job)
+ except OperationalError as err:
+ # Automatically retry the typical transaction serialization
+ # errors
+ if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
+ raise
+
+ _logger.debug("%s OperationalError, postponed", job)
+ raise RetryableJobError(err.pgerror, seconds=PG_RETRY) from err
+
+ except RetryableJobError as err:
+ # delay the job later, requeue
+ retry_postpone(job, str(err), seconds=err.seconds)
+ _logger.debug("%s postponed", job)
+ # Do not trigger the error up because we don't want an exception
+ # traceback in the logs we should have the traceback when all
+ # retries are exhausted
+ env.cr.rollback()
+ return ""
+
+ except (FailedJobError, Exception) as orig_exception:
+ buff = StringIO()
+ traceback.print_exc(file=buff)
+ traceback_txt = buff.getvalue()
+ _logger.error(traceback_txt)
+ job.env.clear()
+ with Registry(job.env.cr.dbname).cursor() as new_cr:
+ job.env = job.env(cr=new_cr)
+ vals = self._get_failure_values(job, traceback_txt, orig_exception)
+ job.set_failed(**vals)
+ job.store()
+ buff.close()
+ raise
+
+ _logger.debug("%s enqueue depends started", job)
+ self._enqueue_dependent_jobs(env, job)
+ _logger.debug("%s enqueue depends done", job)
+
+ return ""
+
+ def _get_failure_values(self, job, traceback_txt, orig_exception):
+ """Collect relevant data from exception."""
+ exception_name = orig_exception.__class__.__name__
+ if hasattr(orig_exception, "__module__"):
+ exception_name = orig_exception.__module__ + "." + exception_name
+ exc_message = (
+ orig_exception.args[0] if orig_exception.args else str(orig_exception)
+ )
+ return {
+ "exc_info": traceback_txt,
+ "exc_name": exception_name,
+ "exc_message": exc_message,
+ }
+
+ # flake8: noqa: C901
+ @http.route("/queue_job/create_test_job", type="http", auth="user")
+ def create_test_job(
+ self,
+ priority=None,
+ max_retries=None,
+ channel=None,
+ description="Test job",
+ size=1,
+ failure_rate=0,
+ ):
+ if not http.request.env.user.has_group("base.group_erp_manager"):
+ raise Forbidden(_("Access Denied"))
+
+ if failure_rate is not None:
+ try:
+ failure_rate = float(failure_rate)
+ except (ValueError, TypeError):
+ failure_rate = 0
+
+ if not (0 <= failure_rate <= 1):
+ raise BadRequest("failure_rate must be between 0 and 1")
+
+ if size is not None:
+ try:
+ size = int(size)
+ except (ValueError, TypeError):
+ size = 1
+
+ if priority is not None:
+ try:
+ priority = int(priority)
+ except ValueError:
+ priority = None
+
+ if max_retries is not None:
+ try:
+ max_retries = int(max_retries)
+ except ValueError:
+ max_retries = None
+
+ if size == 1:
+ return self._create_single_test_job(
+ priority=priority,
+ max_retries=max_retries,
+ channel=channel,
+ description=description,
+ failure_rate=failure_rate,
+ )
+
+ if size > 1:
+ return self._create_graph_test_jobs(
+ size,
+ priority=priority,
+ max_retries=max_retries,
+ channel=channel,
+ description=description,
+ failure_rate=failure_rate,
+ )
+ return ""
+
+ def _create_single_test_job(
+ self,
+ priority=None,
+ max_retries=None,
+ channel=None,
+ description="Test job",
+ size=1,
+ failure_rate=0,
+ ):
+ delayed = (
+ http.request.env["queue.job"]
+ .with_delay(
+ priority=priority,
+ max_retries=max_retries,
+ channel=channel,
+ description=description,
+ )
+ ._test_job(failure_rate=failure_rate)
+ )
+ return f"job uuid: {delayed.db_record().uuid}"
+
+ TEST_GRAPH_MAX_PER_GROUP = 5
+
+ def _create_graph_test_jobs(
+ self,
+ size,
+ priority=None,
+ max_retries=None,
+ channel=None,
+ description="Test job",
+ failure_rate=0,
+ ):
+ model = http.request.env["queue.job"]
+ current_count = 0
+
+ possible_grouping_methods = (chain, group)
+
+ tails = [] # we can connect new graph chains/groups to tails
+ root_delayable = None
+ while current_count < size:
+ jobs_count = min(
+ size - current_count, random.randint(1, self.TEST_GRAPH_MAX_PER_GROUP)
+ )
+
+ jobs = []
+ for __ in range(jobs_count):
+ current_count += 1
+ jobs.append(
+ model.delayable(
+ priority=priority,
+ max_retries=max_retries,
+ channel=channel,
+ description="%s #%d" % (description, current_count),
+ )._test_job(failure_rate=failure_rate)
+ )
+
+ grouping = random.choice(possible_grouping_methods)
+ delayable = grouping(*jobs)
+ if not root_delayable:
+ root_delayable = delayable
+ else:
+ tail_delayable = random.choice(tails)
+ tail_delayable.on_done(delayable)
+ tails.append(delayable)
+
+ root_delayable.delay()
+
+ return (
+ f"graph uuid: {list(root_delayable._head())[0]._generated_job.graph_uuid}"
+ )
diff --git a/odex30_base/queue_job/data/queue_data.xml b/odex30_base/queue_job/data/queue_data.xml
new file mode 100644
index 0000000..1fb70f1
--- /dev/null
+++ b/odex30_base/queue_job/data/queue_data.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ Job failed
+ queue.job
+
+
+
+ AutoVacuum Job Queue
+
+
+
+ 1
+ days
+ code
+ model.autovacuum()
+
+
+
+
+ root
+
+
+
diff --git a/odex30_base/queue_job/data/queue_job_function_data.xml b/odex30_base/queue_job/data/queue_job_function_data.xml
new file mode 100644
index 0000000..0105dbc
--- /dev/null
+++ b/odex30_base/queue_job/data/queue_job_function_data.xml
@@ -0,0 +1,6 @@
+
+
+
+ _test_job
+
+
diff --git a/odex30_base/queue_job/delay.py b/odex30_base/queue_job/delay.py
new file mode 100644
index 0000000..0ba54e4
--- /dev/null
+++ b/odex30_base/queue_job/delay.py
@@ -0,0 +1,663 @@
+# 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(
+ f"Job {jobs[0]} is a single job, it should not have a graph uuid"
+ )
+ 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 f"DelayableChain(\n\t{inner_graph}\n)"
+
+ 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 f"DelayableGroup(\n\t{inner_graph}\n)"
+
+ def on_done(self, *delayables):
+ """Connects the current group to other delayables/chains/groups
+
+ The delayables/chains/groups passed in the parameters will be executed
+ when the current Group is done.
+ """
+ for parent in self._delayables:
+ for child in delayables:
+ self._graph.add_edge(parent, child)
+ return self
+
+ def delay(self):
+ """Delay the whole graph"""
+ self._graph.delay()
+
+
+class Delayable:
+ """Unit of a graph, one Delayable will lead to an enqueued job
+
+ Delayables can have dependencies on each others, as well as dependencies on
+ :class:`~DelayableGroup` or :class:`~DelayableChain` objects.
+
+ This class will generally not be used directly, it is used internally
+ by :meth:`~odoo.addons.queue_job.models.base.Base.delayable`. Look
+ in the base model for more details.
+
+ Delayables can be connected to other Delayable, DelayableChain or
+ DelayableGroup objects by using :meth:`~done`.
+
+ Properties of the future job can be set using the :meth:`~set` method,
+ which always return ``self``::
+
+ delayable.set(priority=15).set({"max_retries": 5, "eta": 15}).delay()
+
+ It can be used for example to set properties dynamically.
+
+ A Delayable is enqueued by calling :meth:`delay()`, which delays the whole
+ graph.
+ Important: :meth:`delay()` must be called on the top-level
+ delayable/chain/group object of the graph.
+ """
+
+ _properties = (
+ "priority",
+ "eta",
+ "max_retries",
+ "description",
+ "channel",
+ "identity_key",
+ )
+ __slots__ = _properties + (
+ "recordset",
+ "_graph",
+ "_job_method",
+ "_job_args",
+ "_job_kwargs",
+ "_generated_job",
+ )
+
+ def __init__(
+ self,
+ recordset,
+ priority=None,
+ eta=None,
+ max_retries=None,
+ description=None,
+ channel=None,
+ identity_key=None,
+ ):
+ self._graph = DelayableGraph()
+ self._graph.add_vertex(self)
+
+ self.recordset = recordset
+
+ self.priority = priority
+ self.eta = eta
+ self.max_retries = max_retries
+ self.description = description
+ self.channel = channel
+ self.identity_key = identity_key
+
+ self._job_method = None
+ self._job_args = ()
+ self._job_kwargs = {}
+
+ self._generated_job = None
+
+ def _head(self):
+ return [self]
+
+ def _tail(self):
+ return [self]
+
+ def __repr__(self):
+ return (
+ f"Delayable({self.recordset}."
+ f"{self._job_method.__name__ if self._job_method else ''}"
+ f"({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(f"No property {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 f"{self.recordset._name}.{self._job_method.__name__}"
+ )
+ for index, delayable in enumerate(delayables):
+ delayable.set(
+ description=f"{description} (split {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(
+ f"only methods can be delayed ({name} called on {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 (
+ f"DelayableRecordset({self.delayable.recordset._name}"
+ f"{getattr(self.delayable.recordset, '_ids', '')})"
+ )
+
+ __repr__ = __str__
diff --git a/odex30_base/queue_job/exception.py b/odex30_base/queue_job/exception.py
new file mode 100644
index 0000000..c04bc8f
--- /dev/null
+++ b/odex30_base/queue_job/exception.py
@@ -0,0 +1,38 @@
+# Copyright 2012-2016 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+
+class BaseQueueJobError(Exception):
+ """Base queue job error"""
+
+
+class JobError(BaseQueueJobError):
+ """A job had an error"""
+
+
+class NoSuchJobError(JobError):
+ """The job does not exist."""
+
+
+class FailedJobError(JobError):
+ """A job had an error having to be resolved."""
+
+
+class RetryableJobError(JobError):
+ """A job had an error but can be retried.
+
+ The job will be retried after the given number of seconds. If seconds is
+ empty, it will be retried according to the ``retry_pattern`` of the job or
+ by :const:`odoo.addons.queue_job.job.RETRY_INTERVAL` if nothing is defined.
+
+ If ``ignore_retry`` is True, the retry counter will not be increased.
+ """
+
+ def __init__(self, msg, seconds=None, ignore_retry=False):
+ super().__init__(msg)
+ self.seconds = seconds
+ self.ignore_retry = ignore_retry
+
+
+class ChannelNotFound(BaseQueueJobError):
+ """A channel could not be found"""
diff --git a/odex30_base/queue_job/fields.py b/odex30_base/queue_job/fields.py
new file mode 100644
index 0000000..8cb45af
--- /dev/null
+++ b/odex30_base/queue_job/fields.py
@@ -0,0 +1,133 @@
+# copyright 2016 Camptocamp
+# license lgpl-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+import json
+from datetime import date, datetime
+
+import dateutil
+import lxml
+from psycopg2.extras import Json as PsycopgJson
+
+from odoo import fields, models
+from odoo.tools.func import lazy
+
+
+class JobSerialized(fields.Json):
+ """Provide the storage for job fields stored as json
+
+ A base_type must be set, it must be dict, list or tuple.
+ When the field is not set, the json will be the corresponding
+ json string ("{}" or "[]").
+
+ Support for some custom types has been added to the json decoder/encoder
+ (see JobEncoder and JobDecoder).
+ """
+
+ type = "job_serialized"
+ _column_type = ("jsonb", "jsonb")
+
+ _base_type = None
+
+ # these are the default values when we convert an empty value
+ _default_json_mapping = {
+ dict: "{}",
+ list: "[]",
+ tuple: "[]",
+ models.BaseModel: lambda env: json.dumps(
+ {"_type": "odoo_recordset", "model": "base", "ids": [], "uid": env.uid}
+ ),
+ }
+
+ def __init__(self, string=fields.SENTINEL, base_type=fields.SENTINEL, **kwargs):
+ super().__init__(string=string, _base_type=base_type, **kwargs)
+
+ def _setup_attrs(self, model, name): # pylint: disable=missing-return
+ super()._setup_attrs(model, name)
+ if self._base_type not in self._default_json_mapping:
+ raise ValueError(f"{self._base_type} is not a supported base type")
+
+ def _base_type_default_json(self, env):
+ default_json = self._default_json_mapping.get(self._base_type)
+ if not isinstance(default_json, str):
+ default_json = default_json(env)
+ return default_json
+
+ def convert_to_column(self, value, record, values=None, validate=True):
+ value = self.convert_to_cache(value, record, validate=validate)
+ return PsycopgJson(value)
+
+ def convert_to_cache(self, value, record, validate=True):
+ # cache format: json.dumps(value) or None
+ if isinstance(value, self._base_type):
+ return json.dumps(value, cls=JobEncoder)
+ else:
+ return value or None
+
+ def convert_to_record(self, value, record):
+ default = self._base_type_default_json(record.env)
+ value = value or default
+ if not isinstance(value, (str | bytes | bytearray)):
+ value = json.dumps(value, cls=JobEncoder)
+ return json.loads(value, cls=JobDecoder, env=record.env)
+
+ def convert_to_export(self, value, record):
+ if not value:
+ return ""
+ return json.dumps(value, cls=JobEncoder)
+
+
+class JobEncoder(json.JSONEncoder):
+ """Encode Odoo recordsets so that we can later recompose them"""
+
+ def _get_record_context(self, obj):
+ return obj._job_prepare_context_before_enqueue()
+
+ def default(self, obj):
+ if isinstance(obj, models.BaseModel):
+ return {
+ "_type": "odoo_recordset",
+ "model": obj._name,
+ "ids": obj.ids,
+ "uid": obj.env.uid,
+ "su": obj.env.su,
+ "context": self._get_record_context(obj),
+ }
+ elif isinstance(obj, datetime):
+ return {"_type": "datetime_isoformat", "value": obj.isoformat()}
+ elif isinstance(obj, date):
+ return {"_type": "date_isoformat", "value": obj.isoformat()}
+ elif isinstance(obj, lxml.etree._Element):
+ return {
+ "_type": "etree_element",
+ "value": lxml.etree.tostring(obj, encoding=str),
+ }
+ elif isinstance(obj, lazy):
+ return obj._value
+ return json.JSONEncoder.default(self, obj)
+
+
+class JobDecoder(json.JSONDecoder):
+ """Decode json, recomposing recordsets"""
+
+ def __init__(self, *args, **kwargs):
+ env = kwargs.pop("env")
+ super().__init__(*args, object_hook=self.object_hook, **kwargs)
+ assert env
+ self.env = env
+
+ def object_hook(self, obj):
+ if "_type" not in obj:
+ return obj
+ type_ = obj["_type"]
+ if type_ == "odoo_recordset":
+ model = self.env(user=obj.get("uid"), su=obj.get("su"))[obj["model"]]
+ if obj.get("context"):
+ model = model.with_context(**obj.get("context"))
+ return model.browse(obj["ids"])
+ elif type_ == "datetime_isoformat":
+ return dateutil.parser.parse(obj["value"])
+ elif type_ == "date_isoformat":
+ return dateutil.parser.parse(obj["value"]).date()
+ elif type_ == "etree_element":
+ return lxml.etree.fromstring(obj["value"])
+ return obj
diff --git a/odex30_base/queue_job/i18n/de.po b/odex30_base/queue_job/i18n/de.po
new file mode 100644
index 0000000..a7d032f
--- /dev/null
+++ b/odex30_base/queue_job/i18n/de.po
@@ -0,0 +1,959 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2022-11-04 14:44+0000\n"
+"Last-Translator: Maria Sparenberg \n"
+"Language-Team: none\n"
+"Language: de\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.14.1\n"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid ""
+" \n"
+" If the max. "
+"retries is 0, the number of retries is infinite."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/controllers/main.py:0
+msgid "Access Denied"
+msgstr "Zugriff verweigert"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
+msgid "Action Needed"
+msgstr "Aktion notwendig"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
+msgid "Activities"
+msgstr "Aktivitäten"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
+msgid "Activity Exception Decoration"
+msgstr "Aussehen von Aktivitätsfehlern"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
+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 "Icon für Aktivitätstyp"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
+msgid "Args"
+msgstr "Argumente"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
+msgid "Attachment Count"
+msgstr "Anzahl der Anhänge"
+
+#. module: queue_job
+#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server
+msgid "AutoVacuum Job Queue"
+msgstr "AutoVacuum für Job-Warteschlange"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_base
+msgid "Base"
+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
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Cancelled by {}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot change the root channel"
+msgstr "Der Root-Kanal kann nicht geändert werden"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot remove the root channel"
+msgstr "Der Root-Kanal kann nicht entfernt werden"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Channel"
+msgstr "Kanal"
+
+#. module: queue_job
+#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
+msgid "Channel complete name must be unique"
+msgstr "Der vollständige Name des Kanals muss eindeutig sein"
+
+#. 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 "Kanäle"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
+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
+msgid "Complete Name"
+msgstr "Vollständiger Name"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
+msgid "Created Date"
+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_job_lock__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
+msgid "Created by"
+msgstr "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_job_lock__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
+msgid "Created on"
+msgstr "Erstellt am"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
+msgid "Current try"
+msgstr "Aktueller Versuch"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+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_queue_job__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
+msgid "Display Name"
+msgstr "Anzeigename"
+
+#. 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 "Erledigt"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr "Zeit der Einreihung"
+
+#. 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 "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"
+msgstr "Exception-Info"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+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
+msgid "Failed"
+msgstr "Fehlgeschlagen"
+
+#. 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 "Abonnenten"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
+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 "Font Awesome Icon z.B. fa-tasks"
+
+#. 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 "Gruppieren nach"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
+msgid "Has Message"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
+msgid "ID"
+msgstr "ID"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon"
+msgstr "Icon"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr "Dies ist das Icon zur Kennzeichnung eines Aktivitätsfehlers."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr "Identitätsschlüssel"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr ""
+"Wenn das Häkchen gesetzt ist, erfordern neue Nachrichten Ihre Aufmerksamkeit."
+
+#. 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 ""
+"Wenn das Häkchen gesetzt ist, gibt es einige Nachrichten mit einem "
+"Übertragungsfehler."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+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 "Ist Abonnent"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_channel
+msgid "Job Channels"
+msgstr "Job-Kanäle"
+
+#. 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 "Job-Funktion"
+
+#. 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 "Job-Funktionen"
+
+#. 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 "Job-Warteschlange"
+
+#. module: queue_job
+#: model:res.groups,name:queue_job.group_queue_job_manager
+msgid "Job Queue Manager"
+msgstr "Job-Warteschlangenverwalter"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
+msgid "Job Serialized"
+msgstr "Job angeordnet"
+
+#. module: queue_job
+#: model:mail.message.subtype,name:queue_job.mt_job_failed
+msgid "Job failed"
+msgstr "Job ist fehlgeschlagen"
+
+#. 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"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+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_queue_job_channel__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
+msgid "Last Updated by"
+msgstr "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_job_lock__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
+msgid "Last Updated on"
+msgstr "Zuletzt aktualisiert am"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Manually set to done by {}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
+msgid "Max. retries"
+msgstr "max. Anzahl von Wiederholungen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
+msgid "Message Delivery error"
+msgstr "Nachrichtenübertragungsfehler"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
+msgid "Messages"
+msgstr "Nachrichten"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
+msgid "Method"
+msgstr "Methode"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
+msgid "Method Name"
+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
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Model {} not found"
+msgstr "Modell {} nicht gefunden"
+
+#. 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 "Bezeichnung"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr "Fälligkeit der nächsten Aktivität"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
+msgid "Next Activity Summary"
+msgstr "Zusammenfassung der nächsten Aktivität"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
+msgid "Next Activity Type"
+msgstr "Typ der nächsten Aktivität"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "No action available for this job"
+msgstr "Für diesen Job ist keine Aktion verfügbar"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+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 "Anzahl der Aktionen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of errors"
+msgstr "Anzahl der Fehler"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of messages with delivery error"
+msgstr "Das ist die Anzahl von Nachrichten mit Übermittlungsfehler"
+
+#. 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
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Parent channel required."
+msgstr "Es ist ein übergeordneter Kanal notwendig."
+
+#. 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 "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"
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr "Warteschlange"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
+msgid "Queue Job"
+msgstr "Job einreihen"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_lock
+msgid "Queue Job Lock"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
+#, fuzzy
+msgid "Record(s)"
+msgstr "Datensatz"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Related"
+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 Action"
+msgstr "Zugehöriger Datensatz"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
+msgid "Related Action (serialized)"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Record"
+msgstr "Zugehöriger Datensatz"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+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"
+msgstr "Entfernungsintervall"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue"
+msgstr "Erneut einreihen"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr "Job erneut einreihen"
+
+#. 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 "Jobs erneut einreihen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
+msgid "Responsible User"
+msgstr "Verantwortlicher Benutzer"
+
+#. 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 "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"
+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 "Alle ausgewählten Jobs als Erledigt markieren"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr "Jobs als Erledigt markieren"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
+msgid "Set jobs to done"
+msgstr "Jobs als Erledigt markieren"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr "Als Erledigt markieren"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+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
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid ""
+"Something bad happened during the execution of the job. More details in the "
+"'Exception Information' section."
+msgstr ""
+"Bei der Ausführung des Jobs ist etwas Ungewöhnliches passiert. Beachten Sie "
+"die Details im Abschnitt \"Exception-Information\"."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
+msgid "Start Date"
+msgstr "Gestartet am"
+
+#. 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 "Gestartet"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "State"
+msgstr "Status"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
+msgid ""
+"Status based on activities\n"
+"Overdue: Due date is already passed\n"
+"Today: Activity date is today\n"
+"Planned: Future activities."
+msgstr ""
+"Der Status hängt von den Aktivitäten ab.\n"
+"Überfällig: Das Fälligkeitsdatum der Aktivität ist überschritten.\n"
+"Heute: Die Aktivität findet heute statt.\n"
+"Geplant: Die Aktivitäten findet in der Zukunft statt."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
+msgid "Task"
+msgstr "Aufgabe"
+
+#. 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 ""
+"Der Job wird fehlschlagen, wenn die Anzahl der Versuche gleich der maximalen "
+"Anzahl der Wiederholungen ist.\n"
+"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."
+msgstr "Die ausgewählten Jobs werden erneut eingereiht."
+
+#. 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 "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."
+msgstr "Typ der Ausnahmeaktivität im Datensatz."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
+msgid "UUID"
+msgstr "UUID"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Related Action for {}.\n"
+"Example of valid format:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Retry Pattern for {}.\n"
+"Example of valid formats:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
+msgid "User ID"
+msgstr "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,name:queue_job.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr "Assistent zur erneuten Einreihung einer Job-Auswahl"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
+msgid "Worker Pid"
+msgstr ""
+
+#~ msgid "If both parameters are 0, ALL jobs will be requeued!"
+#~ msgstr "Wenn beide Parameter 0 sind, werden ALLE Jobs neu eingereiht!"
+
+#~ msgid "SMS Delivery error"
+#~ msgstr "Fehler bei der SMS Nachrichtenübermittlung"
+
+#, python-format
+#~ msgid "Job interrupted and set to Done: nothing to do."
+#~ msgstr "Job unterbrochen und als Erledigt markiert: Es ist nicht zu tun."
+
+#, python-format
+#~ msgid "Manually set to done by %s"
+#~ msgstr "Manuell als erledigt markiert von: %s"
+
+#~ msgid "Record"
+#~ msgstr "Datensatz"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Zuletzt geändert am"
+
+#~ msgid "Main Attachment"
+#~ msgstr "Haupt-Anhang"
+
+#~ msgid "Number of messages which requires an action"
+#~ msgstr "Das ist die Anzahl von Nachrichten, die eine Aktion benötigen"
+
+#~ msgid ""
+#~ " If the max. retries is 0, the number "
+#~ "of retries is infinite."
+#~ msgstr ""
+#~ "Wenn die maximale Anzahl der "
+#~ "Wiederholung auf 0 gesetzt ist, wird dies als unendlich interpretiert."
+#~ "span>"
+
+#~ msgid "Override Channel"
+#~ msgstr "Kanal überschreiben"
+
+#~ msgid "Number of unread messages"
+#~ msgstr "Das ist die Anzahl von ungelesenen Nachrichten"
diff --git a/odex30_base/queue_job/i18n/es.po b/odex30_base/queue_job/i18n/es.po
new file mode 100644
index 0000000..0552dd2
--- /dev/null
+++ b/odex30_base/queue_job/i18n/es.po
@@ -0,0 +1,996 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 15.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2023-09-20 21:07+0000\n"
+"Last-Translator: Ivorra78 \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"
+"X-Generator: Weblate 4.17\n"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid ""
+" \n"
+" If the max. "
+"retries is 0, the number of retries is infinite."
+msgstr ""
+" \n"
+" Si el máx. "
+"reintentos es 0, el número de reintentos es infinito."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/controllers/main.py:0
+msgid "Access Denied"
+msgstr "Acceso denegado"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
+msgid "Action Needed"
+msgstr "Acción requerida"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
+msgid "Activities"
+msgstr "Actividades"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
+msgid "Activity Exception Decoration"
+msgstr "Decoración de Actividad de Excepción"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
+msgid "Activity State"
+msgstr "Estado de la actividad"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
+msgid "Activity Type Icon"
+msgstr "Icono de tipo de actividad"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
+msgid "Args"
+msgstr "Argumentos"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
+msgid "Attachment Count"
+msgstr "Nº de archivos adjuntos"
+
+#. module: queue_job
+#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server
+msgid "AutoVacuum Job Queue"
+msgstr "Vaciado automático de la cola de trabajos"
+
+#. 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 "Cancelar"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
+msgid "Cancel all selected jobs"
+msgstr "Cancelar todos los trabajos seleccionados"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Cancel job"
+msgstr "Cancelar trabajo"
+
+#. 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 "Cancelar trabajos"
+
+#. 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 "Cancelada"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Cancelled by {}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot change the root channel"
+msgstr "No se puede cambiar el canal raíz"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot remove the root channel"
+msgstr "No se puede eliminar el canal raíz"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Channel"
+msgstr "Canal"
+
+#. module: queue_job
+#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
+msgid "Channel complete name must be unique"
+msgstr "El nombre completo del canal debe ser único"
+
+#. 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 "Canales"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
+msgid "Company"
+msgstr "Empresa"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
+msgid "Complete Method Name"
+msgstr "Nombre completo del método"
+
+#. 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 "Nombre completo"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
+msgid "Created Date"
+msgstr "Fecha de creación"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
+msgid "Created by"
+msgstr "Creado por"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Created date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
+msgid "Created on"
+msgstr "Creado el"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
+msgid "Current try"
+msgstr "Intento actual"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr "Intento actual / reintentos máx"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
+msgid "Date Cancelled"
+msgstr "Fecha de cancelación"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
+msgid "Date Done"
+msgstr "Fecha de realización"
+
+#. 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 "Dependencias"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
+msgid "Dependency Graph"
+msgstr "Gráfico de dependencias"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
+msgid "Description"
+msgstr "Descripción"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
+msgid "Display Name"
+msgstr "Nombre mostrado"
+
+#. 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 "Hecho"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr "Hora en que se puso en cola"
+
+#. 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 "En la cola"
+
+#. 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 "Excepción"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
+msgid "Exception Info"
+msgstr "Información de la excepción"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception Information"
+msgstr "Información de la excepción"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
+msgid "Exception Message"
+msgstr "Mensaje de la excepción"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception message"
+msgstr "Mensaje de la excepción"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception:"
+msgstr "Excepción:"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
+msgid "Execute only after"
+msgstr "Ejecutar solo después de"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
+msgid "Execution Time (avg)"
+msgstr "Duración media de ejecución"
+
+#. 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 "Fallido"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
+msgid "Field Type"
+msgstr "Tipo de campo"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_ir_model_fields
+msgid "Fields"
+msgstr "Campos"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
+msgid "Followers"
+msgstr "Seguidores"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
+msgid "Followers (Partners)"
+msgstr "Seguidores (Socios)"
+
+#. 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 "Icono de Font Awesome ej. fa-tasks"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Graph"
+msgstr "Gráfico"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Graph Jobs"
+msgstr "Gráfico de trabajos"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
+msgid "Graph Jobs Count"
+msgstr "Nº de gráfico de trabajos"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
+msgid "Graph UUID"
+msgstr "UUID del Gráfico"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Group By"
+msgstr "Agrupar por"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
+msgid "Has Message"
+msgstr "Tiene un mensaje"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
+msgid "ID"
+msgstr "ID"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon"
+msgstr "Icono"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr "Icono para indicar una actividad de excepción."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr "Clave identificadora"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr ""
+"Si se encuentra seleccionado, hay nuevos mensajes que requieren tu atención."
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
+msgid "If checked, some messages have a delivery error."
+msgstr "Si se encuentra seleccionado, algunos mensajes tienen error de envío."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Invalid job function: {}"
+msgstr "Función del trabajo no válida: {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
+msgid "Is Follower"
+msgstr "Es un seguidor"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_channel
+msgid "Job Channels"
+msgstr "Canales de trabajos"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Job Function"
+msgstr "Función del trabajo"
+
+#. 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 "Funciones de los trabajos"
+
+#. 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 "Cola de trabajos"
+
+#. module: queue_job
+#: model:res.groups,name:queue_job.group_queue_job_manager
+msgid "Job Queue Manager"
+msgstr "Gestor de la cola de trabajos"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
+msgid "Job Serialized"
+msgstr "Trabajo en serie"
+
+#. module: queue_job
+#: model:mail.message.subtype,name:queue_job.mt_job_failed
+msgid "Job failed"
+msgstr "Trabajo fallido"
+
+#. 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 "Trabajos"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Jobs for graph %s"
+msgstr "Trabajos para gráfico %s"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
+msgid "Kwargs"
+msgstr "Kwargs"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 24 hours"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 30 days"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 7 days"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
+msgid "Last Updated by"
+msgstr "Última actualización por"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
+msgid "Last Updated on"
+msgstr "Última actualización el"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Manually set to done by {}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
+msgid "Max. retries"
+msgstr "Reintentos máx"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
+msgid "Message Delivery error"
+msgstr "Error de Envío de Mensaje"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
+msgid "Messages"
+msgstr "Mensajes"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
+msgid "Method"
+msgstr "Método"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
+msgid "Method Name"
+msgstr "Nombre del método"
+
+#. 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 "Modelo"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Model {} not found"
+msgstr "No se ha encontrado el modelo {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr "Mi fecha límite de actividad"
+
+#. 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 "Nombre"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr "Fecha límite de siguiente actividad"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
+msgid "Next Activity Summary"
+msgstr "Resumen de la siguiente actividad"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
+msgid "Next Activity Type"
+msgstr "Siguiente tipo de actividad"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "No action available for this job"
+msgstr "No hay ninguna acción disponible para este trabajo"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Not allowed to change field(s): {}"
+msgstr "No se permite cambiar los campos: {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of Actions"
+msgstr "Número de acciones"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of errors"
+msgstr "Numero de errores"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr "Número de mensajes que requieren una acción"
+
+#. 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 "Número de mensajes con error de envío"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
+msgid "Parent Channel"
+msgstr "Canal padre"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Parent channel required."
+msgstr "Se requiere un canal padre."
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
+msgid ""
+"Pattern expressing from the count of retries on retryable errors, the number "
+"of of seconds to postpone the next execution. Setting the number of seconds "
+"to a 2-element tuple or list will randomize the retry interval between the 2 "
+"values.\n"
+"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"See the module description for details."
+msgstr ""
+"Patrón que expresa cuántos segundos se pospondrá la próxima ejecución, "
+"basado en el número de reintentos de los errores reintentables. Si se usa "
+"una tupla o lista de 2 elementos para expresar el número de segundos, se "
+"escogerá un número aleatorio entre ambos valores.\n"
+"Ejemplo: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Ejemplo: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"Vea la descripción del módulo para más detalles."
+
+#. 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 "Pendiente"
+
+#. 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 "Prioridad"
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr "Cola"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
+msgid "Queue Job"
+msgstr "Cola de trabajos"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_lock
+msgid "Queue Job Lock"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
+msgid "Record(s)"
+msgstr "Registro(s)"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Related"
+msgstr "Relacionado"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
+msgid "Related Action"
+msgstr "Acción relacionada"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
+msgid "Related Action (serialized)"
+msgstr "Acción relacionada (en serie)"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Record"
+msgstr "Registro relacionado"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Records"
+msgstr "Registros relacionados"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
+msgid "Remaining days to execute"
+msgstr "Días restantes para ejecutar"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
+msgid "Removal Interval"
+msgstr "Intervalo de eliminación"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue"
+msgstr "Volver a poner en la cola"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr "Volver a poner el trabajo en la cola"
+
+#. 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 "Volver a poner los trabajos en la cola"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
+msgid "Responsible User"
+msgstr "Usuario responsable"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Result"
+msgstr "Resultado"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Results"
+msgstr "Resultados"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
+msgid "Retry Pattern"
+msgstr "Patrón de reintentos"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
+msgid "Retry Pattern (serialized)"
+msgstr "Patrón de reintentos (en serie)"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_done
+msgid "Set all selected jobs to done"
+msgstr "Marcar como hechos todos los trabajos seleccionados"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr "Marcar trabajos como hechos"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
+msgid "Set jobs to done"
+msgstr "Marcar trabajos como hechos"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr "Marcar como 'Hecho'"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set to done"
+msgstr "Marcar como hecho"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
+msgid "Single shared identifier of a Graph. Empty for a single job."
+msgstr ""
+"Identificador único compartido de un gráfico. Vacío para un solo trabajo."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid ""
+"Something bad happened during the execution of the job. More details in the "
+"'Exception Information' section."
+msgstr ""
+"Algo malo pasó durante la ejecución del trabajo. Más detalles en la sección "
+"'Información de la excepción'."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
+msgid "Start Date"
+msgstr "Fecha de inicio"
+
+#. 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 "Iniciado"
+
+#. 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 "Estado"
+
+#. 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 ""
+"Estado basado en actividades\n"
+"Vencida: la fecha tope ya ha pasado\n"
+"Hoy: La fecha tope es hoy\n"
+"Planificada: futuras actividades."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
+msgid "Task"
+msgstr "Tarea"
+
+#. 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 ""
+"Acción cuando se usa el botón *Acción relacionada* en un trabajo. La acción "
+"por defecto es abrir la vista del registro relacionado con el trabajo. Se "
+"configura como un diccionario con estas claves opcionales: enable, "
+"func_name, kwargs.\n"
+"Vea la descripción del módulo para más detalles."
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
+msgid ""
+"The job will fail if the number of tries reach the max. retries.\n"
+"Retries are infinite when empty."
+msgstr ""
+"El trabajo fallará si alcanza el máx. de re intentos.\n"
+"Los reintentos son infinitos si se deja vacío."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "The selected jobs will be cancelled."
+msgstr "Los trabajos seleccionados serán cancelados."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "The selected jobs will be requeued."
+msgstr "Los trabajos seleccionados volverán a ponerse en la cola."
+
+#. 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 "Los trabajos seleccionados se marcarán como hechos."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr "Tiempo (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 ""
+"Tiempo requerido para ejecutar este trabajo en segundos. Promedio cuando se "
+"agrupa."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr "Tipo de actividad de excepción registrada."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
+msgid "UUID"
+msgstr "UUID"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Related Action for {}.\n"
+"Example of valid format:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+msgstr ""
+"Formato inesperado en la acción relacionada con {}.\n"
+"Ejemplo de un formato válido:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Retry Pattern for {}.\n"
+"Example of valid formats:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
+msgid "User ID"
+msgstr "ID de usuario"
+
+#. 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 "Esperando dependencias"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr "Asistente para volver a poner en cola una selección de trabajos"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
+msgid "Worker Pid"
+msgstr "Pid del trabajador"
+
+#~ msgid "If both parameters are 0, ALL jobs will be requeued!"
+#~ msgstr ""
+#~ "Si ambos parámetros son 0, ¡TODOS los trabajos se volverán a poner en la "
+#~ "cola!"
+
+#~ msgid "Jobs Garbage Collector"
+#~ msgstr "Recolector de basura de trabajos"
+
+#~ msgid "Queue jobs must be created by calling 'with_delay()'."
+#~ msgstr "Los trabajos en cola deben crearse llamando a 'with_delay()'."
+
+#~ msgid "SMS Delivery error"
+#~ msgstr "Error de entrega del SMS"
+
+#, python-format
+#~ msgid "Cancelled by %s"
+#~ msgstr "Cancelado por %s"
+
+#, python-format
+#~ msgid "Job interrupted and set to Done: nothing to do."
+#~ msgstr "Trabajo interrumpido y marcado como hecho: nada que hacer."
+
+#, python-format
+#~ msgid "Manually set to done by %s"
+#~ msgstr "Marcado como hecho a mano por %s"
+
+#~ msgid "Record"
+#~ msgstr "Registro"
+
+#, python-format
+#~ msgid ""
+#~ "Unexpected format of Retry Pattern for {}.\n"
+#~ "Example of valid format:\n"
+#~ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+#~ msgstr ""
+#~ "Formato inesperado en el patrón de reintentos de {}.\n"
+#~ "Ejemplo de un formato válido:\n"
+#~ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Última modificación el"
+
+#~ msgid "Main Attachment"
+#~ msgstr "Adjuntos principales"
+
+#~ msgid "Number of messages which requires an action"
+#~ msgstr "Número de mensajes que requieren una acción"
+
+#~ msgid ""
+#~ " If the max. retries is 0, the number "
+#~ "of retries is infinite."
+#~ msgstr ""
+#~ " Si los reintentos máximos son 0, el "
+#~ "número de reintentos es infinito."
+
+#~ msgid "Override Channel"
+#~ msgstr "Sobreescribir canal"
+
+#~ msgid "Number of unread messages"
+#~ msgstr "Número de mensajes no leidos"
diff --git a/odex30_base/queue_job/i18n/it.po b/odex30_base/queue_job/i18n/it.po
new file mode 100644
index 0000000..3fa0bb1
--- /dev/null
+++ b/odex30_base/queue_job/i18n/it.po
@@ -0,0 +1,973 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 17.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2025-09-18 15:42+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 ""
+" \n"
+" If the max. "
+"retries is 0, the number of retries is infinite."
+msgstr ""
+" \n"
+" Se il massimo "
+"di tentativi è 0, il numero di tentativi è infinito."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/controllers/main.py:0
+msgid "Access Denied"
+msgstr "Accesso negato"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
+msgid "Action Needed"
+msgstr "Azione richiesta"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
+msgid "Activities"
+msgstr "Attività"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
+msgid "Activity Exception Decoration"
+msgstr "Decorazione eccezione attività"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
+msgid "Activity State"
+msgstr "Stato attività"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
+msgid "Activity Type Icon"
+msgstr "Icona tipo attività"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
+msgid "Args"
+msgstr "Argomenti"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
+msgid "Attachment Count"
+msgstr "Conteggio allegati"
+
+#. module: queue_job
+#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server
+msgid "AutoVacuum Job Queue"
+msgstr "Auto pulizia coda lavoro"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_base
+msgid "Base"
+msgstr "Base"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Cancel"
+msgstr "Annulla"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
+msgid "Cancel all selected jobs"
+msgstr "Annulla tutti i lavori selezionati"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Cancel job"
+msgstr "Annulla lavoro"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "Cancel jobs"
+msgstr "Annulla lavori"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Cancelled"
+msgstr "Annullata"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Cancelled by {}"
+msgstr "Annullato da {}"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot change the root channel"
+msgstr "Non si può cambiare il canale radice"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot remove the root channel"
+msgstr "Non si può rimuovere il canale radice"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Channel"
+msgstr "Canale"
+
+#. module: queue_job
+#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
+msgid "Channel complete name must be unique"
+msgstr "Il nome completo del canale deve essere univoco"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
+msgid "Channels"
+msgstr "Canali"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
+msgid "Company"
+msgstr "Azienda"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
+msgid "Complete Method Name"
+msgstr "Nome completo metodo"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
+msgid "Complete Name"
+msgstr "Nome completo"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
+msgid "Created Date"
+msgstr "Data creazione"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
+msgid "Created by"
+msgstr "Creato da"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Created date"
+msgstr "Data creazione"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
+msgid "Created on"
+msgstr "Creato il"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
+msgid "Current try"
+msgstr "Tentativo attuale"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr "Tentativo attuale / massimo tentativi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
+msgid "Date Cancelled"
+msgstr "Data annullamento"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
+msgid "Date Done"
+msgstr "Data completamento"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Dependencies"
+msgstr "Dipendenze"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
+msgid "Dependency Graph"
+msgstr "Grafico dipendenza"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
+msgid "Description"
+msgstr "Descrizione"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
+msgid "Display Name"
+msgstr "Nome visualizzato"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Done"
+msgstr "Completata"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr "Ora accodamento"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Enqueued"
+msgstr "In coda"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception"
+msgstr "Eccezione"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
+msgid "Exception Info"
+msgstr "Informazioni eccezione"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception Information"
+msgstr "Informazioni eccezione"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
+msgid "Exception Message"
+msgstr "Messaggio eccezione"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception message"
+msgstr "Messaggio eccezione"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception:"
+msgstr "Eccezione:"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
+msgid "Execute only after"
+msgstr "Eseguire solo dopo"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
+msgid "Execution Time (avg)"
+msgstr "Tempo esecuzione (medio)"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Failed"
+msgstr "Fallito"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
+msgid "Field Type"
+msgstr "Tipo campo"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_ir_model_fields
+msgid "Fields"
+msgstr "Campi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
+msgid "Followers"
+msgstr "Seguito da"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
+msgid "Followers (Partners)"
+msgstr "Seguito da (partner)"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr "Icona Font Awesome es. fa-tasks"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Graph"
+msgstr "Grafico"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Graph Jobs"
+msgstr "Grafico lavori"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
+msgid "Graph Jobs Count"
+msgstr "Grafico conteggio lavori"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
+msgid "Graph UUID"
+msgstr "Grafico UUID"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Group By"
+msgstr "Raggruppa per"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
+msgid "Has Message"
+msgstr "Ha un messaggio"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
+msgid "ID"
+msgstr "ID"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon"
+msgstr "Icona"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr "Icona per indicare un'attività eccezione."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr "Chiave identità"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr "Se selezionata, nuovi messaggi richiedono attenzione."
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
+msgid "If checked, some messages have a delivery error."
+msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Invalid job function: {}"
+msgstr "Funzione lavoro non valida: {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
+msgid "Is Follower"
+msgstr "Segue"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_channel
+msgid "Job Channels"
+msgstr "Canali lavoro"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Job Function"
+msgstr "Funzione lavoro"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job_function
+#: model:ir.model,name:queue_job.model_queue_job_function
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_function
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+msgid "Job Functions"
+msgstr "Funzioni lavoro"
+
+#. module: queue_job
+#: model:ir.module.category,name:queue_job.module_category_queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_root
+msgid "Job Queue"
+msgstr "Coda lavoro"
+
+#. module: queue_job
+#: model:res.groups,name:queue_job.group_queue_job_manager
+msgid "Job Queue Manager"
+msgstr "Gestore coda lavoro"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
+msgid "Job Serialized"
+msgstr "Lavoro serializzato"
+
+#. module: queue_job
+#: model:mail.message.subtype,name:queue_job.mt_job_failed
+msgid "Job failed"
+msgstr "Lavro fallito"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
+#: model:ir.ui.menu,name:queue_job.menu_queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Jobs"
+msgstr "Lavori"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Jobs for graph %s"
+msgstr "Lavori per grafico %s"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
+msgid "Kwargs"
+msgstr "Kwargs"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 24 hours"
+msgstr "Ultime 24 ore"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 30 days"
+msgstr "Ultimi 30 giorni"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 7 days"
+msgstr "Ultimi 7 giorni"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
+msgid "Last Updated by"
+msgstr "Ultimo aggiornamento di"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
+msgid "Last Updated on"
+msgstr "Ultimo aggiornamento il"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Manually set to done by {}"
+msgstr "Impostato manualmente a fatto da {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
+msgid "Max. retries"
+msgstr "Massimo tentativi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
+msgid "Message Delivery error"
+msgstr "Errore di consegna messaggio"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
+msgid "Messages"
+msgstr "Messaggi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
+msgid "Method"
+msgstr "Metodo"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
+msgid "Method Name"
+msgstr "Nome metodo"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Model"
+msgstr "Modello"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Model {} not found"
+msgstr "Modello {} non trovato"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr "Scadenza mia attività"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
+msgid "Name"
+msgstr "Nome"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr "Scadenza prossima attività"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
+msgid "Next Activity Summary"
+msgstr "Riepilogo prossima attività"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
+msgid "Next Activity Type"
+msgstr "Tipo prossima attività"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "No action available for this job"
+msgstr "Nessuna azione disponibile per questo lavoro"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Not allowed to change field(s): {}"
+msgstr "Non autorizzato a modificare i campi: {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of Actions"
+msgstr "Numero di azioni"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of errors"
+msgstr "Numero di errori"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr "Numero di messaggi che richiedono un'azione"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of messages with delivery error"
+msgstr "Numero di messaggi con errore di consegna"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
+msgid "Parent Channel"
+msgstr "Canale padre"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Parent channel required."
+msgstr "Richiesto canale padre."
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
+msgid ""
+"Pattern expressing from the count of retries on retryable errors, the number "
+"of of seconds to postpone the next execution. Setting the number of seconds "
+"to a 2-element tuple or list will randomize the retry interval between the 2 "
+"values.\n"
+"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"See the module description for details."
+msgstr ""
+"Schema derivante dal conteggio dei tentativi degli errori ripetibili, numero "
+"di secondi per ritardare l'esecuzione successiva. Impostando il numero di "
+"secondi ad una tupla di due elementi o un elenco renderà causale "
+"l'intervallo tra i tentativi tra i due valori.\n"
+"Esempio: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Esempio: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"Vedere la descrizione del modulo per i dettagli."
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Pending"
+msgstr "In attesa"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: 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 "Coda"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
+msgid "Queue Job"
+msgstr "Lavoro in coda"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_lock
+msgid "Queue Job Lock"
+msgstr "Blocco coda lavoro"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
+msgid "Record(s)"
+msgstr "Record"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Related"
+msgstr "Collegato"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
+msgid "Related Action"
+msgstr "Azione collegata"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
+msgid "Related Action (serialized)"
+msgstr "Azione collegata (serializzata)"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Record"
+msgstr "Record collegato"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Records"
+msgstr "Record collegati"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
+msgid "Remaining days to execute"
+msgstr "Giorni rimanenti all'esecuzione"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
+msgid "Removal Interval"
+msgstr "Intervallo rimozione"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue"
+msgstr "Rimetti in coda"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr "Riaccoda lavoro"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue Jobs"
+msgstr "Riaccoda lavori"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
+msgid "Responsible User"
+msgstr "Utente responsabile"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Result"
+msgstr "Risultato"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Results"
+msgstr "Risultati"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
+msgid "Retry Pattern"
+msgstr "Riprova schema"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
+msgid "Retry Pattern (serialized)"
+msgstr "Riprova schema (serializzato)"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_done
+msgid "Set all selected jobs to done"
+msgstr "Imposta a completati tutti i lavori selezionati"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr "Imposta i lavori a completato"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
+msgid "Set jobs to done"
+msgstr "Imposta i lavori a completato"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr "Imposta come 'Completato'"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set to done"
+msgstr "Imposta a completato"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
+msgid "Single shared identifier of a Graph. Empty for a single job."
+msgstr ""
+"Singolo identificatore condiviso di un grafico. Vuoto per un lavoro singolo."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid ""
+"Something bad happened during the execution of the job. More details in the "
+"'Exception Information' section."
+msgstr ""
+"Qualcosa è andato male durante l'esecuzione del lavoro. Maggiori dettagli "
+"nella sezione 'informazioni eccezione'."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
+msgid "Start Date"
+msgstr "Data inizio"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Started"
+msgstr "Iniziato"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "State"
+msgstr "Stato"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
+msgid ""
+"Status based on activities\n"
+"Overdue: Due date is already passed\n"
+"Today: Activity date is today\n"
+"Planned: Future activities."
+msgstr ""
+"Stato in base alle attività\n"
+"Scaduto: la data richiesta è trascorsa\n"
+"Oggi: la data attività è oggi\n"
+"Pianificato: attività future."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
+msgid "Task"
+msgstr "Lavoro"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action
+msgid ""
+"The action when the button *Related Action* is used on a job. The default "
+"action is to open the view of the record related to the job. Configured as a "
+"dictionary with optional keys: enable, func_name, kwargs.\n"
+"See the module description for details."
+msgstr ""
+"L'azione quando si usa il pulsante 'Azione collegata' in un lavoro. L'azione "
+"predefinita è di aprire la vista del record collegato al lavoro. Configrata "
+"come dizionari con chiavi opzionali: enable, func_name, kwargs.\n"
+"Vedere la descrizione del modulo per i dettagli."
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
+msgid ""
+"The job will fail if the number of tries reach the max. retries.\n"
+"Retries are infinite when empty."
+msgstr ""
+"Il lavoro fallirà se il numero di tentativi raggiunge il massimo.\n"
+"I tentativi sono infiniti quando vuoto."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "The selected jobs will be cancelled."
+msgstr "I lavori selezionati verranno annullati."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "The selected jobs will be requeued."
+msgstr "I lavori selezionati verranno riaccodati."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "The selected jobs will be set to done."
+msgstr "I lavori selezionati verranno impostati a completato."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr "Ora (e)"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
+msgid "Time required to execute this job in seconds. Average when grouped."
+msgstr ""
+"Tempo in secondi richiesto per eseguire il lavoro. Medio quando raggruppati."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr "Provato molte volte"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr "Tipo di attività eccezione sul record."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
+msgid "UUID"
+msgstr "UUID"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Related Action for {}.\n"
+"Example of valid format:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+msgstr ""
+"Formato inaspettato di azione colegata per {}.\n"
+"Esempio di formato valido:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Retry Pattern for {}.\n"
+"Example of valid formats:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+msgstr ""
+"Formatto inatteso dello schema del nuovo tentativo per {}.\n"
+"Esempio di formati validi:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
+msgid "User ID"
+msgstr "ID utente"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Wait Dependencies"
+msgstr "Attesa dipendenze"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr "Procedura guidata per riaccodare una selezione di lavori"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
+msgid "Worker Pid"
+msgstr "PID worker"
+
+#~ msgid "If both parameters are 0, ALL jobs will be requeued!"
+#~ msgstr "Se entrambi i parametri sono 0, tutti i lavori verranno riaccodati!"
+
+#~ msgid "Jobs Garbage Collector"
+#~ msgstr "Garbage collector lavori"
+
+#~ msgid "Queue jobs must be created by calling 'with_delay()'."
+#~ msgstr "Il lavoro in coda deve essere creato chiamando 'with_delay()'."
+
+#~ msgid "SMS Delivery error"
+#~ msgstr "Errore consegna SMS"
+
+#, python-format
+#~ msgid "Cancelled by %s"
+#~ msgstr "Annullata da %s"
+
+#, python-format
+#~ msgid "Job interrupted and set to Done: nothing to do."
+#~ msgstr "Lavoro interrotto e impostato a completato: nulla da fare."
+
+#, python-format
+#~ msgid "Manually set to done by %s"
+#~ msgstr "Impostato manualmente a completato da %s"
+
+#~ msgid "Record"
+#~ msgstr "Record"
+
+#, python-format
+#~ msgid ""
+#~ "Unexpected format of Retry Pattern for {}.\n"
+#~ "Example of valid format:\n"
+#~ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+#~ msgstr ""
+#~ "Formato inaspettato di schema tentativo per {}.\n"
+#~ "Esempio di formato valido:\n"
+#~ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
diff --git a/odex30_base/queue_job/i18n/nl.po b/odex30_base/queue_job/i18n/nl.po
new file mode 100644
index 0000000..9c5f302
--- /dev/null
+++ b/odex30_base/queue_job/i18n/nl.po
@@ -0,0 +1,902 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: nl\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 ""
+" \n"
+" If the max. "
+"retries is 0, the number of retries is infinite."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/controllers/main.py:0
+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
+msgid "AutoVacuum Job Queue"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_base
+msgid "Base"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Cancel"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
+msgid "Cancel all selected jobs"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Cancel job"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "Cancel jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Cancelled"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Cancelled by {}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot change the root channel"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot remove the root channel"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Channel"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
+msgid "Channel complete name must be unique"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
+msgid "Channels"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
+msgid "Company"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
+msgid "Complete Method Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
+msgid "Complete Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
+msgid "Created Date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Created date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
+msgid "Current try"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
+msgid "Date Cancelled"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
+msgid "Date Done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Dependencies"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
+msgid "Dependency Graph"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
+msgid "Description"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Enqueued"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
+msgid "Exception Info"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception Information"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
+msgid "Exception Message"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception message"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception:"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
+msgid "Execute only after"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
+msgid "Execution Time (avg)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Failed"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
+msgid "Field Type"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_ir_model_fields
+msgid "Fields"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
+msgid "Followers"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
+msgid "Followers (Partners)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Graph"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Graph Jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
+msgid "Graph Jobs Count"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
+msgid "Graph UUID"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Group By"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
+msgid "Has Message"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
+msgid "ID"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
+msgid "If checked, some messages have a delivery error."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+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
+#: model:ir.actions.act_window,name:queue_job.action_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
+#: model:ir.ui.menu,name:queue_job.menu_queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Jobs"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Jobs for graph %s"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
+msgid "Kwargs"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 24 hours"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 30 days"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 7 days"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Manually set to done by {}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
+msgid "Max. retries"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
+msgid "Message Delivery error"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
+msgid "Messages"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
+msgid "Method"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
+msgid "Method Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Model"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Model {} not found"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
+msgid "Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
+msgid "Next Activity Summary"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
+msgid "Next Activity Type"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "No action available for this job"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Not allowed to change field(s): {}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of Actions"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of errors"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of messages with delivery error"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
+msgid "Parent Channel"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Parent channel required."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
+msgid ""
+"Pattern expressing from the count of retries on retryable errors, the number "
+"of of seconds to postpone the next execution. Setting the number of seconds "
+"to a 2-element tuple or list will randomize the retry interval between the 2 "
+"values.\n"
+"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"See the module description for details."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Pending"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Priority"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
+msgid "Queue Job"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_lock
+msgid "Queue Job Lock"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
+msgid "Record(s)"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Related"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
+msgid "Related Action"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
+msgid "Related Action (serialized)"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Record"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Records"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
+msgid "Remaining days to execute"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
+msgid "Removal Interval"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue Jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
+msgid "Responsible User"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Result"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Results"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
+msgid "Retry Pattern"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
+msgid "Retry Pattern (serialized)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_done
+msgid "Set all selected jobs to done"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
+msgid "Set jobs to done"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set to done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
+msgid "Single shared identifier of a Graph. Empty for a single job."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+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
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Related Action for {}.\n"
+"Example of valid format:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Retry Pattern for {}.\n"
+"Example of valid formats:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
+msgid "User ID"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Wait Dependencies"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
+msgid "Worker Pid"
+msgstr ""
diff --git a/odex30_base/queue_job/i18n/nl_NL.po b/odex30_base/queue_job/i18n/nl_NL.po
new file mode 100644
index 0000000..601b18f
--- /dev/null
+++ b/odex30_base/queue_job/i18n/nl_NL.po
@@ -0,0 +1,944 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2025-08-15 15:25+0000\n"
+"Last-Translator: Bosd \n"
+"Language-Team: none\n"
+"Language: nl_NL\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 ""
+" \n"
+" If the max. "
+"retries is 0, the number of retries is infinite."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/controllers/main.py:0
+msgid "Access Denied"
+msgstr "Toegang geweigerd"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
+msgid "Action Needed"
+msgstr "Actie vereist"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
+msgid "Activities"
+msgstr "Activiteiten"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
+msgid "Activity Exception Decoration"
+msgstr "Decoratie activiteituitzondering"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
+msgid "Activity State"
+msgstr "Activiteitsstatus"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
+msgid "Activity Type Icon"
+msgstr "Icoon activiteitstype"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
+msgid "Args"
+msgstr "Args"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
+msgid "Attachment Count"
+msgstr "Aantal bijlagen"
+
+#. module: queue_job
+#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server
+msgid "AutoVacuum Job Queue"
+msgstr "AutoVacuum wachtrijtaken"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_base
+msgid "Base"
+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 "Annuleren"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
+msgid "Cancel all selected jobs"
+msgstr "Alle geselecteerde taken annuleren"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Cancel job"
+msgstr "Taak annuleren"
+
+#. 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 "Taken annuleren"
+
+#. 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 "Geannuleerd"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Cancelled by {}"
+msgstr "Geannuleerd door {}"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot change the root channel"
+msgstr "Het hoofdkanaal kan niet gewijzigd worden"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot remove the root channel"
+msgstr "Het hoofdkanaal kan niet verwijderd worden"
+
+#. 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 "Kanaal"
+
+#. module: queue_job
+#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
+msgid "Channel complete name must be unique"
+msgstr "Volledige naam van het kanaal moet uniek zijn"
+
+#. 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 "Kanalen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
+msgid "Company"
+msgstr "Bedrijf"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
+msgid "Complete Method Name"
+msgstr "Volledige methodenaam"
+
+#. 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 "Volledige naam"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
+msgid "Created Date"
+msgstr "Aanmaakdatum"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
+msgid "Created by"
+msgstr "Aangemaakt door"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Created date"
+msgstr "Aanmaakdatum"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
+msgid "Created on"
+msgstr "Aangemaakt op"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
+msgid "Current try"
+msgstr "Huidige poging"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr "Huidige poging / max. herpogingen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
+msgid "Date Cancelled"
+msgstr "Datum geannuleerd"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
+msgid "Date Done"
+msgstr "Datum voltooid"
+
+#. 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 "Afhankelijkheden"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
+msgid "Dependency Graph"
+msgstr "Afhankelijkheidsgrafiek"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
+msgid "Description"
+msgstr "Omschrijving"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
+msgid "Display Name"
+msgstr "Weergavenaam"
+
+#. 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 "Voltooid"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr "Wachttijd"
+
+#. 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 de wachtrij"
+
+#. 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 "Uitzondering"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
+msgid "Exception Info"
+msgstr "Uitzonderingsinformatie"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception Information"
+msgstr "Uitzonderingsinformatie"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
+msgid "Exception Message"
+msgstr "Uitzonderingsbericht"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception message"
+msgstr "Uitzonderingsbericht"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception:"
+msgstr "Uitzondering:"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
+msgid "Execute only after"
+msgstr "Alleen uitvoeren na"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
+msgid "Execution Time (avg)"
+msgstr "Uitvoeringstijd (gem)"
+
+#. 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 "Mislukt"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
+msgid "Field Type"
+msgstr "Veldtype"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_ir_model_fields
+msgid "Fields"
+msgstr "Velden"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
+msgid "Followers"
+msgstr "Volgers"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
+msgid "Followers (Partners)"
+msgstr "Volgers (contacten)"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr "Font awesome icoon b.v. fa-tasks"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Graph"
+msgstr "Grafiek"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Graph Jobs"
+msgstr "Taakgrafiek"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
+msgid "Graph Jobs Count"
+msgstr "Taakgraaf aantal"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
+msgid "Graph UUID"
+msgstr "Grafiek 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 "Groeperen op"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
+msgid "Has Message"
+msgstr "Heeft bericht"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
+msgid "ID"
+msgstr "ID"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon"
+msgstr "Icoon"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr "Icoon om een activiteit-uitzondering aan te geven."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr "Identiteitssleutel"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr "Indien aangevinkt, vereisen nieuwe berichten uw aandacht."
+
+#. 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 "Indien aangevinkt, hebben sommige berichten een afleverfout."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Invalid job function: {}"
+msgstr "Ongeldige taakfunctie: {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
+msgid "Is Follower"
+msgstr "Is volger"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_channel
+msgid "Job Channels"
+msgstr "Taakkanalen"
+
+#. 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 "Taakfunctie"
+
+#. 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 "Taakfuncties"
+
+#. 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 "Takenwachtrij"
+
+#. module: queue_job
+#: model:res.groups,name:queue_job.group_queue_job_manager
+msgid "Job Queue Manager"
+msgstr "Takenwachtrijbeheerder"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
+msgid "Job Serialized"
+msgstr "Taak geserialiseerd"
+
+#. module: queue_job
+#: model:mail.message.subtype,name:queue_job.mt_job_failed
+msgid "Job failed"
+msgstr "Taak mislukt"
+
+#. 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 "Taken"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Jobs for graph %s"
+msgstr "Taken voor grafiek %s"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
+msgid "Kwargs"
+msgstr "Kwargs"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 24 hours"
+msgstr "Afgelopen 24 uur"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 30 days"
+msgstr "Afgelopen 30 dagen"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 7 days"
+msgstr "Afgelopen 7 dagen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
+msgid "Last Updated by"
+msgstr "Laatst bijgewerkt door"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
+msgid "Last Updated on"
+msgstr "Laatst bijgewerkt op"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Manually set to done by {}"
+msgstr "Handmatig als voltooid ingesteld door {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
+msgid "Max. retries"
+msgstr "Max. herpogingen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
+msgid "Message Delivery error"
+msgstr "Fout bij bezorgen bericht"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
+msgid "Messages"
+msgstr "Berichten"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
+msgid "Method"
+msgstr "Methode"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
+msgid "Method Name"
+msgstr "Methodenaam"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Model"
+msgstr "Model"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Model {} not found"
+msgstr "Model {} niet gevonden"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr "Mijn activiteitstermijn"
+
+#. 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 "Naam"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr "Volgende activiteitstermijn"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
+msgid "Next Activity Summary"
+msgstr "Samenvatting volgende activiteit"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
+msgid "Next Activity Type"
+msgstr "Volgende activiteitstype"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "No action available for this job"
+msgstr "Geen actie beschikbaar voor deze taak"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Not allowed to change field(s): {}"
+msgstr "Niet toegestaan om veld(en) te wijzigen: {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of Actions"
+msgstr "Aantal acties"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of errors"
+msgstr "Aantal fouten"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr "Aantal berichten dat een actie vereist"
+
+#. 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 "Aantal berichten met een afleverfout"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
+msgid "Parent Channel"
+msgstr "Bovenliggend kanaal"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Parent channel required."
+msgstr "Bovenliggend kanaal vereist."
+
+#. 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 ""
+"Patroon dat aangeeft, vanaf het aantal herpogingen bij herhaalbare fouten, "
+"het aantal seconden om de volgende uitvoering uit te stellen. Door het "
+"aantal seconden in te stellen op een 2-elementen tuple of lijst, wordt het "
+"herhaalpunt willekeurig gekozen tussen de 2 waarden.\n"
+"Voorbeeld: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Voorbeeld: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"Zie de modulebeschrijving voor details."
+
+#. 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 afwachting"
+
+#. 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 "Prioriteit"
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr "Wachtrij"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
+msgid "Queue Job"
+msgstr "Wachtrijtaak"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_lock
+msgid "Queue Job Lock"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
+msgid "Record(s)"
+msgstr "Record(s)"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Related"
+msgstr "Gerelateerd"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
+msgid "Related Action"
+msgstr "Gerelateerde actie"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
+msgid "Related Action (serialized)"
+msgstr "Gerelateerde actie (geserialiseerd)"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Record"
+msgstr "Gerelateerd record"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Records"
+msgstr "Gerelateerde records"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
+msgid "Remaining days to execute"
+msgstr "Resterende dagen om uit te voeren"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
+msgid "Removal Interval"
+msgstr "Verwijderingsinterval"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue"
+msgstr "Opnieuw in de wachtrij plaatsen"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr "Taak opnieuw in de wachtrij plaatsen"
+
+#. 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 "Taken opnieuw in de wachtrij plaatsen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
+msgid "Responsible User"
+msgstr "Verantwoordelijke gebruiker"
+
+#. 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 "Resultaat"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Results"
+msgstr "Resultaten"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
+msgid "Retry Pattern"
+msgstr "Herpogingspatroon"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
+msgid "Retry Pattern (serialized)"
+msgstr "Herpogingspatroon (geserialiseerd)"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_done
+msgid "Set all selected jobs to done"
+msgstr "Alle geselecteerde taken op voltooid zetten"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr "Taken op voltooid zetten"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
+msgid "Set jobs to done"
+msgstr "Taken op voltooid zetten"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr "Op 'Voltooid' zetten"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set to done"
+msgstr "Op voltooid zetten"
+
+#. 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 ""
+"Enkele gedeelde identificatie van een grafiek. Leeg voor een enkele taak."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid ""
+"Something bad happened during the execution of the job. More details in the "
+"'Exception Information' section."
+msgstr ""
+"Er is iets misgegaan tijdens de uitvoering van de taak. Meer details in de "
+"sectie 'Uitzonderingsinformatie'."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
+msgid "Start Date"
+msgstr "Startdatum"
+
+#. 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 "Gestart"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "State"
+msgstr "Status"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
+msgid ""
+"Status based on activities\n"
+"Overdue: Due date is already passed\n"
+"Today: Activity date is today\n"
+"Planned: Future activities."
+msgstr ""
+"Status gebaseerd op activiteiten\n"
+"Achterstallig: Vervaldatum is al verstreken\n"
+"Vandaag: Activiteitsdatum is vandaag\n"
+"Gepland: Activiteiten in de toekomst."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
+msgid "Task"
+msgstr "Taak"
+
+#. 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 ""
+"De actie wanneer de knop *Gerelateerde actie* wordt gebruikt op een taak. De "
+"standaardactie is het openen van de weergave van het record dat gerelateerd "
+"is aan de taak. Ingesteld als een woordenboek met optionele sleutels: "
+"enable, func_name, kwargs.\n"
+"Zie de modulebeschrijving voor details."
+
+#. 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 ""
+"De taak zal mislukken als het aantal pogingen het max. aantal herpogingen "
+"bereikt.\n"
+"Herpogingen zijn oneindig wanneer dit veld leeg is."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "The selected jobs will be cancelled."
+msgstr "De geselecteerde taken worden geannuleerd."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "The selected jobs will be requeued."
+msgstr "De geselecteerde taken worden opnieuw in de wachtrij geplaatst."
+
+#. 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 "De geselecteerde taken worden op voltooid gezet."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr "Tijd (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 ""
+"Benodigde tijd om deze taak uit te voeren in seconden. Gemiddeld wanneer "
+"gegroepeerd."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr "Vaak geprobeerd"
+
+#. 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 "Type van de activiteit-uitzondering op het record."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
+msgid "UUID"
+msgstr "UUID"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Related Action for {}.\n"
+"Example of valid format:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+msgstr ""
+"Onverwacht formaat van gerelateerde actie voor {}.\n"
+"Voorbeeld van geldig formaat:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+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 ""
+"Onverwacht formaat van herpogingspatroon voor {}.\n"
+"Voorbeeld van geldige formaten:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
+msgid "User ID"
+msgstr "Gebruikers-ID"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Wait Dependencies"
+msgstr "Wacht op afhankelijkheden"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr "Wizard om een selectie van taken opnieuw in de wachtrij te plaatsen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
+msgid "Worker Pid"
+msgstr "Werknemer PID"
+
+#~ msgid "If both parameters are 0, ALL jobs will be requeued!"
+#~ msgstr ""
+#~ "Als beide parameters 0 zijn, worden ALLE taken opnieuw in de wachtrij "
+#~ "geplaatst!"
+
+#~ msgid "Jobs Garbage Collector"
+#~ msgstr "Takenafvalverzamelaar"
diff --git a/odex30_base/queue_job/i18n/queue_job.pot b/odex30_base/queue_job/i18n/queue_job.pot
new file mode 100644
index 0000000..9637c42
--- /dev/null
+++ b/odex30_base/queue_job/i18n/queue_job.pot
@@ -0,0 +1,894 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid ""
+" \n"
+" If the max. retries is 0, the number of retries is infinite."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/controllers/main.py:0
+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
+msgid "AutoVacuum Job Queue"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_base
+msgid "Base"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Cancel"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
+msgid "Cancel all selected jobs"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Cancel job"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "Cancel jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Cancelled"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Cancelled by {}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot change the root channel"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot remove the root channel"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Channel"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
+msgid "Channel complete name must be unique"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
+msgid "Channels"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
+msgid "Company"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
+msgid "Complete Method Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
+msgid "Complete Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
+msgid "Created Date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Created date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
+msgid "Current try"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
+msgid "Date Cancelled"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
+msgid "Date Done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Dependencies"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
+msgid "Dependency Graph"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
+msgid "Description"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Enqueued"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
+msgid "Exception Info"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception Information"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
+msgid "Exception Message"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception message"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception:"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
+msgid "Execute only after"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
+msgid "Execution Time (avg)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Failed"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
+msgid "Field Type"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_ir_model_fields
+msgid "Fields"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
+msgid "Followers"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
+msgid "Followers (Partners)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Graph"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Graph Jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
+msgid "Graph Jobs Count"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
+msgid "Graph UUID"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Group By"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
+msgid "Has Message"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
+msgid "ID"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
+msgid "If checked, some messages have a delivery error."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+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
+#: model:ir.actions.act_window,name:queue_job.action_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
+#: model:ir.ui.menu,name:queue_job.menu_queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Jobs"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Jobs for graph %s"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
+msgid "Kwargs"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 24 hours"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 30 days"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 7 days"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Manually set to done by {}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
+msgid "Max. retries"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
+msgid "Message Delivery error"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
+msgid "Messages"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
+msgid "Method"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
+msgid "Method Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Model"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Model {} not found"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
+msgid "Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
+msgid "Next Activity Summary"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
+msgid "Next Activity Type"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "No action available for this job"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Not allowed to change field(s): {}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of Actions"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of errors"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of messages with delivery error"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
+msgid "Parent Channel"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Parent channel required."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
+msgid ""
+"Pattern expressing from the count of retries on retryable errors, the number of of seconds to postpone the next execution. Setting the number of seconds to a 2-element tuple or list will randomize the retry interval between the 2 values.\n"
+"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"See the module description for details."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Pending"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Priority"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
+msgid "Queue Job"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_lock
+msgid "Queue Job Lock"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
+msgid "Record(s)"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Related"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
+msgid "Related Action"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
+msgid "Related Action (serialized)"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Record"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Records"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
+msgid "Remaining days to execute"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
+msgid "Removal Interval"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue Jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
+msgid "Responsible User"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Result"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Results"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
+msgid "Retry Pattern"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
+msgid "Retry Pattern (serialized)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_done
+msgid "Set all selected jobs to done"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
+msgid "Set jobs to done"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set to done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
+msgid "Single shared identifier of a Graph. Empty for a single job."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+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
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Related Action for {}.\n"
+"Example of valid format:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs\" {{\"limit\": 10}}}}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Retry Pattern for {}.\n"
+"Example of valid formats:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
+msgid "User ID"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Wait Dependencies"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
+msgid "Worker Pid"
+msgstr ""
diff --git a/odex30_base/queue_job/i18n/tr.po b/odex30_base/queue_job/i18n/tr.po
new file mode 100644
index 0000000..b2b41d9
--- /dev/null
+++ b/odex30_base/queue_job/i18n/tr.po
@@ -0,0 +1,948 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2025-06-16 17:26+0000\n"
+"Last-Translator: Betül Öğmen \n"
+"Language-Team: none\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.10.4\n"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid ""
+" \n"
+" If the max. "
+"retries is 0, the number of retries is infinite."
+msgstr ""
+" \n"
+" Eğer maksimum "
+"yeniden deneme sayısı 0 ise, yeniden deneme sayısı sonsuzdur."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/controllers/main.py:0
+msgid "Access Denied"
+msgstr "Erişim Reddedildi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction
+msgid "Action Needed"
+msgstr "Eylem Gerekli"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_ids
+msgid "Activities"
+msgstr "Aktiviteler"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_decoration
+msgid "Activity Exception Decoration"
+msgstr "Aktivite İstisna Dekorasyonu"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_state
+msgid "Activity State"
+msgstr "Aktivite Durumu"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_icon
+msgid "Activity Type Icon"
+msgstr "Aktivite Türü Simgesi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__args
+msgid "Args"
+msgstr "Argümanlar"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_attachment_count
+msgid "Attachment Count"
+msgstr "Ek Sayısı"
+
+#. module: queue_job
+#: model:ir.actions.server,name:queue_job.ir_cron_autovacuum_queue_jobs_ir_actions_server
+msgid "AutoVacuum Job Queue"
+msgstr "Otomatik Temizleme İş Kuyruğu"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_base
+msgid "Base"
+msgstr "Temel"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Cancel"
+msgstr "İptal"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
+msgid "Cancel all selected jobs"
+msgstr "Tüm seçilen işleri iptal et"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Cancel job"
+msgstr "İşi iptal et"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "Cancel jobs"
+msgstr "İşleri iptal et"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Cancelled"
+msgstr "İptal Edildi"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Cancelled by {}"
+msgstr "İptal eden: {}"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot change the root channel"
+msgstr "Kök kanal değiştirilemez"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot remove the root channel"
+msgstr "Kök kanal kaldırılamaz"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Channel"
+msgstr "Kanal"
+
+#. module: queue_job
+#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
+msgid "Channel complete name must be unique"
+msgstr "Kanal tam adı benzersiz olmalıdır"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
+msgid "Channels"
+msgstr "Kanallar"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
+msgid "Company"
+msgstr "Şirket"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
+msgid "Complete Method Name"
+msgstr "Tam Yöntem Adı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
+msgid "Complete Name"
+msgstr "Tam Ad"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
+msgid "Created Date"
+msgstr "Oluşturulma Tarihi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
+msgid "Created by"
+msgstr "Oluşturan"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Created date"
+msgstr "Oluşturulma tarihi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
+msgid "Created on"
+msgstr "Oluşturulma"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
+msgid "Current try"
+msgstr "Geçerli deneme"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr "Geçerli deneme / max. yeniden deneme"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
+msgid "Date Cancelled"
+msgstr "İptal Tarihi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
+msgid "Date Done"
+msgstr "Tamamlanma Tarihi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Dependencies"
+msgstr "Bağımlılıklar"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
+msgid "Dependency Graph"
+msgstr "Bağımlılık Grafiği"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
+msgid "Description"
+msgstr "Açıklama"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
+msgid "Display Name"
+msgstr "Görünen Ad"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Done"
+msgstr "Tamamlandı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr "Sıraya Alma Zamanı"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Enqueued"
+msgstr "Sıraya Alındı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception"
+msgstr "İstisna"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
+msgid "Exception Info"
+msgstr "İstisna Bilgisi"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception Information"
+msgstr "İstisna Bilgisi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
+msgid "Exception Message"
+msgstr "İstisna Mesajı"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception message"
+msgstr "İstisna mesajı"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception:"
+msgstr "İstisna:"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
+msgid "Execute only after"
+msgstr "Yalnızca şundan sonra çalıştır"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
+msgid "Execution Time (avg)"
+msgstr "Çalışma Süresi (ortalama)"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Failed"
+msgstr "Başarısız"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
+msgid "Field Type"
+msgstr "Alan Türü"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_ir_model_fields
+msgid "Fields"
+msgstr "Alanlar"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
+msgid "Followers"
+msgstr "Takipçiler"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
+msgid "Followers (Partners)"
+msgstr "Takipçiler (Ortaklar)"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Graph"
+msgstr "Grafik"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Graph Jobs"
+msgstr "Grafik İşler"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
+msgid "Graph Jobs Count"
+msgstr "Grafik İşler Sayısı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
+msgid "Graph UUID"
+msgstr "Grafik UUID"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Group By"
+msgstr "Grupla"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
+msgid "Has Message"
+msgstr "Mesaj Var"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
+msgid "ID"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon"
+msgstr "Simge"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr "Bir istisna aktivitesini belirtmek için simge."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr "Kimlik Anahtarı"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr "İşaretlenirse, sizi bekleyen mesajlar var."
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
+msgid "If checked, some messages have a delivery error."
+msgstr "İşaretlenirse, bazı mesajlar teslimat hatası içerir."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Invalid job function: {}"
+msgstr "Geçersiz iş fonksiyonu: {}\""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_is_follower
+msgid "Is Follower"
+msgstr "Takipçi"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_channel
+msgid "Job Channels"
+msgstr "İş Kanalları"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__job_function_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Job Function"
+msgstr "İş Fonksiyonu"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job_function
+#: model:ir.model,name:queue_job.model_queue_job_function
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__job_function_ids
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_function
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+msgid "Job Functions"
+msgstr "İş Fonksiyonları"
+
+#. module: queue_job
+#: model:ir.module.category,name:queue_job.module_category_queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_root
+msgid "Job Queue"
+msgstr "İş Kuyruğu"
+
+#. module: queue_job
+#: model:res.groups,name:queue_job.group_queue_job_manager
+msgid "Job Queue Manager"
+msgstr "İş Kuyruğu Yöneticisi"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__ir_model_fields__ttype__job_serialized
+msgid "Job Serialized"
+msgstr "İş Serileştirilmiş"
+
+#. module: queue_job
+#: model:mail.message.subtype,name:queue_job.mt_job_failed
+msgid "Job failed"
+msgstr "İş başarısız oldu"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
+#: model:ir.ui.menu,name:queue_job.menu_queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Jobs"
+msgstr "İşler"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Jobs for graph %s"
+msgstr "Grafik için işler %s"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
+msgid "Kwargs"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 24 hours"
+msgstr "Son 24 Saat"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 30 days"
+msgstr "Son 30 Gün"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 7 days"
+msgstr "Son 7 Gün"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
+msgid "Last Updated by"
+msgstr "Son Güncelleyen"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
+msgid "Last Updated on"
+msgstr "Son Güncelleme"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Manually set to done by {}"
+msgstr "Manuel olarak {} tarafından tamamlandı olarak ayarlandı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
+msgid "Max. retries"
+msgstr "Maks. yeniden deneme"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
+msgid "Message Delivery error"
+msgstr "Mesaj Teslimat hatası"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
+msgid "Messages"
+msgstr "Mesajlar"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
+msgid "Method"
+msgstr "Yöntem"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
+msgid "Method Name"
+msgstr "Yöntem Adı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Model"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Model {} not found"
+msgstr "Model {} bulunamadı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr "Aktivite Son Tarihim"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
+msgid "Name"
+msgstr "Ad"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr "Sonraki Aktivite Son Tarihi"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
+msgid "Next Activity Summary"
+msgstr "Sonraki Aktivite Özeti"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
+msgid "Next Activity Type"
+msgstr "Sonraki Aktivite Türü"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "No action available for this job"
+msgstr "Bu iş için uygun bir eylem yok"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Not allowed to change field(s): {}"
+msgstr "Alan(lar)ı değiştirme izni yok: {}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of Actions"
+msgstr "Eylem Sayısı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of errors"
+msgstr "Hata sayısı"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr "Eylem gerektiren mesaj sayısı"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of messages with delivery error"
+msgstr "Teslimat hatası içeren mesaj sayısı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
+msgid "Parent Channel"
+msgstr "Üst Kanal"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Parent channel required."
+msgstr "Üst kanal gerekli."
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
+msgid ""
+"Pattern expressing from the count of retries on retryable errors, the number "
+"of of seconds to postpone the next execution. Setting the number of seconds "
+"to a 2-element tuple or list will randomize the retry interval between the 2 "
+"values.\n"
+"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"See the module description for details."
+msgstr ""
+"Yeniden denenebilir hatalarda yeniden deneme sayısından, bir sonraki "
+"çalışmanın ertelenmesi için saniye sayısını ifade eden desen. Saniye "
+"sayısını 2 elemanlı bir demet veya liste olarak ayarlama, yeniden deneme "
+"aralığını 2 değer arasında rastgele hale getirir.\n"
+"Örneğin: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Örneğin: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"Ayrıntılar için modül açıklamasına bakınız."
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Pending"
+msgstr "Beklemede"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Priority"
+msgstr "Öncelik"
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr "Kuyruk"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
+msgid "Queue Job"
+msgstr "Kuyruk İşi"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_lock
+msgid "Queue Job Lock"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
+msgid "Record(s)"
+msgstr "Kayıt(lar)"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Related"
+msgstr "İlgili"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
+msgid "Related Action"
+msgstr "İlgili Eylem"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
+msgid "Related Action (serialized)"
+msgstr "İlgili Eylem (serileştirilmiş)"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Record"
+msgstr "İlgili Kayıt"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Records"
+msgstr "İlgili Kayıtlar"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
+msgid "Remaining days to execute"
+msgstr "Çalıştırılacak kalan günler"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
+msgid "Removal Interval"
+msgstr "Kaldırma Aralığı"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue"
+msgstr "Yeniden sıraya al"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr "İşi yeniden sıraya al"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue Jobs"
+msgstr "İşleri yeniden sıraya al"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
+msgid "Responsible User"
+msgstr "Sorumlu Kullanıcı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Result"
+msgstr "Sonuç"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Results"
+msgstr "Sonuçlar"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
+msgid "Retry Pattern"
+msgstr "Deseni Yeniden Dene"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
+msgid "Retry Pattern (serialized)"
+msgstr "Deseni Yeniden Dene (serileştirilmiş)"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_done
+msgid "Set all selected jobs to done"
+msgstr "Tüm seçilen işleri tamamlandı olarak ayarla"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr "İşleri tamamlandı olarak ayarla"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
+msgid "Set jobs to done"
+msgstr "İşleri tamamlandı olarak ayarla"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr "'Tamamlandı' olarak ayarla"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set to done"
+msgstr "'Tamamlandı' olarak ayarla"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
+msgid "Single shared identifier of a Graph. Empty for a single job."
+msgstr "Bir Grafik için tek paylaşılan tanımlayıcı. Tek bir iş için boş."
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid ""
+"Something bad happened during the execution of the job. More details in the "
+"'Exception Information' section."
+msgstr ""
+"İşinin çalışması sırasında kötü bir şey oldu. 'İstisna Bilgisi' bölümünde "
+"daha fazla ayrıntı bulabilirsiniz."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_started
+msgid "Start Date"
+msgstr "Başlama Tarihi"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__started
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Started"
+msgstr "Başladı"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__state
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "State"
+msgstr "Durum"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_state
+msgid ""
+"Status based on activities\n"
+"Overdue: Due date is already passed\n"
+"Today: Activity date is today\n"
+"Planned: Future activities."
+msgstr ""
+"Aktivitelere dayalı durum\n"
+"Geçikmiş: Son tarih zaten geçti\n"
+"Bugün: Aktivite tarihi bugün\n"
+"Planlandı: Gelecekteki aktiviteler."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
+msgid "Task"
+msgstr "Görev"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action
+msgid ""
+"The action when the button *Related Action* is used on a job. The default "
+"action is to open the view of the record related to the job. Configured as a "
+"dictionary with optional keys: enable, func_name, kwargs.\n"
+"See the module description for details."
+msgstr ""
+"İşteki *İlgili Eylem* düğmesi kullanıldığında gerçekleşen eylem. Varsayılan "
+"eylem, işle ilişkili kaydın görünümünü açmaktır. İsteğe bağlı anahtarlar ile "
+"yapılandırılmış bir sözlük: etkinleştir, func_name, kwargs.\n"
+"Ayrıntılar için modül açıklamasına bakınız."
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
+msgid ""
+"The job will fail if the number of tries reach the max. retries.\n"
+"Retries are infinite when empty."
+msgstr ""
+"Deneme sayısı maksimum yeniden deneme sayısına ulaşırsa iş başarısız olur.\n"
+"Boş olduğunda yeniden denemeler sonsuzdur."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "The selected jobs will be cancelled."
+msgstr "Seçilen işler iptal edilecek."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "The selected jobs will be requeued."
+msgstr "Seçilen işler yeniden sıraya alınacak."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "The selected jobs will be set to done."
+msgstr "Seçilen işler tamamlandı olarak ayarlanacak."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr "Zaman (sn)"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
+msgid "Time required to execute this job in seconds. Average when grouped."
+msgstr ""
+"Bu işi çalıştırmak için gereken süre saniye cinsinden. Gruplandığında "
+"ortalama."
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr "Birçok kez denendi"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr "Kayıttaki istisna aktivitesinin türü."
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
+msgid "UUID"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Related Action for {}.\n"
+"Example of valid format:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+msgstr ""
+"{} için Beklenmeyen İlgili Eylem formatı.\n"
+"Geçerli format örneği:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Retry Pattern for {}.\n"
+"Example of valid formats:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+msgstr ""
+"{} için Beklenmeyen Yeniden Dene Deseni formatı.\n"
+"Geçerli format örneği:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
+msgid "User ID"
+msgstr "Kullanıcı Id'si"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Wait Dependencies"
+msgstr "Bağımlılıkları Bekle"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr "Seçilen işleri yeniden sıraya almak için sihirbaz"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
+msgid "Worker Pid"
+msgstr "Çalışan Pid"
+
+#~ msgid "If both parameters are 0, ALL jobs will be requeued!"
+#~ msgstr "Eğer her iki parametre de 0 ise, TÜM işler yeniden sıraya alınacak!"
+
+#~ msgid "Jobs Garbage Collector"
+#~ msgstr "İşler Çöp Toplayıcı"
+
+#~ msgid "Queue jobs must be created by calling 'with_delay()'."
+#~ msgstr "Kuyruk işleri 'with_delay()' çağrılarak oluşturulmalıdır."
+
+#~ msgid "SMS Delivery error"
+#~ msgstr "SMS Teslim hatası"
diff --git a/odex30_base/queue_job/i18n/tr_TR.po b/odex30_base/queue_job/i18n/tr_TR.po
new file mode 100644
index 0000000..9c520e2
--- /dev/null
+++ b/odex30_base/queue_job/i18n/tr_TR.po
@@ -0,0 +1,902 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: tr_TR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid ""
+" \n"
+" If the max. "
+"retries is 0, the number of retries is infinite."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/controllers/main.py:0
+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
+msgid "AutoVacuum Job Queue"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_base
+msgid "Base"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Cancel"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
+msgid "Cancel all selected jobs"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Cancel job"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "Cancel jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Cancelled"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Cancelled by {}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot change the root channel"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot remove the root channel"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Channel"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
+msgid "Channel complete name must be unique"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
+msgid "Channels"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
+msgid "Company"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
+msgid "Complete Method Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
+msgid "Complete Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
+msgid "Created Date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Created date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
+msgid "Current try"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
+msgid "Date Cancelled"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
+msgid "Date Done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Dependencies"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
+msgid "Dependency Graph"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
+msgid "Description"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Enqueued"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
+msgid "Exception Info"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception Information"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
+msgid "Exception Message"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception message"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception:"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
+msgid "Execute only after"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
+msgid "Execution Time (avg)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Failed"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
+msgid "Field Type"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_ir_model_fields
+msgid "Fields"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
+msgid "Followers"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
+msgid "Followers (Partners)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Graph"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Graph Jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_jobs_count
+msgid "Graph Jobs Count"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__graph_uuid
+msgid "Graph UUID"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Group By"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
+msgid "Has Message"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
+msgid "ID"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
+msgid "If checked, some messages have a delivery error."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+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
+#: model:ir.actions.act_window,name:queue_job.action_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
+#: model:ir.ui.menu,name:queue_job.menu_queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Jobs"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Jobs for graph %s"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
+msgid "Kwargs"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 24 hours"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 30 days"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 7 days"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Manually set to done by {}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
+msgid "Max. retries"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
+msgid "Message Delivery error"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
+msgid "Messages"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
+msgid "Method"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
+msgid "Method Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Model"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Model {} not found"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
+msgid "Name"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
+msgid "Next Activity Summary"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
+msgid "Next Activity Type"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "No action available for this job"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Not allowed to change field(s): {}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of Actions"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of errors"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of messages with delivery error"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
+msgid "Parent Channel"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Parent channel required."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_retry_pattern
+msgid ""
+"Pattern expressing from the count of retries on retryable errors, the number "
+"of of seconds to postpone the next execution. Setting the number of seconds "
+"to a 2-element tuple or list will randomize the retry interval between the 2 "
+"values.\n"
+"Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+"Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+"See the module description for details."
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Pending"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Priority"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
+msgid "Queue Job"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_lock
+msgid "Queue Job Lock"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
+msgid "Record(s)"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Related"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
+msgid "Related Action"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
+msgid "Related Action (serialized)"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Record"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Records"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
+msgid "Remaining days to execute"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
+msgid "Removal Interval"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue Jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
+msgid "Responsible User"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Result"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Results"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
+msgid "Retry Pattern"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
+msgid "Retry Pattern (serialized)"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_done
+msgid "Set all selected jobs to done"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
+msgid "Set jobs to done"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set to done"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
+msgid "Single shared identifier of a Graph. Empty for a single job."
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+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
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Related Action for {}.\n"
+"Example of valid format:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Retry Pattern for {}.\n"
+"Example of valid formats:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
+msgid "User ID"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Wait Dependencies"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
+msgid "Worker Pid"
+msgstr ""
diff --git a/odex30_base/queue_job/i18n/zh_CN.po b/odex30_base/queue_job/i18n/zh_CN.po
new file mode 100644
index 0000000..ef8f925
--- /dev/null
+++ b/odex30_base/queue_job/i18n/zh_CN.po
@@ -0,0 +1,984 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * queue_job
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2024-07-02 09:47+0000\n"
+"Last-Translator: xtanuiha \n"
+"Language-Team: none\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Weblate 4.17\n"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid ""
+" \n"
+" If the max. "
+"retries is 0, the number of retries is infinite."
+msgstr ""
+" \n"
+" 如果最大重试次"
+"数设置为0,则表示重试次数是无限的。"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/controllers/main.py:0
+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
+msgid "AutoVacuum Job Queue"
+msgstr "自动清空作业队列"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_base
+msgid "Base"
+msgstr "基础"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Cancel"
+msgstr "取消"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_cancelled
+msgid "Cancel all selected jobs"
+msgstr "取消所有选中作业"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Cancel job"
+msgstr "取消作业"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "Cancel jobs"
+msgstr "取消作业"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__cancelled
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Cancelled"
+msgstr "已取消"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Cancelled by {}"
+msgstr ""
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot change the root channel"
+msgstr "无法更改root频道"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+msgid "Cannot remove the root channel"
+msgstr "无法删除root频道"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_function_search
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Channel"
+msgstr "频道"
+
+#. module: queue_job
+#: model:ir.model.constraint,message:queue_job.constraint_queue_job_channel_name_uniq
+msgid "Channel complete name must be unique"
+msgstr "频道完整名称必须是唯一的"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_queue_job_channel
+#: model:ir.ui.menu,name:queue_job.menu_queue_job_channel
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_channel_search
+msgid "Channels"
+msgstr "频道"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__company_id
+msgid "Company"
+msgstr "公司"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__channel_method_name
+msgid "Complete Method Name"
+msgstr "完整方法名"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__complete_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__channel
+msgid "Complete Name"
+msgstr "完整名称"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_created
+msgid "Created Date"
+msgstr "创建日期"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_uid
+msgid "Created by"
+msgstr "创建者"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Created date"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__create_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__create_date
+msgid "Created on"
+msgstr "创建时间"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__retry
+msgid "Current try"
+msgstr "当前尝试"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr "当前尝试/最大重试次数"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_cancelled
+msgid "Date Cancelled"
+msgstr "取消日期"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_done
+msgid "Date Done"
+msgstr "完成日期"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Dependencies"
+msgstr "依赖项"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__dependency_graph
+msgid "Dependency Graph"
+msgstr "依赖关系图"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__name
+msgid "Description"
+msgstr "说明"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__display_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__display_name
+msgid "Display Name"
+msgstr "显示名称"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__done
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Done"
+msgstr "完成"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__date_enqueued
+msgid "Enqueue Time"
+msgstr "排队时间"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__enqueued
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Enqueued"
+msgstr "排队"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_name
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception"
+msgstr "异常"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_info
+msgid "Exception Info"
+msgstr "异常信息"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception Information"
+msgstr "异常信息"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exc_message
+msgid "Exception Message"
+msgstr "异常信息"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Exception message"
+msgstr "异常信息"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Exception:"
+msgstr "异常:"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__eta
+msgid "Execute only after"
+msgstr "仅在此之后执行"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__exec_time
+msgid "Execution Time (avg)"
+msgstr "平均执行时间"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__failed
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Failed"
+msgstr "失败"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_ir_model_fields__ttype
+msgid "Field Type"
+msgstr "字段类型"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_ir_model_fields
+msgid "Fields"
+msgstr "字段"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_follower_ids
+msgid "Followers"
+msgstr "关注者"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_partner_ids
+msgid "Followers (Partners)"
+msgstr "关注者(业务伙伴)"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr "Font Awesome 图标,例如:fa-tasks"
+
+#. 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 "图表唯一标识符(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 "分组"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__has_message
+msgid "Has Message"
+msgstr "有消息"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__id
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__id
+msgid "ID"
+msgstr "ID"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon"
+msgstr "图标"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr "指示异常活动的图标。"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__identity_key
+msgid "Identity Key"
+msgstr "身份密钥"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr "确认后, 出现提示消息。"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error
+msgid "If checked, some messages have a delivery error."
+msgstr "如果勾选此项, 某些消息将会产生传递错误。"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+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
+#: model:ir.actions.act_window,name:queue_job.action_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__job_ids
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__job_ids
+#: model:ir.ui.menu,name:queue_job.menu_queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_graph
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_pivot
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Jobs"
+msgstr "作业"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Jobs for graph %s"
+msgstr "图表 %s 的作业"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__kwargs
+msgid "Kwargs"
+msgstr "关键字参数"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 24 hours"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 30 days"
+msgstr ""
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Last 7 days"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_uid
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_uid
+msgid "Last Updated by"
+msgstr "最后更新者"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_cancelled__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_jobs_to_done__write_date
+#: model:ir.model.fields,field_description:queue_job.field_queue_requeue_job__write_date
+msgid "Last Updated on"
+msgstr "最后更新时间"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Manually set to done by {}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__max_retries
+msgid "Max. retries"
+msgstr "最大重试次数"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error
+msgid "Message Delivery error"
+msgstr "消息递送错误"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_ids
+msgid "Messages"
+msgstr "消息"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__method
+msgid "Method"
+msgstr "方法名称"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__method_name
+msgid "Method Name"
+msgstr "方法名称"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__model_name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__model_id
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Model"
+msgstr "模型"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid "Model {} not found"
+msgstr "模型 {} 未找到"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr "我的活动截止日期"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__name
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__name
+msgid "Name"
+msgstr "名称"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr "下一活动截止日期"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_summary
+msgid "Next Activity Summary"
+msgstr "下一活动摘要"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_type_id
+msgid "Next Activity Type"
+msgstr "下一活动类型"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "No action available for this job"
+msgstr "此作业无法执行任何操作"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Not allowed to change field(s): {}"
+msgstr "不允许更改字段:{}"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of Actions"
+msgstr "操作次数"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of errors"
+msgstr "错误数量"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr "需要处理的消息数量"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__message_has_error_counter
+msgid "Number of messages with delivery error"
+msgstr "递送错误消息数量"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__parent_id
+msgid "Parent Channel"
+msgstr "父频道"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_channel.py:0
+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 ""
+"表达式用于根据重试可重试错误的次数,指定下一次执行推迟的秒数。将秒数设置为一"
+"个两个元素的元组或列表将会在两个值之间随机化重试间隔。 \n"
+"示例:{1: 10, 5: 20, 10: 30, 15: 300}。 \n"
+"示例:{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}。 \n"
+"详情请参阅模块描述。"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__pending
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Pending"
+msgstr "等待"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__priority
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Priority"
+msgstr "优先级"
+
+#. module: queue_job
+#: model:ir.ui.menu,name:queue_job.menu_queue
+msgid "Queue"
+msgstr "队列"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_lock__queue_job_id
+msgid "Queue Job"
+msgstr "队列作业"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_job_lock
+msgid "Queue Job Lock"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__records
+msgid "Record(s)"
+msgstr "记录"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Related"
+msgstr "相关的"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_related_action
+msgid "Related Action"
+msgstr "相关动作"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__related_action
+msgid "Related Action (serialized)"
+msgstr "相关操作(已序列化)"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Record"
+msgstr "相关记录"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+msgid "Related Records"
+msgstr "相关记录"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_tree
+msgid "Remaining days to execute"
+msgstr "剩余执行天数"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_channel__removal_interval
+msgid "Removal Interval"
+msgstr "清除间隔"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue"
+msgstr "重新排队"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Requeue Job"
+msgstr "重新排队作业"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_requeue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "Requeue Jobs"
+msgstr "重新排队作业"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__activity_user_id
+msgid "Responsible User"
+msgstr "负责的用户"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__result
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Result"
+msgstr "结果"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Results"
+msgstr "结果"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__edit_retry_pattern
+msgid "Retry Pattern"
+msgstr "重试模式"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job_function__retry_pattern
+msgid "Retry Pattern (serialized)"
+msgstr "重试模式(已序列化)"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_jobs_to_done
+msgid "Set all selected jobs to done"
+msgstr "将所有选定的作业设置为完成"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set jobs done"
+msgstr "设置作业完成"
+
+#. module: queue_job
+#: model:ir.actions.act_window,name:queue_job.action_set_jobs_done
+msgid "Set jobs to done"
+msgstr "将作业设置为完成"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr "设置为“完成”"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "Set to done"
+msgstr "设置为完成"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__graph_uuid
+msgid "Single shared identifier of a Graph. Empty for a single job."
+msgstr "图的唯一共享标识符。对于单个任务,则为空。"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job.py:0
+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 ""
+"基于活动的状态\n"
+"逾期:已经超过截止日期\n"
+"现今:活动日期是当天\n"
+"计划:未来的活动。"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__func_string
+msgid "Task"
+msgstr "任务"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job_function__edit_related_action
+msgid ""
+"The action when the button *Related Action* is used on a job. The default "
+"action is to open the view of the record related to the job. Configured as a "
+"dictionary with optional keys: enable, func_name, kwargs.\n"
+"See the module description for details."
+msgstr ""
+"当在作业上使用“相关操作”按钮时所执行的动作。默认动作是打开与该作业相关的记录"
+"视图。此配置作为一个字典,包含可选键:enable, func_name, kwargs。 详细配置请"
+"参阅模块说明。"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__max_retries
+msgid ""
+"The job will fail if the number of tries reach the max. retries.\n"
+"Retries are infinite when empty."
+msgstr ""
+"如果尝试次数达到最大重试次数,作业将失败。\n"
+"空的时候重试是无限的。"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_cancelled
+msgid "The selected jobs will be cancelled."
+msgstr "被选中的任务将被取消。"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_requeue_job
+msgid "The selected jobs will be requeued."
+msgstr "所选作业将重新排队。"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_set_jobs_done
+msgid "The selected jobs will be set to done."
+msgstr "所选作业将设置为完成。"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_form
+msgid "Time (s)"
+msgstr "时间(秒)"
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__exec_time
+msgid "Time required to execute this job in seconds. Average when grouped."
+msgstr "以秒为单位执行此任务所需的时间。分组时为平均值。"
+
+#. module: queue_job
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Tried many times"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr "记录的异常活动的类型。"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__uuid
+msgid "UUID"
+msgstr "UUID"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Related Action for {}.\n"
+"Example of valid format:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+msgstr ""
+"对于 {},关联操作的格式不符合预期。 \n"
+"有效格式的示例:\n"
+"{{\"enable\": True, \"func_name\": \"related_action_foo\", "
+"\"kwargs\" {{\"limit\": 10}}}}"
+
+#. module: queue_job
+#. odoo-python
+#: code:addons/queue_job/models/queue_job_function.py:0
+msgid ""
+"Unexpected format of Retry Pattern for {}.\n"
+"Example of valid formats:\n"
+"{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+"{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+msgstr ""
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__user_id
+msgid "User ID"
+msgstr "用户"
+
+#. module: queue_job
+#: model:ir.model.fields.selection,name:queue_job.selection__queue_job__state__wait_dependencies
+#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search
+msgid "Wait Dependencies"
+msgstr "等待依赖项"
+
+#. module: queue_job
+#: model:ir.model,name:queue_job.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr "重新排队向导所选的作业"
+
+#. module: queue_job
+#: model:ir.model.fields,field_description:queue_job.field_queue_job__worker_pid
+msgid "Worker Pid"
+msgstr "工作进程PID"
+
+#~ msgid "If both parameters are 0, ALL jobs will be requeued!"
+#~ msgstr "如果两个参数都为0,所有任务都将被重新排队!"
+
+#~ msgid "Jobs Garbage Collector"
+#~ msgstr "作业垃圾收集器"
+
+#~ msgid "Queue jobs must be created by calling 'with_delay()'."
+#~ msgstr "队列任务必须通过调用'with_delay()'方法来创建。"
+
+#~ msgid "SMS Delivery error"
+#~ msgstr "短信传递错误"
+
+#, python-format
+#~ msgid "Cancelled by %s"
+#~ msgstr "被%s取消"
+
+#, python-format
+#~ msgid "Job interrupted and set to Done: nothing to do."
+#~ msgstr "作业中断并设置为已完成:无需执行任何操作。"
+
+#, python-format
+#~ msgid "Manually set to done by %s"
+#~ msgstr "由%s手动设置为完成"
+
+#~ msgid "Record"
+#~ msgstr "记录"
+
+#, python-format
+#~ msgid ""
+#~ "Unexpected format of Retry Pattern for {}.\n"
+#~ "Example of valid format:\n"
+#~ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+#~ msgstr ""
+#~ "对于 {},重试模式的格式不符合预期。 有效格式的示例:\n"
+#~ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+
+#~ msgid "Last Modified on"
+#~ msgstr "最后修改日"
+
+#~ msgid "Main Attachment"
+#~ msgstr "附件"
+
+#~ msgid "Number of messages which requires an action"
+#~ msgstr "需要操作消息数量"
+
+#~ msgid ""
+#~ " If the max. retries is 0, the number "
+#~ "of retries is infinite."
+#~ msgstr ""
+#~ "如果最大重试次数是0,则重试次数是无限"
+#~ "的。"
+
+#~ msgid "Override Channel"
+#~ msgstr "覆盖频道"
+
+#~ msgid "Number of unread messages"
+#~ msgstr "未读消息数量"
diff --git a/odex30_base/queue_job/job.py b/odex30_base/queue_job/job.py
new file mode 100644
index 0000000..6cfe12f
--- /dev/null
+++ b/odex30_base/queue_job/job.py
@@ -0,0 +1,870 @@
+# Copyright 2013-2020 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+import hashlib
+import inspect
+import logging
+import os
+import sys
+import uuid
+import weakref
+from datetime import datetime, timedelta
+from random import randint
+
+import odoo
+
+from .exception import FailedJobError, NoSuchJobError, RetryableJobError
+
+WAIT_DEPENDENCIES = "wait_dependencies"
+PENDING = "pending"
+ENQUEUED = "enqueued"
+CANCELLED = "cancelled"
+DONE = "done"
+STARTED = "started"
+FAILED = "failed"
+
+STATES = [
+ (WAIT_DEPENDENCIES, "Wait Dependencies"),
+ (PENDING, "Pending"),
+ (ENQUEUED, "Enqueued"),
+ (STARTED, "Started"),
+ (DONE, "Done"),
+ (CANCELLED, "Cancelled"),
+ (FAILED, "Failed"),
+]
+
+DEFAULT_PRIORITY = 10 # used by the PriorityQueue to sort the jobs
+DEFAULT_MAX_RETRIES = 5
+RETRY_INTERVAL = 10 * 60 # seconds
+
+_logger = logging.getLogger(__name__)
+
+
+def identity_exact(job_):
+ """Identity function using the model, method and all arguments as key
+
+ When used, this identity key will have the effect that when a job should be
+ created and a pending job with the exact same recordset and arguments, the
+ second will not be created.
+
+ It should be used with the ``identity_key`` argument:
+
+ .. python::
+
+ from odoo.addons.queue_job.job import identity_exact
+
+ # [...]
+ delayable = self.with_delay(identity_key=identity_exact)
+ delayable.export_record(force=True)
+
+ Alternative identity keys can be built using the various fields of the job.
+ For example, you could compute a hash using only some arguments of
+ the job.
+
+ .. python::
+
+ def identity_example(job_):
+ hasher = hashlib.sha1()
+ hasher.update(job_.model_name)
+ hasher.update(job_.method_name)
+ hasher.update(str(sorted(job_.recordset.ids)))
+ hasher.update(str(job_.args[1]))
+ hasher.update(str(job_.kwargs.get('foo', '')))
+ return hasher.hexdigest()
+
+ Usually you will probably always want to include at least the name of the
+ model and method.
+ """
+ hasher = identity_exact_hasher(job_)
+ return hasher.hexdigest()
+
+
+def identity_exact_hasher(job_):
+ """Prepare hasher object for identity_exact."""
+ hasher = hashlib.sha1()
+ hasher.update(job_.model_name.encode("utf-8"))
+ hasher.update(job_.method_name.encode("utf-8"))
+ hasher.update(str(sorted(job_.recordset.ids)).encode("utf-8"))
+ hasher.update(str(job_.args).encode("utf-8"))
+ hasher.update(str(sorted(job_.kwargs.items())).encode("utf-8"))
+ return hasher
+
+
+class Job:
+ """A Job is a task to execute. It is the in-memory representation of a job.
+
+ Jobs are stored in the ``queue.job`` Odoo Model, but they are handled
+ through this class.
+
+ .. attribute:: uuid
+
+ Id (UUID) of the job.
+
+ .. attribute:: graph_uuid
+
+ Shared UUID of the job's graph. Empty if the job is a single job.
+
+ .. attribute:: state
+
+ State of the job, can pending, enqueued, started, done or failed.
+ The start state is pending and the final state is done.
+
+ .. attribute:: retry
+
+ The current try, starts at 0 and each time the job is executed,
+ it increases by 1.
+
+ .. attribute:: max_retries
+
+ The maximum number of retries allowed before the job is
+ considered as failed.
+
+ .. attribute:: args
+
+ Arguments passed to the function when executed.
+
+ .. attribute:: kwargs
+
+ Keyword arguments passed to the function when executed.
+
+ .. attribute:: description
+
+ Human description of the job.
+
+ .. attribute:: func
+
+ The python function itself.
+
+ .. attribute:: model_name
+
+ Odoo model on which the job will run.
+
+ .. attribute:: priority
+
+ Priority of the job, 0 being the higher priority.
+
+ .. attribute:: date_created
+
+ Date and time when the job was created.
+
+ .. attribute:: date_enqueued
+
+ Date and time when the job was enqueued.
+
+ .. attribute:: date_started
+
+ Date and time when the job was started.
+
+ .. attribute:: date_done
+
+ Date and time when the job was done.
+
+ .. attribute:: result
+
+ A description of the result (for humans).
+
+ .. attribute:: exc_name
+
+ Exception error name when the job failed.
+
+ .. attribute:: exc_message
+
+ Exception error message when the job failed.
+
+ .. attribute:: exc_info
+
+ Exception information (traceback) when the job failed.
+
+ .. attribute:: user_id
+
+ Odoo user id which created the job
+
+ .. attribute:: eta
+
+ Estimated Time of Arrival of the job. It will not be executed
+ before this date/time.
+
+ .. attribute:: recordset
+
+ Model recordset when we are on a delayed Model method
+
+ .. attribute::channel
+
+ The complete name of the channel to use to process the job. If
+ provided it overrides the one defined on the job's function.
+
+ .. attribute::identity_key
+
+ A key referencing the job, multiple job with the same key will not
+ be added to a channel if the existing job with the same key is not yet
+ started or executed.
+
+ """
+
+ @classmethod
+ def load(cls, env, job_uuid):
+ """Read a single job from the Database
+
+ Raise an error if the job is not found.
+ """
+ stored = cls.db_records_from_uuids(env, [job_uuid])
+ if not stored:
+ raise NoSuchJobError(f"Job {job_uuid} does no longer exist in the storage.")
+ return cls._load_from_db_record(stored)
+
+ @classmethod
+ def load_many(cls, env, job_uuids):
+ """Read jobs in batch from the Database
+
+ Jobs not found are ignored.
+ """
+ recordset = cls.db_records_from_uuids(env, job_uuids)
+ return {cls._load_from_db_record(record) for record in recordset}
+
+ def add_lock_record(self):
+ """
+ Create row in db to be locked while the job is being performed.
+ """
+ self.env.cr.execute(
+ """
+ INSERT INTO
+ queue_job_lock (id, queue_job_id)
+ SELECT
+ id, id
+ FROM
+ queue_job
+ WHERE
+ uuid = %s
+ ON CONFLICT(id)
+ DO NOTHING;
+ """,
+ [self.uuid],
+ )
+
+ def lock(self):
+ """
+ Lock row of job that is being performed
+
+ If a job cannot be locked,
+ it means that the job wasn't started,
+ a RetryableJobError is thrown.
+ """
+ self.env.cr.execute(
+ """
+ SELECT
+ *
+ FROM
+ queue_job_lock
+ WHERE
+ queue_job_id in (
+ SELECT
+ id
+ FROM
+ queue_job
+ WHERE
+ uuid = %s
+ AND state='started'
+ )
+ FOR UPDATE;
+ """,
+ [self.uuid],
+ )
+
+ # 1 job should be locked
+ if 1 != len(self.env.cr.fetchall()):
+ raise RetryableJobError(
+ f"Trying to lock job that wasn't started, uuid: {self.uuid}"
+ )
+
+ @classmethod
+ def _load_from_db_record(cls, job_db_record):
+ stored = job_db_record
+
+ args = stored.args
+ kwargs = stored.kwargs
+ method_name = stored.method_name
+
+ recordset = stored.records
+ method = getattr(recordset, method_name)
+
+ eta = None
+ if stored.eta:
+ eta = stored.eta
+
+ job_ = cls(
+ method,
+ args=args,
+ kwargs=kwargs,
+ priority=stored.priority,
+ eta=eta,
+ job_uuid=stored.uuid,
+ description=stored.name,
+ channel=stored.channel,
+ identity_key=stored.identity_key,
+ )
+
+ if stored.date_created:
+ job_.date_created = stored.date_created
+
+ if stored.date_enqueued:
+ job_.date_enqueued = stored.date_enqueued
+
+ if stored.date_started:
+ job_.date_started = stored.date_started
+
+ if stored.date_done:
+ job_.date_done = stored.date_done
+
+ if stored.date_cancelled:
+ job_.date_cancelled = stored.date_cancelled
+
+ job_.state = stored.state
+ job_.graph_uuid = stored.graph_uuid if stored.graph_uuid else None
+ job_.result = stored.result if stored.result else None
+ job_.exc_info = stored.exc_info if stored.exc_info else None
+ job_.retry = stored.retry
+ job_.max_retries = stored.max_retries
+ if stored.company_id:
+ job_.company_id = stored.company_id.id
+ job_.identity_key = stored.identity_key
+ job_.worker_pid = stored.worker_pid
+
+ job_.__depends_on_uuids.update(stored.dependencies.get("depends_on", []))
+ job_.__reverse_depends_on_uuids.update(
+ stored.dependencies.get("reverse_depends_on", [])
+ )
+ return job_
+
+ def job_record_with_same_identity_key(self):
+ """Check if a job to be executed with the same key exists."""
+ existing = (
+ self.env["queue.job"]
+ .sudo()
+ .search(
+ [
+ ("identity_key", "=", self.identity_key),
+ ("state", "in", [WAIT_DEPENDENCIES, PENDING, ENQUEUED]),
+ ],
+ limit=1,
+ )
+ )
+ return existing
+
+ @staticmethod
+ def db_records_from_uuids(env, job_uuids):
+ model = env["queue.job"].sudo()
+ record = model.search([("uuid", "in", tuple(job_uuids))])
+ return record.with_env(env).sudo()
+
+ def __init__(
+ self,
+ func,
+ args=None,
+ kwargs=None,
+ priority=None,
+ eta=None,
+ job_uuid=None,
+ max_retries=None,
+ description=None,
+ channel=None,
+ identity_key=None,
+ ):
+ """Create a Job
+
+ :param func: function to execute
+ :type func: function
+ :param args: arguments for func
+ :type args: tuple
+ :param kwargs: keyworkd arguments for func
+ :type kwargs: dict
+ :param priority: priority of the job,
+ the smaller is the higher priority
+ :type priority: int
+ :param eta: the job can be executed only after this datetime
+ (or now + timedelta)
+ :type eta: datetime or timedelta
+ :param job_uuid: UUID of the job
+ :param max_retries: maximum number of retries before giving up and set
+ the job state to 'failed'. A value of 0 means infinite retries.
+ :param description: human description of the job. If None, description
+ is computed from the function doc or name
+ :param channel: The complete channel name to use to process the job.
+ :param identity_key: A hash to uniquely identify a job, or a function
+ that returns this hash (the function takes the job
+ as argument)
+ """
+ if args is None:
+ args = ()
+ if isinstance(args, list):
+ args = tuple(args)
+ assert isinstance(args, tuple), f"{args}: args are not a tuple"
+ if kwargs is None:
+ kwargs = {}
+
+ assert isinstance(kwargs, dict), f"{kwargs}: kwargs are not a dict"
+
+ if not _is_model_method(func):
+ raise TypeError("Job accepts only methods of Models")
+
+ recordset = func.__self__
+ env = recordset.env
+ self.method_name = func.__name__
+ self.recordset = recordset
+
+ self.env = env
+ self.job_model = self.env["queue.job"]
+ self.job_model_name = "queue.job"
+
+ self.job_config = (
+ self.env["queue.job.function"].sudo().job_config(self.job_function_name)
+ )
+
+ self.state = PENDING
+
+ self.retry = 0
+ if max_retries is None:
+ self.max_retries = DEFAULT_MAX_RETRIES
+ else:
+ self.max_retries = max_retries
+
+ self._uuid = job_uuid
+ self.graph_uuid = None
+
+ self.args = args
+ self.kwargs = kwargs
+
+ self.__depends_on_uuids = set()
+ self.__reverse_depends_on_uuids = set()
+ self._depends_on = set()
+ self._reverse_depends_on = weakref.WeakSet()
+
+ self.priority = priority
+ if self.priority is None:
+ self.priority = DEFAULT_PRIORITY
+
+ self.date_created = datetime.now()
+ self._description = description
+
+ if isinstance(identity_key, str):
+ self._identity_key = identity_key
+ self._identity_key_func = None
+ else:
+ # we'll compute the key on the fly when called
+ # from the function
+ self._identity_key = None
+ self._identity_key_func = identity_key
+
+ self.date_enqueued = None
+ self.date_started = None
+ self.date_done = None
+ self.date_cancelled = None
+
+ self.result = None
+ self.exc_name = None
+ self.exc_message = None
+ self.exc_info = None
+
+ if "company_id" in env.context:
+ company_id = env.context["company_id"]
+ else:
+ company_id = env.company.id
+ self.company_id = company_id
+ self._eta = None
+ self.eta = eta
+ self.channel = channel
+ self.worker_pid = None
+
+ def add_depends(self, jobs):
+ if self in jobs:
+ raise ValueError("job cannot depend on itself")
+ self.__depends_on_uuids |= {j.uuid for j in jobs}
+ self._depends_on.update(jobs)
+ for parent in jobs:
+ parent.__reverse_depends_on_uuids.add(self.uuid)
+ parent._reverse_depends_on.add(self)
+ if any(j.state != DONE for j in jobs):
+ self.state = WAIT_DEPENDENCIES
+
+ def perform(self):
+ """Execute the job.
+
+ The job is executed with the user which has initiated it.
+ """
+ self.retry += 1
+ try:
+ self.result = self.func(*tuple(self.args), **self.kwargs)
+ except RetryableJobError as err:
+ if err.ignore_retry:
+ self.retry -= 1
+ raise
+ elif not self.max_retries: # infinite retries
+ raise
+ elif self.retry >= self.max_retries:
+ type_, value, traceback = sys.exc_info()
+ # change the exception type but keep the original
+ # traceback and message:
+ # http://blog.ianbicking.org/2007/09/12/re-raising-exceptions/
+ new_exc = FailedJobError(
+ "Max. retries (%d) reached: %s" % (self.max_retries, value or type_)
+ )
+ raise new_exc from err
+ raise
+
+ return self.result
+
+ def _get_common_dependent_jobs_query(self):
+ return """
+ UPDATE queue_job
+ SET state = %s
+ FROM (
+ SELECT child.id, array_agg(parent.state) as parent_states
+ FROM queue_job job
+ JOIN LATERAL
+ json_array_elements_text(
+ job.dependencies::json->'reverse_depends_on'
+ ) child_deps ON true
+ JOIN queue_job child
+ ON child.graph_uuid = job.graph_uuid
+ AND child.uuid = child_deps
+ JOIN LATERAL
+ json_array_elements_text(
+ child.dependencies::json->'depends_on'
+ ) parent_deps ON true
+ JOIN queue_job parent
+ ON parent.graph_uuid = job.graph_uuid
+ AND parent.uuid = parent_deps
+ WHERE job.uuid = %s
+ GROUP BY child.id
+ ) jobs
+ WHERE
+ queue_job.id = jobs.id
+ AND %s = ALL(jobs.parent_states)
+ AND state = %s;
+ """
+
+ def enqueue_waiting(self):
+ sql = self._get_common_dependent_jobs_query()
+ self.env.cr.execute(sql, (PENDING, self.uuid, DONE, WAIT_DEPENDENCIES))
+ self.env["queue.job"].invalidate_model(["state"])
+
+ def cancel_dependent_jobs(self):
+ sql = self._get_common_dependent_jobs_query()
+ self.env.cr.execute(sql, (CANCELLED, self.uuid, CANCELLED, WAIT_DEPENDENCIES))
+ self.env["queue.job"].invalidate_model(["state"])
+
+ def store(self):
+ """Store the Job"""
+ job_model = self.env["queue.job"]
+ # The sentinel is used to prevent edition sensitive fields (such as
+ # method_name) from RPC methods.
+ edit_sentinel = job_model.EDIT_SENTINEL
+
+ db_record = self.db_record()
+ if db_record:
+ db_record.with_context(_job_edit_sentinel=edit_sentinel).write(
+ self._store_values()
+ )
+ else:
+ job_model.with_context(_job_edit_sentinel=edit_sentinel).sudo().create(
+ self._store_values(create=True)
+ )
+
+ def _store_values(self, create=False):
+ vals = {
+ "state": self.state,
+ "priority": self.priority,
+ "retry": self.retry,
+ "max_retries": self.max_retries,
+ "exc_name": self.exc_name,
+ "exc_message": self.exc_message,
+ "exc_info": self.exc_info,
+ "company_id": self.company_id,
+ "result": str(self.result) if self.result else False,
+ "date_enqueued": False,
+ "date_started": False,
+ "date_done": False,
+ "exec_time": False,
+ "date_cancelled": False,
+ "eta": False,
+ "identity_key": False,
+ "worker_pid": self.worker_pid,
+ "graph_uuid": self.graph_uuid,
+ }
+
+ if self.date_enqueued:
+ vals["date_enqueued"] = self.date_enqueued
+ if self.date_started:
+ vals["date_started"] = self.date_started
+ if self.date_done:
+ vals["date_done"] = self.date_done
+ if self.exec_time:
+ vals["exec_time"] = self.exec_time
+ if self.date_cancelled:
+ vals["date_cancelled"] = self.date_cancelled
+ if self.eta:
+ vals["eta"] = self.eta
+ if self.identity_key:
+ vals["identity_key"] = self.identity_key
+
+ dependencies = {
+ "depends_on": [parent.uuid for parent in self.depends_on],
+ "reverse_depends_on": [
+ children.uuid for children in self.reverse_depends_on
+ ],
+ }
+ vals["dependencies"] = dependencies
+
+ if create:
+ vals.update(
+ {
+ "user_id": self.env.uid,
+ "channel": self.channel,
+ # The following values must never be modified after the
+ # creation of the job
+ "uuid": self.uuid,
+ "name": self.description,
+ "func_string": self.func_string,
+ "date_created": self.date_created,
+ "model_name": self.recordset._name,
+ "method_name": self.method_name,
+ "job_function_id": self.job_config.job_function_id,
+ "channel_method_name": self.job_function_name,
+ "records": self.recordset,
+ "args": self.args,
+ "kwargs": self.kwargs,
+ }
+ )
+
+ vals_from_model = self._store_values_from_model()
+ # Sanitize values: make sure you cannot screw core values
+ vals_from_model = {k: v for k, v in vals_from_model.items() if k not in vals}
+ vals.update(vals_from_model)
+ return vals
+
+ def _store_values_from_model(self):
+ vals = {}
+ value_handlers_candidates = (
+ "_job_store_values_for_" + self.method_name,
+ "_job_store_values",
+ )
+ for candidate in value_handlers_candidates:
+ handler = getattr(self.recordset, candidate, None)
+ if handler is not None:
+ vals = handler(self)
+ return vals
+
+ @property
+ def func_string(self):
+ model = repr(self.recordset)
+ args = [repr(arg) for arg in self.args]
+ kwargs = [f"{key}={val!r}" for key, val in self.kwargs.items()]
+ all_args = ", ".join(args + kwargs)
+ return f"{model}.{self.method_name}({all_args})"
+
+ def __eq__(self, other):
+ return self.uuid == other.uuid
+
+ def __hash__(self):
+ return self.uuid.__hash__()
+
+ def db_record(self):
+ return self.db_records_from_uuids(self.env, [self.uuid])
+
+ @property
+ def func(self):
+ recordset = self.recordset.with_context(job_uuid=self.uuid)
+ return getattr(recordset, self.method_name)
+
+ @property
+ def job_function_name(self):
+ func_model = self.env["queue.job.function"].sudo()
+ return func_model.job_function_name(self.recordset._name, self.method_name)
+
+ @property
+ def identity_key(self):
+ if self._identity_key is None:
+ if self._identity_key_func:
+ self._identity_key = self._identity_key_func(self)
+ return self._identity_key
+
+ @identity_key.setter
+ def identity_key(self, value):
+ if isinstance(value, str):
+ self._identity_key = value
+ self._identity_key_func = None
+ else:
+ # we'll compute the key on the fly when called
+ # from the function
+ self._identity_key = None
+ self._identity_key_func = value
+
+ @property
+ def depends_on(self):
+ if not self._depends_on:
+ self._depends_on = Job.load_many(self.env, self.__depends_on_uuids)
+ return self._depends_on
+
+ @property
+ def reverse_depends_on(self):
+ if not self._reverse_depends_on:
+ self._reverse_depends_on = Job.load_many(
+ self.env, self.__reverse_depends_on_uuids
+ )
+ return set(self._reverse_depends_on)
+
+ @property
+ def description(self):
+ if self._description:
+ return self._description
+ elif self.func.__doc__:
+ return self.func.__doc__.splitlines()[0].strip()
+ else:
+ return f"{self.model_name}.{self.func.__name__}"
+
+ @property
+ def uuid(self):
+ """Job ID, this is an UUID"""
+ if self._uuid is None:
+ self._uuid = str(uuid.uuid4())
+ return self._uuid
+
+ @property
+ def model_name(self):
+ return self.recordset._name
+
+ @property
+ def user_id(self):
+ return self.recordset.env.uid
+
+ @property
+ def eta(self):
+ return self._eta
+
+ @eta.setter
+ def eta(self, value):
+ if not value:
+ self._eta = None
+ elif isinstance(value, timedelta):
+ self._eta = datetime.now() + value
+ elif isinstance(value, int):
+ self._eta = datetime.now() + timedelta(seconds=value)
+ else:
+ self._eta = value
+
+ @property
+ def channel(self):
+ return self._channel or self.job_config.channel
+
+ @channel.setter
+ def channel(self, value):
+ self._channel = value
+
+ @property
+ def exec_time(self):
+ if self.date_done and self.date_started:
+ return (self.date_done - self.date_started).total_seconds()
+ return None
+
+ def set_pending(self, result=None, reset_retry=True):
+ if any(j.state != DONE for j in self.depends_on):
+ self.state = WAIT_DEPENDENCIES
+ else:
+ self.state = PENDING
+ self.date_enqueued = None
+ self.date_started = None
+ self.date_done = None
+ self.worker_pid = None
+ self.date_cancelled = None
+ if reset_retry:
+ self.retry = 0
+ if result is not None:
+ self.result = result
+
+ def set_enqueued(self):
+ self.state = ENQUEUED
+ self.date_enqueued = datetime.now()
+ self.date_started = None
+ self.worker_pid = None
+
+ def set_started(self):
+ self.state = STARTED
+ self.date_started = datetime.now()
+ self.worker_pid = os.getpid()
+ self.add_lock_record()
+
+ def set_done(self, result=None):
+ self.state = DONE
+ self.exc_name = None
+ self.exc_info = None
+ self.date_done = datetime.now()
+ if result is not None:
+ self.result = result
+
+ def set_cancelled(self, result=None):
+ self.state = CANCELLED
+ self.date_cancelled = datetime.now()
+ if result is not None:
+ self.result = result
+
+ def set_failed(self, **kw):
+ self.state = FAILED
+ for k, v in kw.items():
+ if v is not None:
+ setattr(self, k, v)
+
+ def __repr__(self):
+ return "" % (self.uuid, self.priority)
+
+ def _get_retry_seconds(self, seconds=None):
+ retry_pattern = self.job_config.retry_pattern
+ if not seconds and retry_pattern:
+ # ordered from higher to lower count of retries
+ patt = sorted(retry_pattern.items(), key=lambda t: t[0])
+ seconds = RETRY_INTERVAL
+ for retry_count, postpone_seconds in patt:
+ if self.retry >= retry_count:
+ seconds = postpone_seconds
+ else:
+ break
+ elif not seconds:
+ seconds = RETRY_INTERVAL
+ if isinstance(seconds, (list | tuple)):
+ seconds = randint(seconds[0], seconds[1])
+ return seconds
+
+ def postpone(self, result=None, seconds=None):
+ """Postpone the job
+
+ Write an estimated time arrival to n seconds
+ later than now. Used when an retryable exception
+ want to retry a job later.
+ """
+ eta_seconds = self._get_retry_seconds(seconds)
+ self.eta = timedelta(seconds=eta_seconds)
+ self.exc_name = None
+ self.exc_info = None
+ if result is not None:
+ self.result = result
+
+ def related_action(self):
+ record = self.db_record()
+ if not self.job_config.related_action_enable:
+ return None
+
+ funcname = self.job_config.related_action_func_name
+ if not funcname:
+ funcname = record._default_related_action
+ if not isinstance(funcname, str):
+ raise ValueError(
+ "related_action must be the name of the "
+ "method on queue.job as string"
+ )
+ action = getattr(record, funcname)
+ action_kwargs = self.job_config.related_action_kwargs
+ return action(**action_kwargs)
+
+
+def _is_model_method(func):
+ return inspect.ismethod(func) and isinstance(
+ func.__self__.__class__, odoo.models.MetaModel
+ )
diff --git a/odex30_base/queue_job/jobrunner/__init__.py b/odex30_base/queue_job/jobrunner/__init__.py
new file mode 100644
index 0000000..e2561b0
--- /dev/null
+++ b/odex30_base/queue_job/jobrunner/__init__.py
@@ -0,0 +1,163 @@
+# Copyright (c) 2015-2016 ACSONE SA/NV ()
+# Copyright 2016 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+import logging
+from threading import Thread
+import time
+
+from odoo.service import server
+from odoo.tools import config
+
+try:
+ from odoo.addons.server_environment import serv_config
+
+ if serv_config.has_section("queue_job"):
+ queue_job_config = serv_config["queue_job"]
+ else:
+ queue_job_config = {}
+except ImportError:
+ queue_job_config = config.misc.get("queue_job", {})
+
+
+from .runner import QueueJobRunner, _channels
+
+_logger = logging.getLogger(__name__)
+
+START_DELAY = 5
+
+
+# Here we monkey patch the Odoo server to start the job runner thread
+# in the main server process (and not in forked workers). This is
+# very easy to deploy as we don't need another startup script.
+
+
+class QueueJobRunnerThread(Thread):
+ def __init__(self):
+ Thread.__init__(self)
+ self.daemon = True
+ self.runner = QueueJobRunner.from_environ_or_config()
+
+ def run(self):
+ # sleep a bit to let the workers start at ease
+ time.sleep(START_DELAY)
+ self.runner.run()
+
+ def stop(self):
+ self.runner.stop()
+
+
+class WorkerJobRunner(server.Worker):
+ """Jobrunner workers"""
+
+ def __init__(self, multi):
+ super().__init__(multi)
+ self.watchdog_timeout = None
+ self.runner = QueueJobRunner.from_environ_or_config()
+ self._recover = False
+
+ def sleep(self):
+ pass
+
+ def signal_handler(self, sig, frame): # pylint: disable=missing-return
+ _logger.debug("WorkerJobRunner (%s) received signal %s", self.pid, sig)
+ super().signal_handler(sig, frame)
+ self.runner.stop()
+
+ def process_work(self):
+ if self._recover:
+ _logger.info("WorkerJobRunner (%s) runner is reinitialized", self.pid)
+ self.runner = QueueJobRunner.from_environ_or_config()
+ self._recover = False
+ _logger.debug("WorkerJobRunner (%s) starting up", self.pid)
+ time.sleep(START_DELAY)
+ self.runner.run()
+
+ def signal_time_expired_handler(self, n, stack):
+ _logger.info(
+ "Worker (%d) CPU time limit (%s) reached.Stop gracefully and recover",
+ self.pid,
+ config["limit_time_cpu"],
+ )
+ self._recover = True
+ self.runner.stop()
+
+
+runner_thread = None
+
+
+def _is_runner_enabled():
+ return not _channels().strip().startswith("root:0")
+
+
+def _start_runner_thread(server_type):
+ global runner_thread
+ if not config["stop_after_init"]:
+ if _is_runner_enabled():
+ _logger.info("starting jobrunner thread (in %s)", server_type)
+ runner_thread = QueueJobRunnerThread()
+ runner_thread.start()
+ else:
+ _logger.info(
+ "jobrunner thread (in %s) NOT started, "
+ "because the root channel's capacity is set to 0",
+ server_type,
+ )
+
+
+orig_prefork__init__ = server.PreforkServer.__init__
+orig_prefork_process_spawn = server.PreforkServer.process_spawn
+orig_prefork_worker_pop = server.PreforkServer.worker_pop
+orig_threaded_start = server.ThreadedServer.start
+orig_threaded_stop = server.ThreadedServer.stop
+
+
+def prefork__init__(server, app):
+ res = orig_prefork__init__(server, app)
+ server.jobrunner = {}
+ return res
+
+
+def prefork_process_spawn(server):
+ orig_prefork_process_spawn(server)
+ if not hasattr(server, "jobrunner"):
+ # if 'queue_job' is not in server wide modules, PreforkServer is
+ # not initialized with a 'jobrunner' attribute, skip this
+ return
+ if not server.jobrunner and _is_runner_enabled():
+ server.worker_spawn(WorkerJobRunner, server.jobrunner)
+
+
+def prefork_worker_pop(server, pid):
+ res = orig_prefork_worker_pop(server, pid)
+ if not hasattr(server, "jobrunner"):
+ # if 'queue_job' is not in server wide modules, PreforkServer is
+ # not initialized with a 'jobrunner' attribute, skip this
+ return res
+ if pid in server.jobrunner:
+ server.jobrunner.pop(pid)
+ return res
+
+
+def threaded_start(server, *args, **kwargs):
+ res = orig_threaded_start(server, *args, **kwargs)
+ _start_runner_thread("threaded server")
+ return res
+
+
+def threaded_stop(server):
+ global runner_thread
+ if runner_thread:
+ runner_thread.stop()
+ res = orig_threaded_stop(server)
+ if runner_thread:
+ runner_thread.join()
+ runner_thread = None
+ return res
+
+
+server.PreforkServer.__init__ = prefork__init__
+server.PreforkServer.process_spawn = prefork_process_spawn
+server.PreforkServer.worker_pop = prefork_worker_pop
+server.ThreadedServer.start = threaded_start
+server.ThreadedServer.stop = threaded_stop
diff --git a/odex30_base/queue_job/jobrunner/__main__.py b/odex30_base/queue_job/jobrunner/__main__.py
new file mode 100644
index 0000000..8f9628c
--- /dev/null
+++ b/odex30_base/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/odex30_base/queue_job/jobrunner/channels.py b/odex30_base/queue_job/jobrunner/channels.py
new file mode 100644
index 0000000..c895d9c
--- /dev/null
+++ b/odex30_base/queue_job/jobrunner/channels.py
@@ -0,0 +1,1082 @@
+# Copyright (c) 2015-2016 ACSONE SA/NV ()
+# 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 CANCELLED, DONE, ENQUEUED, FAILED, PENDING, STARTED, WAIT_DEPENDENCIES
+
+NOT_DONE = (WAIT_DEPENDENCIES, PENDING, ENQUEUED, STARTED, FAILED)
+JobSortingKey = namedtuple("SortingKey", "eta priority date_created seq")
+
+_logger = logging.getLogger(__name__)
+
+
+class PriorityQueue:
+ """A priority queue that supports removing arbitrary objects.
+
+ Adding an object already in the queue is a no op.
+ Popping an empty queue returns None.
+
+ >>> q = PriorityQueue()
+ >>> q.add(2)
+ >>> q.add(3)
+ >>> q.add(3)
+ >>> q.add(1)
+ >>> q[0]
+ 1
+ >>> len(q)
+ 3
+ >>> q.pop()
+ 1
+ >>> q.remove(2)
+ >>> len(q)
+ 1
+ >>> q[0]
+ 3
+ >>> q.pop()
+ 3
+ >>> q.pop()
+ >>> q.add(2)
+ >>> q.remove(2)
+ >>> q.add(2)
+ >>> q.pop()
+ 2
+ """
+
+ def __init__(self):
+ self._heap = []
+ self._known = set() # all objects in the heap (including removed)
+ self._removed = set() # all objects that have been removed
+
+ def __len__(self):
+ return len(self._known) - len(self._removed)
+
+ def __getitem__(self, i):
+ if i != 0:
+ raise IndexError()
+ while True:
+ if not self._heap:
+ raise IndexError()
+ o = self._heap[0]
+ if o in self._removed:
+ o2 = heappop(self._heap)
+ assert o2 == o
+ self._removed.remove(o)
+ self._known.remove(o)
+ else:
+ return o
+
+ def __contains__(self, o):
+ return o in self._known and o not in self._removed
+
+ def add(self, o):
+ if o is None:
+ raise ValueError()
+ self._removed.discard(o)
+ if o in self._known:
+ return
+ self._known.add(o)
+ heappush(self._heap, o)
+
+ def remove(self, o):
+ if o is None:
+ raise ValueError()
+ if o not in self._known:
+ return
+ self._removed.add(o)
+
+ def pop(self):
+ while True:
+ try:
+ o = heappop(self._heap)
+ except IndexError:
+ # queue is empty
+ return None
+ self._known.remove(o)
+ if o in self._removed:
+ self._removed.remove(o)
+ else:
+ return o
+
+
+@total_ordering
+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 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
+ * then jobs with a smaller sequence come first
+
+ Here are some examples.
+
+ j1 comes before j2 because it has an earlier date_created
+
+ >>> j1 = ChannelJob(None, None, 1,
+ ... seq=0, date_created=1, priority=9, eta=None)
+ >>> j1
+
+ >>> j2 = ChannelJob(None, None, 2,
+ ... seq=0, date_created=2, priority=9, eta=None)
+ >>> j1 < j2
+ True
+
+ j3 comes first because it has lower priority,
+ despite having a creation date after j1 and j2
+
+ >>> j3 = ChannelJob(None, None, 3,
+ ... seq=0, date_created=3, priority=2, eta=None)
+ >>> j3 < j1
+ True
+
+ 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
+ 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
+
+ >>> j6 = ChannelJob(None, None, 6,
+ ... seq=0, date_created=5, priority=9, eta=2)
+ >>> j6 < j4 < j5
+ True
+
+ Here is the complete suite:
+
+ >>> j6 < j4 < j5 and j3 < j1 < j2
+ True
+
+ j0 has the same properties as j1 but they are not considered
+ equal as they are different instances
+
+ >>> j0 = ChannelJob(None, None, 1,
+ ... seq=0, date_created=1, priority=9, eta=None)
+ >>> j0 == j1
+ False
+ >>> j0 == j0
+ True
+
+ Comparison excluding eta:
+
+ >>> j1.sorting_key_ignoring_eta() < j2.sorting_key_ignoring_eta()
+ True
+
+ """
+
+ __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._sorting_key = JobSortingKey(eta, priority, date_created, seq)
+
+ def __repr__(self):
+ return f""
+
+ def __eq__(self, other):
+ return id(self) == id(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):
+ # DEPRECATED
+ return self._sorting_key
+
+ def sorting_key_ignoring_eta(self):
+ return self._sorting_key[1:]
+
+ def __lt__(self, other):
+ # 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:
+ """A channel queue is a priority queue for jobs.
+
+ Jobs with an eta are set aside until their eta is past due, at
+ which point they start competing normally with other jobs.
+
+ >>> q = ChannelQueue()
+ >>> j1 = ChannelJob(None, None, 1,
+ ... seq=0, date_created=1, priority=1, eta=10)
+ >>> j2 = ChannelJob(None, None, 2,
+ ... seq=0, date_created=2, priority=1, eta=None)
+ >>> j3 = ChannelJob(None, None, 3,
+ ... seq=0, date_created=3, priority=1, eta=None)
+ >>> q.add(j1)
+ >>> q.add(j2)
+ >>> q.add(j3)
+
+ Wakeup time is the eta of job 1.
+
+ >>> q.get_wakeup_time()
+ 10
+
+ We have not reached the eta of job 1, so we get job 2.
+
+ >>> q.pop(now=1)
+
+
+ Wakeup time is still the eta of job 1, and we get job 1 when we are past
+ it's eta.
+
+ >>> q.get_wakeup_time()
+ 10
+ >>> q.pop(now=11)
+
+
+ Now there is no wakeup time anymore, because no job have an eta.
+
+ >>> q.get_wakeup_time()
+ 0
+ >>> q.pop(now=12)
+
+ >>> q.get_wakeup_time()
+ 0
+ >>> q.pop(now=13)
+
+ Observe that job with past eta still run after jobs with higher priority.
+
+ >>> j4 = ChannelJob(None, None, 4,
+ ... seq=0, date_created=4, priority=10, eta=20)
+ >>> j5 = ChannelJob(None, None, 5,
+ ... seq=0, date_created=5, priority=1, eta=None)
+ >>> q.add(j4)
+ >>> q.add(j5)
+ >>> q.get_wakeup_time()
+ 20
+ >>> q.pop(21)
+
+ >>> q.get_wakeup_time()
+ 0
+ >>> q.pop(22)
+
+
+ Test a sequential queue.
+
+ >>> sq = ChannelQueue(sequential=True)
+ >>> j6 = ChannelJob(None, None, 6,
+ ... seq=0, date_created=6, priority=1, eta=None)
+ >>> j7 = ChannelJob(None, None, 7,
+ ... seq=0, date_created=7, priority=1, eta=20)
+ >>> j8 = ChannelJob(None, None, 8,
+ ... seq=0, date_created=8, priority=1, eta=None)
+ >>> sq.add(j6)
+ >>> sq.add(j7)
+ >>> sq.add(j8)
+ >>> sq.pop(10)
+
+ >>> sq.pop(15)
+ >>> sq.pop(20)
+
+ >>> sq.pop(30)
+
+ """
+
+ def __init__(self, sequential=False):
+ self._queue = PriorityQueue()
+ self._eta_queue = PriorityQueue()
+ self.sequential = sequential
+
+ def __len__(self):
+ return len(self._eta_queue) + len(self._queue)
+
+ def __contains__(self, o):
+ return o in self._eta_queue or o in self._queue
+
+ def add(self, job):
+ if job.eta:
+ self._eta_queue.add(job)
+ else:
+ self._queue.add(job)
+
+ def remove(self, job):
+ self._eta_queue.remove(job)
+ self._queue.remove(job)
+
+ def pop(self, now):
+ while self._eta_queue and self._eta_queue[0].eta <= now:
+ eta_job = self._eta_queue.pop()
+ 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]
+ job = self._queue[0]
+
+ if eta_job.sorting_key_ignoring_eta() < job.sorting_key_ignoring_eta():
+ # eta ignored, the job with eta has higher priority
+ # than the job without eta; since it's a sequential
+ # queue we wait until eta
+ return None
+ return self._queue.pop()
+
+ def get_wakeup_time(self, wakeup_time=0):
+ if self._eta_queue:
+ if not wakeup_time:
+ wakeup_time = self._eta_queue[0].eta
+ else:
+ wakeup_time = min(wakeup_time, self._eta_queue[0].eta)
+ return wakeup_time
+
+
+class Channel:
+ """A channel for jobs, with a maximum capacity.
+
+ When jobs are created by queue_job modules, they may be associated
+ to a job channel. Jobs with no channel are inserted into the root channel.
+
+ Job channels are joined in a hierarchy down to the root channel.
+ When a job channel has available capacity, jobs are dequeued, marked
+ as running in the channel and are inserted into the queue of the
+ parent channel where they wait for available capacity and so on.
+
+ Job channels can be visualized as water channels with a given flow
+ limit (= capacity). Channels are joined together in a downstream channel
+ and the flow limit of the downstream channel limits upstream channels.::
+
+ ---------------------+
+ |
+ |
+ Ch. A C:4,Q:12,R:4 +-----------------------
+
+ ---------------------+ Ch. root C:5,Q:0,R:4
+ |
+ ---------------------+
+ Ch. B C:1,Q:0,R:0
+ ---------------------+-----------------------
+
+ The above diagram illustrates two channels joining in the root channel.
+ The root channel has a capacity of 5, and 4 running jobs coming from
+ Channel A. Channel A has a capacity of 4, all in use (passed down to the
+ root channel), and 12 jobs enqueued. Channel B has a capacity of 1,
+ none in use. This means that whenever a new job comes in channel B,
+ there will be available room for it to run in the root channel.
+
+ Note that from the point of view of a channel, 'running' means enqueued
+ in the downstream channel. Only jobs marked running in the root channel
+ are actually sent to Odoo for execution.
+
+ Should a downstream channel have less capacity than its upstream channels,
+ jobs going downstream will be enqueued in the downstream channel,
+ and compete normally according to their properties (priority, etc).
+
+ Using this technique, it is possible to enforce sequence in a channel
+ with a capacity of 1. It is also possible to dedicate a channel with a
+ limited capacity for application-autocreated subchannels
+ without risking to overflow the system.
+ """
+
+ def __init__(self, name, parent, capacity=None, sequential=False, throttle=0):
+ self.name = name
+ self.parent = parent
+ if self.parent:
+ self.parent.children[name] = self
+ self.children = {}
+ self._queue = ChannelQueue()
+ self._running = set()
+ self._failed = set()
+ self._pause_until = 0 # utc seconds since the epoch
+ self.capacity = capacity
+ self.throttle = throttle # seconds
+ self.sequential = sequential
+
+ @property
+ def sequential(self):
+ return self._queue.sequential
+
+ @sequential.setter
+ def sequential(self, val):
+ self._queue.sequential = val
+
+ def configure(self, config):
+ """Configure a channel from a dictionary.
+
+ Supported keys are:
+
+ * capacity
+ * sequential
+ * throttle
+ """
+ assert self.fullname.endswith(config["name"])
+ self.capacity = config.get("capacity", None)
+ self.sequential = bool(config.get("sequential", False))
+ self.throttle = int(config.get("throttle", 0))
+ if self.sequential and self.capacity != 1:
+ raise ValueError("A sequential channel must have a capacity of 1")
+
+ @property
+ def fullname(self):
+ """The full name of the channel, in dot separated notation."""
+ if self.parent:
+ return self.parent.fullname + "." + self.name
+ else:
+ return self.name
+
+ def get_subchannel_by_name(self, subchannel_name):
+ return self.children.get(subchannel_name)
+
+ def __str__(self):
+ capacity = "∞" if self.capacity is None else str(self.capacity)
+ return "%s(C:%s,Q:%d,R:%d,F:%d)" % (
+ self.fullname,
+ capacity,
+ len(self._queue),
+ len(self._running),
+ len(self._failed),
+ )
+
+ def remove(self, job):
+ """Remove a job from the channel."""
+ self._queue.remove(job)
+ self._running.discard(job)
+ self._failed.discard(job)
+ if self.parent:
+ self.parent.remove(job)
+
+ def set_done(self, job):
+ """Mark a job as done.
+
+ This removes it from the channel queue.
+ """
+ self.remove(job)
+ _logger.debug("job %s marked done in channel %s", job.uuid, self)
+
+ def set_pending(self, job):
+ """Mark a job as pending.
+
+ This puts the job in the channel queue and remove it
+ from parent channels queues.
+ """
+ if job not in self._queue:
+ self._queue.add(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)
+
+ def set_running(self, job):
+ """Mark a job as running.
+
+ This also marks the job as running in parent channels.
+ """
+ if job not in self._running:
+ self._queue.remove(job)
+ self._running.add(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."""
+ if job not in self._failed:
+ self._queue.remove(job)
+ self._running.discard(job)
+ self._failed.add(job)
+ if self.parent:
+ self.parent.remove(job)
+ _logger.debug("job %s marked failed in channel %s", job.uuid, self)
+
+ def has_capacity(self):
+ if self.sequential and self._failed:
+ # a sequential queue blocks on failed jobs
+ return False
+ if not self.capacity:
+ # unlimited capacity
+ return True
+ return len(self._running) < self.capacity
+
+ def get_jobs_to_run(self, now):
+ """Get jobs that are ready to run in channel.
+
+ This works by enqueuing jobs that are ready to run in children
+ channels, then yielding jobs from the channel queue until
+ ``capacity`` jobs are marked running in the channel.
+
+ If the ``throttle`` option is set on the channel, then it yields
+ no job until at least throttle seconds have elapsed since the previous
+ yield.
+
+ :param now: the current datetime in seconds
+
+ :return: iterator of
+ :class:`odoo.addons.queue_job.jobrunner.ChannelJob`
+ """
+ # enqueue jobs of children channels
+ for child in self.children.values():
+ for job in child.get_jobs_to_run(now):
+ self._queue.add(job)
+ # is this channel paused?
+ if self.throttle and self._pause_until:
+ if now < self._pause_until:
+ if self.has_capacity():
+ _logger.debug(
+ "channel %s paused until %s because "
+ "of throttle delay between jobs",
+ self,
+ self._pause_until,
+ )
+ return
+ else:
+ # unpause, this is important to avoid perpetual wakeup
+ # while the channel is at full capacity
+ self._pause_until = 0
+ _logger.debug("channel %s unpaused at %s", self, now)
+ # yield jobs that are ready to run, while we have capacity
+ while self.has_capacity():
+ job = self._queue.pop(now)
+ if not job:
+ return
+ self._running.add(job)
+ _logger.debug("job %s marked running in channel %s", job.uuid, self)
+ yield job
+ if self.throttle:
+ self._pause_until = now + self.throttle
+ _logger.debug("pausing channel %s until %s", self, self._pause_until)
+ return
+
+ def get_wakeup_time(self, wakeup_time=0):
+ if not self.has_capacity():
+ # this channel is full, do not request timed wakeup, as
+ # a notification will wakeup the runner when a job finishes
+ return wakeup_time
+ if self._pause_until:
+ # this channel is paused, request wakeup at the end of the pause
+ if not wakeup_time:
+ wakeup_time = self._pause_until
+ else:
+ wakeup_time = min(wakeup_time, self._pause_until)
+ # since this channel is paused, no need to look at the
+ # wakeup time of children nor eta jobs, as such jobs would not
+ # run anyway because they would end up in this paused channel
+ return wakeup_time
+ wakeup_time = self._queue.get_wakeup_time(wakeup_time)
+ for child in self.children.values():
+ wakeup_time = child.get_wakeup_time(wakeup_time)
+ return wakeup_time
+
+
+def split_strip(s, sep, maxsplit=-1):
+ """Split string and strip each component.
+
+ >>> split_strip("foo: bar baz\\n: fred:", ":")
+ ['foo', 'bar baz', 'fred', '']
+ """
+ return [x.strip() for x in s.split(sep, maxsplit)]
+
+
+class ChannelManager:
+ """High level interface for channels
+
+ This class handles:
+
+ * configuration of channels
+ * high level api to create and remove jobs (notify, remove_job, remove_db)
+ * get jobs to run
+
+ Here is how the runner will use it.
+
+ Let's create a channel manager and configure it.
+
+ >>> from pprint import pprint as pp
+ >>> cm = ChannelManager()
+ >>> cm.simple_configure('root:4,A:4,B:1')
+ >>> db = 'db'
+
+ Add a few jobs in channel A with priority 10
+
+ >>> cm.notify(db, 'A', 'A1', 1, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'A', 'A2', 2, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'A', 'A3', 3, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'A', 'A4', 4, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'A', 'A5', 5, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'A', 'A6', 6, 0, 10, None, 'pending')
+
+ Add a few jobs in channel B with priority 5
+
+ >>> cm.notify(db, 'B', 'B1', 1, 0, 5, None, 'pending')
+ >>> cm.notify(db, 'B', 'B2', 2, 0, 5, None, 'pending')
+
+ We must now run one job from queue B which has a capacity of 1
+ and 3 jobs from queue A so the root channel capacity of 4 is filled.
+
+ >>> pp(list(cm.get_jobs_to_run(now=100)))
+ [, , , ]
+
+ Job A2 is done. Next job to run is A5, even if we have
+ higher priority job in channel B, because channel B has a capacity of 1.
+
+ >>> cm.notify(db, 'A', 'A2', 2, 0, 10, None, 'done')
+ >>> pp(list(cm.get_jobs_to_run(now=100)))
+ []
+
+ Job B1 is done. Next job to run is B2 because it has higher priority.
+
+ >>> cm.notify(db, 'B', 'B1', 1, 0, 5, None, 'done')
+ >>> pp(list(cm.get_jobs_to_run(now=100)))
+ []
+
+ Let's say A1 is done and A6 gets a higher priority. A6 will run next.
+
+ >>> cm.notify(db, 'A', 'A1', 1, 0, 10, None, 'done')
+ >>> cm.notify(db, 'A', 'A6', 6, 0, 5, None, 'pending')
+ >>> pp(list(cm.get_jobs_to_run(now=100)))
+ []
+
+ Let's test the throttling mechanism. Configure a 2 seconds delay
+ on channel A, end enqueue two jobs.
+
+ >>> cm = ChannelManager()
+ >>> cm.simple_configure('root:4,A:4:throttle=2')
+ >>> cm.notify(db, 'A', 'A1', 1, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'A', 'A2', 2, 0, 10, None, 'pending')
+
+ We have only one job to run, because of the throttle.
+
+ >>> pp(list(cm.get_jobs_to_run(now=100)))
+ []
+ >>> cm.get_wakeup_time()
+ 102
+
+ We have no job to run, because of the throttle.
+
+ >>> pp(list(cm.get_jobs_to_run(now=101)))
+ []
+ >>> cm.get_wakeup_time()
+ 102
+
+ 2 seconds later, we can run the other job (even though the first one
+ is still running, because we have enough capacity).
+
+ >>> pp(list(cm.get_jobs_to_run(now=102)))
+ []
+ >>> cm.get_wakeup_time()
+ 104
+
+ Let's test throttling in combination with a queue reaching full capacity.
+
+ >>> cm = ChannelManager()
+ >>> cm.simple_configure('root:4,T:2:throttle=2')
+ >>> cm.notify(db, 'T', 'T1', 1, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'T', 'T2', 2, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'T', 'T3', 3, 0, 10, None, 'pending')
+
+ >>> pp(list(cm.get_jobs_to_run(now=100)))
+ []
+ >>> pp(list(cm.get_jobs_to_run(now=102)))
+ []
+
+ Channel is now full, so no job to run even though throttling
+ delay is over.
+
+ >>> pp(list(cm.get_jobs_to_run(now=103)))
+ []
+ >>> cm.get_wakeup_time() # no wakeup time, since queue is full
+ 0
+ >>> pp(list(cm.get_jobs_to_run(now=104)))
+ []
+ >>> cm.get_wakeup_time() # queue is still full
+ 0
+
+ >>> cm.notify(db, 'T', 'T1', 1, 0, 10, None, 'done')
+ >>> pp(list(cm.get_jobs_to_run(now=105)))
+ []
+ >>> cm.get_wakeup_time() # queue is full
+ 0
+ >>> cm.notify(db, 'T', 'T2', 1, 0, 10, None, 'done')
+ >>> cm.get_wakeup_time()
+ 107
+
+ Test wakeup time behaviour in presence of eta.
+
+ >>> cm = ChannelManager()
+ >>> cm.simple_configure('root:4,E:1')
+ >>> cm.notify(db, 'E', 'E1', 1, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'E', 'E2', 2, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'E', 'E3', 3, 0, 10, None, 'pending')
+
+ >>> pp(list(cm.get_jobs_to_run(now=100)))
+ []
+ >>> pp(list(cm.get_jobs_to_run(now=101)))
+ []
+ >>> cm.notify(db, 'E', 'E1', 1, 0, 10, 105, 'pending')
+ >>> cm.get_wakeup_time() # wakeup at eta
+ 105
+ >>> pp(list(cm.get_jobs_to_run(now=102))) # but there is capacity
+ []
+ >>> pp(list(cm.get_jobs_to_run(now=106))) # no capacity anymore
+ []
+ >>> cm.get_wakeup_time() # no timed wakeup because no capacity
+ 0
+ >>> cm.notify(db, 'E', 'E2', 1, 0, 10, None, 'done')
+ >>> cm.get_wakeup_time()
+ 105
+ >>> pp(list(cm.get_jobs_to_run(now=107))) # no capacity anymore
+ []
+ >>> cm.get_wakeup_time()
+ 0
+
+ Test wakeup time behaviour in a sequential queue.
+
+ >>> cm = ChannelManager()
+ >>> cm.simple_configure('root:4,S:1:sequential')
+ >>> cm.notify(db, 'S', 'S1', 1, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'S', 'S2', 2, 0, 10, None, 'pending')
+ >>> cm.notify(db, 'S', 'S3', 3, 0, 10, None, 'pending')
+
+ >>> pp(list(cm.get_jobs_to_run(now=100)))
+ []
+ >>> cm.notify(db, 'S', 'S1', 1, 0, 10, None, 'failed')
+ >>> pp(list(cm.get_jobs_to_run(now=101)))
+ []
+ >>> cm.notify(db, 'S', 'S2', 2, 0, 10, 105, 'pending')
+ >>> pp(list(cm.get_jobs_to_run(now=102)))
+ []
+
+ No wakeup time because due to eta, because the sequential queue
+ is waiting for a failed job.
+
+ >>> cm.get_wakeup_time()
+ 0
+ >>> cm.notify(db, 'S', 'S1', 1, 0, 10, None, 'pending')
+ >>> cm.get_wakeup_time()
+ 105
+ >>> pp(list(cm.get_jobs_to_run(now=102)))
+ []
+ >>> pp(list(cm.get_jobs_to_run(now=103)))
+ []
+ >>> cm.notify(db, 'S', 'S1', 1, 0, 10, None, 'done')
+
+ At this stage, we have S2 with an eta of 105 and since the
+ queue is sequential, we wait for it.
+
+ >>> pp(list(cm.get_jobs_to_run(now=103)))
+ []
+ >>> pp(list(cm.get_jobs_to_run(now=105)))
+ []
+ >>> cm.notify(db, 'S', 'S2', 2, 0, 10, 105, 'done')
+ >>> pp(list(cm.get_jobs_to_run(now=105)))
+ []
+ >>> cm.notify(db, 'S', 'S3', 3, 0, 10, None, 'done')
+ >>> pp(list(cm.get_jobs_to_run(now=105)))
+ []
+ """
+
+ def __init__(self):
+ self._jobs_by_uuid = WeakValueDictionary()
+ self._root_channel = Channel(name="root", parent=None, capacity=1)
+ self._channels_by_name = WeakValueDictionary(root=self._root_channel)
+
+ @classmethod
+ def parse_simple_config(cls, config_string):
+ """Parse a simple channels configuration string.
+
+ The general form is as follow:
+ channel(.subchannel)*(:capacity(:key(=value)?)*)? [, ...]
+
+ If capacity is absent, it defaults to 1.
+ If a key is present without value, it gets True as value.
+ When declaring subchannels, the root channel may be omitted
+ (ie sub:4 is the same as root.sub:4).
+
+ Returns a list of channel configuration dictionaries.
+
+ >>> from pprint import pprint as pp
+ >>> pp(ChannelManager.parse_simple_config('root:4'))
+ [{'capacity': 4, 'name': 'root'}]
+ >>> pp(ChannelManager.parse_simple_config('root:4,root.sub:2'))
+ [{'capacity': 4, 'name': 'root'}, {'capacity': 2, 'name': 'root.sub'}]
+ >>> pp(ChannelManager.parse_simple_config('root:4,root.sub:2:'
+ ... 'sequential:k=v'))
+ [{'capacity': 4, 'name': 'root'},
+ {'capacity': 2, 'k': 'v', 'name': 'root.sub', 'sequential': True}]
+ >>> pp(ChannelManager.parse_simple_config('root'))
+ [{'capacity': 1, 'name': 'root'}]
+ >>> pp(ChannelManager.parse_simple_config('sub:2'))
+ [{'capacity': 2, 'name': 'sub'}]
+
+ It ignores whitespace around values, and drops empty entries which
+ would be generated by trailing commas, or commented lines on the Odoo
+ config file.
+
+ >>> pp(ChannelManager.parse_simple_config('''
+ ... root : 4,
+ ... ,
+ ... foo bar:1: k=va lue,
+ ... '''))
+ [{'capacity': 4, 'name': 'root'},
+ {'capacity': 1, 'k': 'va lue', 'name': 'foo bar'}]
+
+ It's also possible to replace commas with line breaks, which is more
+ readable if the channel configuration comes from the odoo config file.
+
+ >>> pp(ChannelManager.parse_simple_config('''
+ ... root : 4
+ ... foo bar:1: k=va lue
+ ... baz
+ ... '''))
+ [{'capacity': 4, 'name': 'root'},
+ {'capacity': 1, 'k': 'va lue', 'name': 'foo bar'},
+ {'capacity': 1, 'name': 'baz'}]
+ """
+ res = []
+ config_string = config_string.replace("\n", ",")
+ for channel_config_string in split_strip(config_string, ","):
+ if not channel_config_string:
+ # ignore empty entries (commented lines, trailing commas)
+ continue
+ config = {}
+ config_items = split_strip(channel_config_string, ":")
+ name = config_items[0]
+ if not name:
+ raise ValueError(
+ f"Invalid channel config {config_string}: missing channel name"
+ )
+ config["name"] = name
+ if len(config_items) > 1:
+ capacity = config_items[1]
+ try:
+ config["capacity"] = int(capacity)
+ except Exception as ex:
+ raise ValueError(
+ f"Invalid channel config {config_string}: "
+ f"invalid capacity {capacity}"
+ ) from ex
+ for config_item in config_items[2:]:
+ kv = split_strip(config_item, "=")
+ if len(kv) == 1:
+ k, v = kv[0], True
+ elif len(kv) == 2:
+ k, v = kv
+ else:
+ raise ValueError(
+ f"Invalid channel config {config_string}: "
+ f"incorrect config item {config_item}"
+ )
+ if k in config:
+ raise ValueError(
+ f"Invalid channel config {config_string}: "
+ f"duplicate key {k}"
+ )
+ config[k] = v
+ else:
+ config["capacity"] = 1
+ res.append(config)
+ return res
+
+ def simple_configure(self, config_string):
+ """Configure the channel manager from a simple configuration string
+
+ >>> cm = ChannelManager()
+ >>> c = cm.get_channel_by_name('root')
+ >>> c.capacity
+ 1
+ >>> cm.simple_configure('root:4,autosub.sub:2,seq:1:sequential')
+ >>> cm.get_channel_by_name('root').capacity
+ 4
+ >>> cm.get_channel_by_name('root').sequential
+ False
+ >>> cm.get_channel_by_name('root.autosub').capacity
+ >>> cm.get_channel_by_name('root.autosub.sub').capacity
+ 2
+ >>> cm.get_channel_by_name('root.autosub.sub').sequential
+ False
+ >>> cm.get_channel_by_name('autosub.sub').capacity
+ 2
+ >>> cm.get_channel_by_name('seq').capacity
+ 1
+ >>> cm.get_channel_by_name('seq').sequential
+ True
+ """
+ for config in ChannelManager.parse_simple_config(config_string):
+ self.get_channel_from_config(config)
+
+ def get_channel_from_config(self, config):
+ """Return a Channel object from a parsed configuration.
+
+ If the channel does not exist it is created.
+ The configuration is applied on the channel before returning it.
+ If some of the parent channels are missing when creating a subchannel,
+ the parent channels are auto created with an infinite capacity
+ (except for the root channel, which defaults to a capacity of 1
+ when not configured explicity).
+ """
+ channel = self.get_channel_by_name(config["name"], autocreate=True)
+ channel.configure(config)
+ _logger.info("Configured channel: %s", channel)
+ return channel
+
+ 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
+ with a default configuration and inserted in the Channels structure.
+ If autocreate is False and the channel does not exist, an exception
+ is raised.
+
+ >>> cm = ChannelManager()
+ >>> c = cm.get_channel_by_name('root', autocreate=False)
+ >>> c.name
+ 'root'
+ >>> c.fullname
+ 'root'
+ >>> c = cm.get_channel_by_name('root.sub', autocreate=True)
+ >>> c.name
+ 'sub'
+ >>> c.fullname
+ 'root.sub'
+ >>> c = cm.get_channel_by_name('sub', autocreate=True)
+ >>> c.name
+ 'sub'
+ >>> c.fullname
+ 'root.sub'
+ >>> c = cm.get_channel_by_name('autosub.sub', autocreate=True)
+ >>> c.name
+ 'sub'
+ >>> c.fullname
+ 'root.autosub.sub'
+ >>> c = cm.get_channel_by_name(None)
+ >>> c.fullname
+ 'root'
+ >>> c = cm.get_channel_by_name('root.sub')
+ >>> c.fullname
+ 'root.sub'
+ >>> 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
+ if not channel_name.startswith(self._root_channel.name + "."):
+ 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 and not parent_fallback:
+ raise ChannelNotFound(f"Channel {channel_name} not found")
+ 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:
+ subchannel = Channel(subchannel_name, parent, capacity=None)
+ self._channels_by_name[subchannel.fullname] = subchannel
+ parent = subchannel
+ return parent
+
+ def notify(
+ self, db_name, channel_name, uuid, seq, date_created, priority, eta, state
+ ):
+ channel = self.get_channel_by_name(channel_name, parent_fallback=True)
+ job = self._jobs_by_uuid.get(uuid)
+ if job:
+ # db_name is invariant
+ assert job.db_name == db_name
+ # date_created is invariant
+ assert job.date_created == date_created
+ # if one of the job properties that influence
+ # scheduling order has changed, we remove the job
+ # from the queues and create a new job object
+ if (
+ seq != job.seq
+ or priority != job.priority
+ or eta != job.eta
+ or channel != job.channel
+ ):
+ _logger.debug("job %s properties changed, rescheduling it", uuid)
+ self.remove_job(uuid)
+ job = None
+ if not job:
+ job = ChannelJob(db_name, channel, uuid, seq, date_created, priority, eta)
+ self._jobs_by_uuid[uuid] = job
+ # state transitions
+ if not state or state in (DONE, CANCELLED):
+ job.channel.set_done(job)
+ elif state == PENDING:
+ job.channel.set_pending(job)
+ elif state in (ENQUEUED, STARTED):
+ 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)
+
+ def remove_job(self, uuid):
+ job = self._jobs_by_uuid.get(uuid)
+ if job:
+ job.channel.remove(job)
+ del self._jobs_by_uuid[job.uuid]
+
+ def remove_db(self, db_name):
+ for job in list(self._jobs_by_uuid.values()):
+ if job.db_name == db_name:
+ job.channel.remove(job)
+ del self._jobs_by_uuid[job.uuid]
+
+ def get_jobs_to_run(self, now):
+ return self._root_channel.get_jobs_to_run(now)
+
+ def get_wakeup_time(self):
+ return self._root_channel.get_wakeup_time()
diff --git a/odex30_base/queue_job/jobrunner/runner.py b/odex30_base/queue_job/jobrunner/runner.py
new file mode 100644
index 0000000..a1aa70a
--- /dev/null
+++ b/odex30_base/queue_job/jobrunner/runner.py
@@ -0,0 +1,625 @@
+# Copyright (c) 2015-2016 ACSONE SA/NV ()
+# Copyright 2015-2016 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+"""
+What is the job runner?
+-----------------------
+The job runner is the main process managing the dispatch of delayed jobs to
+available Odoo workers
+
+How does it work?
+-----------------
+
+* It starts as a thread in the Odoo main process or as a new worker
+* It receives postgres NOTIFY messages each time jobs are
+ added or updated in the queue_job table.
+* It maintains an in-memory priority queue of jobs that
+ is populated from the queue_job tables in all databases.
+* It does not run jobs itself, but asks Odoo to run them through an
+ anonymous ``/queue_job/runjob`` HTTP request. [1]_
+
+How to use it?
+--------------
+
+* Optionally adjust your configuration through environment variables:
+
+ - ``ODOO_QUEUE_JOB_CHANNELS=root:4`` (or any other channels
+ configuration), default ``root:1``.
+ - ``ODOO_QUEUE_JOB_SCHEME=https``, default ``http``.
+ - ``ODOO_QUEUE_JOB_HOST=load-balancer``, default ``http_interface``
+ or ``localhost`` if unset.
+ - ``ODOO_QUEUE_JOB_PORT=443``, default ``http_port`` or 8069 if unset.
+ - ``ODOO_QUEUE_JOB_HTTP_AUTH_USER=jobrunner``, default empty.
+ - ``ODOO_QUEUE_JOB_HTTP_AUTH_PASSWORD=s3cr3t``, default empty.
+ - ``ODOO_QUEUE_JOB_JOBRUNNER_DB_HOST=master-db``, default ``db_host``
+ or ``False`` if unset.
+ - ``ODOO_QUEUE_JOB_JOBRUNNER_DB_PORT=5432``, default ``db_port``
+ or ``False`` if unset.
+ - ``ODOO_QUEUE_JOB_JOBRUNNER_DB_USER=userdb``, default ``db_user``
+ or ``False`` if unset.
+ - ``ODOO_QUEUE_JOB_JOBRUNNER_DB_PASSWORD=passdb``, default ``db_password``
+ or ``False`` if unset.
+
+* Alternatively, configure the channels through the Odoo configuration
+ file, like:
+
+.. code-block:: ini
+
+ [queue_job]
+ channels = root:4
+ scheme = https
+ host = load-balancer
+ port = 443
+ http_auth_user = jobrunner
+ http_auth_password = s3cr3t
+ jobrunner_db_host = master-db
+ jobrunner_db_port = 5432
+ jobrunner_db_user = userdb
+ jobrunner_db_password = passdb
+
+* Or, if using ``anybox.recipe.odoo``, add this to your buildout configuration:
+
+.. code-block:: ini
+
+ [odoo]
+ recipe = anybox.recipe.odoo
+ (...)
+ queue_job.channels = root:4
+ queue_job.scheme = https
+ queue_job.host = load-balancer
+ queue_job.port = 443
+ queue_job.http_auth_user = jobrunner
+ queue_job.http_auth_password = s3cr3t
+
+* Start Odoo with ``--load=web,web_kanban,queue_job``
+ and ``--workers`` greater than 1 [2]_, or set the ``server_wide_modules``
+ option in The Odoo configuration file:
+
+.. code-block:: ini
+
+ [options]
+ (...)
+ workers = 4
+ server_wide_modules = web,web_kanban,queue_job
+ (...)
+
+* Or, if using ``anybox.recipe.odoo``:
+
+.. code-block:: ini
+
+ [odoo]
+ recipe = anybox.recipe.odoo
+ (...)
+ options.workers = 4
+ options.server_wide_modules = web,web_kanban,queue_job
+
+* Confirm the runner is starting correctly by checking the odoo log file:
+
+.. code-block:: none
+
+ ...INFO...queue_job.jobrunner.runner: starting
+ ...INFO...queue_job.jobrunner.runner: initializing database connections
+ ...INFO...queue_job.jobrunner.runner: queue job runner ready for db
+ ...INFO...queue_job.jobrunner.runner: database connections ready
+
+* Create jobs (eg using base_import_async) and observe they
+ start immediately and in parallel.
+
+* Tip: to enable debug logging for the queue job, use
+ ``--log-handler=odoo.addons.queue_job:DEBUG``
+
+Caveat
+------
+
+* After creating a new database or installing queue_job on an
+ existing database, Odoo must be restarted for the runner to detect it.
+
+.. rubric:: Footnotes
+
+.. [1] From a security standpoint, it is safe to have an anonymous HTTP
+ request because this request only accepts to run jobs that are
+ enqueued.
+.. [2] It works with the threaded Odoo server too, although this way
+ of running Odoo is obviously not for production purposes.
+"""
+
+import logging
+import os
+import selectors
+import threading
+import time
+from contextlib import closing, contextmanager
+
+import psycopg2
+import requests
+from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
+
+import odoo
+from odoo.tools import config
+
+from . import queue_job_config
+from .channels import ENQUEUED, NOT_DONE, ChannelManager
+
+SELECT_TIMEOUT = 60
+ERROR_RECOVERY_DELAY = 5
+PG_ADVISORY_LOCK_ID = 2293787760715711918
+
+_logger = logging.getLogger(__name__)
+
+select = selectors.DefaultSelector
+
+
+class MasterElectionLost(Exception):
+ pass
+
+
+# Unfortunately, it is not possible to extend the Odoo
+# server command line arguments, so we resort to environment variables
+# to configure the runner (channels mostly).
+#
+# On the other hand, the odoo configuration file can be extended at will,
+# so we check it in addition to the environment variables.
+
+
+def _channels():
+ return (
+ os.environ.get("ODOO_QUEUE_JOB_CHANNELS")
+ or queue_job_config.get("channels")
+ or "root:1"
+ )
+
+
+def _odoo_now():
+ # important: this must return the same as postgresql
+ # EXTRACT(EPOCH FROM TIMESTAMP dt)
+ return time.time()
+
+
+def _connection_info_for(db_name):
+ db_or_uri, connection_info = odoo.sql_db.connection_info_for(db_name)
+
+ for p in ("host", "port", "user", "password"):
+ cfg = os.environ.get(
+ f"ODOO_QUEUE_JOB_JOBRUNNER_DB_{p.upper()}"
+ ) or queue_job_config.get("jobrunner_db_" + p)
+
+ if cfg:
+ connection_info[p] = cfg
+
+ return connection_info
+
+
+def _async_http_get(scheme, host, port, user, password, db_name, job_uuid):
+ # TODO: better way to HTTP GET asynchronously (grequest, ...)?
+ # if this was python3 I would be doing this with
+ # asyncio, aiohttp and aiopg
+ def urlopen():
+ url = f"{scheme}://{host}:{port}/queue_job/runjob?db={db_name}&job_uuid={job_uuid}"
+ # pylint: disable=except-pass
+ try:
+ auth = None
+ if user:
+ auth = (user, password)
+ # we are not interested in the result, so we set a short timeout
+ # but not too short so we trap and log hard configuration errors
+ response = requests.get(url, timeout=1, auth=auth)
+
+ # raise_for_status will result in either nothing, a Client Error
+ # for HTTP Response codes between 400 and 500 or a Server Error
+ # for codes between 500 and 600
+ response.raise_for_status()
+ except requests.Timeout:
+ # A timeout is a normal behaviour, it shouldn't be logged as an exception
+ pass
+ except Exception:
+ _logger.exception("exception in GET %s", url)
+
+ thread = threading.Thread(target=urlopen)
+ thread.daemon = True
+ thread.start()
+
+
+class Database:
+ def __init__(self, db_name):
+ self.db_name = db_name
+ connection_info = _connection_info_for(db_name)
+ self.conn = psycopg2.connect(**connection_info)
+ try:
+ self.conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
+ self.has_queue_job = self._has_queue_job()
+ if self.has_queue_job:
+ self._acquire_master_lock()
+ self._initialize()
+ except BaseException:
+ self.close()
+ raise
+
+ def close(self):
+ # pylint: disable=except-pass
+ # if close fail for any reason, it's either because it's already closed
+ # and we don't care, or for any reason but anyway it will be closed on
+ # del
+ try:
+ self.conn.close()
+ except Exception:
+ pass
+ self.conn = None
+
+ def _acquire_master_lock(self):
+ """Acquire the master runner lock or raise MasterElectionLost"""
+ with closing(self.conn.cursor()) as cr:
+ cr.execute("SELECT pg_try_advisory_lock(%s)", (PG_ADVISORY_LOCK_ID,))
+ if not cr.fetchone()[0]:
+ msg = f"could not acquire master runner lock on {self.db_name}"
+ raise MasterElectionLost(msg)
+
+ def _has_queue_job(self):
+ with closing(self.conn.cursor()) as cr:
+ cr.execute(
+ "SELECT 1 FROM pg_tables WHERE tablename=%s", ("ir_module_module",)
+ )
+ if not cr.fetchone():
+ _logger.debug("%s doesn't seem to be an odoo db", self.db_name)
+ return False
+ cr.execute(
+ "SELECT 1 FROM ir_module_module WHERE name=%s AND state=%s",
+ ("queue_job", "installed"),
+ )
+ if not cr.fetchone():
+ _logger.debug("queue_job is not installed for db %s", self.db_name)
+ return False
+ cr.execute(
+ """SELECT COUNT(1)
+ FROM information_schema.triggers
+ WHERE event_object_table = %s
+ AND trigger_name = %s""",
+ ("queue_job", "queue_job_notify"),
+ )
+ if cr.fetchone()[0] != 3: # INSERT, DELETE, UPDATE
+ _logger.error(
+ "queue_job_notify trigger is missing in db %s", self.db_name
+ )
+ return False
+ return True
+
+ def _initialize(self):
+ with closing(self.conn.cursor()) as cr:
+ cr.execute("LISTEN queue_job")
+
+ @contextmanager
+ def select_jobs(self, where, args):
+ # pylint: disable=sql-injection
+ # the checker thinks we are injecting values but we are not, we are
+ # adding the where conditions, values are added later properly with
+ # parameters
+ query = (
+ "SELECT channel, uuid, id as seq, date_created, "
+ "priority, EXTRACT(EPOCH FROM eta), state "
+ f"FROM queue_job WHERE {where}"
+ )
+ with closing(self.conn.cursor("select_jobs", withhold=True)) as cr:
+ cr.execute(query, args)
+ yield cr
+
+ def keep_alive(self):
+ query = "SELECT 1"
+ with closing(self.conn.cursor()) as cr:
+ cr.execute(query)
+
+ def set_job_enqueued(self, uuid):
+ with closing(self.conn.cursor()) as cr:
+ cr.execute(
+ "UPDATE queue_job SET state=%s, "
+ "date_enqueued=date_trunc('seconds', "
+ " now() at time zone 'utc') "
+ "WHERE uuid=%s",
+ (ENQUEUED, uuid),
+ )
+
+ def _query_requeue_dead_jobs(self):
+ return """
+ UPDATE
+ queue_job
+ SET
+ state=(
+ CASE
+ WHEN
+ max_retries IS NOT NULL AND
+ max_retries != 0 AND -- infinite retries if max_retries is 0
+ retry IS NOT NULL AND
+ retry>max_retries
+ THEN 'failed'
+ ELSE 'pending'
+ END),
+ retry=(
+ CASE
+ WHEN state='started'
+ THEN COALESCE(retry,0)+1 ELSE retry
+ END),
+ exc_name=(
+ CASE
+ WHEN
+ max_retries IS NOT NULL AND
+ max_retries != 0 AND -- infinite retries if max_retries is 0
+ retry IS NOT NULL AND
+ retry>max_retries
+ THEN 'JobFoundDead'
+ ELSE exc_name
+ END),
+ exc_info=(
+ CASE
+ WHEN
+ max_retries IS NOT NULL AND
+ max_retries != 0 AND -- infinite retries if max_retries is 0
+ retry IS NOT NULL AND
+ retry>max_retries
+ THEN 'Job found dead after too many retries'
+ ELSE exc_info
+ END)
+ WHERE
+ id in (
+ SELECT
+ queue_job_id
+ FROM
+ queue_job_lock
+ WHERE
+ queue_job_id in (
+ SELECT
+ id
+ FROM
+ queue_job
+ WHERE
+ state IN ('enqueued','started')
+ AND date_enqueued <
+ (now() AT TIME ZONE 'utc' - INTERVAL '10 sec')
+ )
+ FOR UPDATE SKIP LOCKED
+ )
+ RETURNING uuid
+ """
+
+ def requeue_dead_jobs(self):
+ """
+ Set started and enqueued jobs but not locked to pending
+
+ A job is locked when it's being executed
+ When a job is killed, it releases the lock
+
+ If the number of retries exceeds the number of max retries,
+ the job is set as 'failed' with the error 'JobFoundDead'.
+
+ Adding a buffer on 'date_enqueued' to check
+ that it has been enqueued for more than 10sec.
+ This prevents from requeuing jobs before they are actually started.
+
+ When Odoo shuts down normally, it waits for running jobs to finish.
+ However, when the Odoo server crashes or is otherwise force-stopped,
+ running jobs are interrupted while the runner has no chance to know
+ they have been aborted.
+ """
+
+ with closing(self.conn.cursor()) as cr:
+ query = self._query_requeue_dead_jobs()
+
+ cr.execute(query)
+
+ for (uuid,) in cr.fetchall():
+ _logger.warning("Re-queued dead job with uuid: %s", uuid)
+
+
+class QueueJobRunner:
+ def __init__(
+ self,
+ scheme="http",
+ host="localhost",
+ port=8069,
+ user=None,
+ password=None,
+ channel_config_string=None,
+ ):
+ self.scheme = scheme
+ self.host = host
+ self.port = port
+ self.user = user
+ self.password = password
+ self.channel_manager = ChannelManager()
+ if channel_config_string is None:
+ channel_config_string = _channels()
+ self.channel_manager.simple_configure(channel_config_string)
+ self.db_by_name = {}
+ self._stop = False
+ self._stop_pipe = os.pipe()
+
+ def __del__(self):
+ # pylint: disable=except-pass
+ try:
+ os.close(self._stop_pipe[0])
+ except OSError:
+ pass
+ try:
+ os.close(self._stop_pipe[1])
+ except OSError:
+ pass
+
+ @classmethod
+ def from_environ_or_config(cls):
+ scheme = os.environ.get("ODOO_QUEUE_JOB_SCHEME") or queue_job_config.get(
+ "scheme"
+ )
+ host = (
+ os.environ.get("ODOO_QUEUE_JOB_HOST")
+ or queue_job_config.get("host")
+ or config["http_interface"]
+ )
+ port = (
+ os.environ.get("ODOO_QUEUE_JOB_PORT")
+ or queue_job_config.get("port")
+ or config["http_port"]
+ )
+ user = os.environ.get("ODOO_QUEUE_JOB_HTTP_AUTH_USER") or queue_job_config.get(
+ "http_auth_user"
+ )
+ password = os.environ.get(
+ "ODOO_QUEUE_JOB_HTTP_AUTH_PASSWORD"
+ ) or queue_job_config.get("http_auth_password")
+ runner = cls(
+ scheme=scheme or "http",
+ host=host or "localhost",
+ port=port or 8069,
+ user=user,
+ password=password,
+ )
+ return runner
+
+ def get_db_names(self):
+ if config["db_name"]:
+ db_names = config["db_name"].split(",")
+ else:
+ db_names = odoo.service.db.list_dbs(True)
+ return db_names
+
+ def close_databases(self, remove_jobs=True):
+ for db_name, db in self.db_by_name.items():
+ try:
+ if remove_jobs:
+ self.channel_manager.remove_db(db_name)
+ db.close()
+ except Exception:
+ _logger.warning("error closing database %s", db_name, exc_info=True)
+ self.db_by_name = {}
+
+ def initialize_databases(self):
+ for db_name in sorted(self.get_db_names()):
+ # sorting is important to avoid deadlocks in acquiring the master lock
+ db = Database(db_name)
+ if db.has_queue_job:
+ self.db_by_name[db_name] = db
+ with db.select_jobs("state in %s", (NOT_DONE,)) as cr:
+ for job_data in cr:
+ self.channel_manager.notify(db_name, *job_data)
+ _logger.info("queue job runner ready for db %s", db_name)
+ else:
+ db.close()
+
+ def requeue_dead_jobs(self):
+ for db in self.db_by_name.values():
+ if db.has_queue_job:
+ db.requeue_dead_jobs()
+
+ def run_jobs(self):
+ now = _odoo_now()
+ for job in self.channel_manager.get_jobs_to_run(now):
+ if self._stop:
+ break
+ _logger.info("asking Odoo to run job %s on db %s", job.uuid, job.db_name)
+ self.db_by_name[job.db_name].set_job_enqueued(job.uuid)
+ _async_http_get(
+ self.scheme,
+ self.host,
+ self.port,
+ self.user,
+ self.password,
+ job.db_name,
+ job.uuid,
+ )
+
+ def process_notifications(self):
+ for db in self.db_by_name.values():
+ if not db.conn.notifies:
+ # If there are no activity in the queue_job table it seems that
+ # tcp keepalives are not sent (in that very specific scenario),
+ # causing some intermediaries (such as haproxy) to close the
+ # connection, making the jobrunner to restart on a socket error
+ db.keep_alive()
+ while db.conn.notifies:
+ if self._stop:
+ break
+ notification = db.conn.notifies.pop()
+ uuid = notification.payload
+ with db.select_jobs("uuid = %s", (uuid,)) as cr:
+ job_datas = cr.fetchone()
+ if job_datas:
+ self.channel_manager.notify(db.db_name, *job_datas)
+ else:
+ self.channel_manager.remove_job(uuid)
+
+ def wait_notification(self):
+ for db in self.db_by_name.values():
+ if db.conn.notifies:
+ # something is going on in the queue, no need to wait
+ return
+ # wait for something to happen in the queue_job tables
+ # we'll select() on database connections and the stop pipe
+ conns = [db.conn for db in self.db_by_name.values()]
+ conns.append(self._stop_pipe[0])
+ # look if the channels specify a wakeup time
+ wakeup_time = self.channel_manager.get_wakeup_time()
+ if not wakeup_time:
+ # this could very well be no timeout at all, because
+ # any activity in the job queue will wake us up, but
+ # let's have a timeout anyway, just to be safe
+ timeout = SELECT_TIMEOUT
+ else:
+ timeout = wakeup_time - _odoo_now()
+ # wait for a notification or a timeout;
+ # if timeout is negative (ie wakeup time in the past),
+ # do not wait; this should rarely happen
+ # because of how get_wakeup_time is designed; actually
+ # if timeout remains a large negative number, it is most
+ # probably a bug
+ _logger.debug("select() timeout: %.2f sec", timeout)
+ if timeout > 0:
+ if conns and not self._stop:
+ with select() as sel:
+ for conn in conns:
+ sel.register(conn, selectors.EVENT_READ)
+ events = sel.select(timeout=timeout)
+ for key, _mask in events:
+ if key.fileobj == self._stop_pipe[0]:
+ # stop-pipe is not a conn so doesn't need poll()
+ continue
+ key.fileobj.poll()
+
+ def stop(self):
+ _logger.info("graceful stop requested")
+ self._stop = True
+ # wakeup the select() in wait_notification
+ os.write(self._stop_pipe[1], b".")
+
+ def run(self):
+ _logger.info("starting")
+ while not self._stop:
+ # outer loop does exception recovery
+ try:
+ _logger.debug("initializing database connections")
+ # TODO: how to detect new databases or databases
+ # on which queue_job is installed after server start?
+ self.initialize_databases()
+ _logger.info("database connections ready")
+ # inner loop does the normal processing
+ while not self._stop:
+ self.requeue_dead_jobs()
+ self.process_notifications()
+ self.run_jobs()
+ self.wait_notification()
+ except KeyboardInterrupt:
+ self.stop()
+ except InterruptedError:
+ # Interrupted system call, i.e. KeyboardInterrupt during select
+ self.stop()
+ except MasterElectionLost as e:
+ _logger.debug(
+ "master election lost: %s, sleeping %ds and retrying",
+ e,
+ ERROR_RECOVERY_DELAY,
+ )
+ self.close_databases()
+ time.sleep(ERROR_RECOVERY_DELAY)
+ except Exception:
+ _logger.exception(
+ "exception: sleeping %ds and retrying", ERROR_RECOVERY_DELAY
+ )
+ self.close_databases()
+ time.sleep(ERROR_RECOVERY_DELAY)
+ self.close_databases(remove_jobs=False)
+ _logger.info("stopped")
diff --git a/odex30_base/queue_job/migrations/18.0.1.0.0/pre-migrate.py b/odex30_base/queue_job/migrations/18.0.1.0.0/pre-migrate.py
new file mode 100644
index 0000000..ce624e1
--- /dev/null
+++ b/odex30_base/queue_job/migrations/18.0.1.0.0/pre-migrate.py
@@ -0,0 +1,29 @@
+from openupgradelib import openupgrade
+
+from odoo.tools import SQL
+
+
+def migrate(cr, version):
+ if not version:
+ return
+
+ # List of tables and their corresponding columns
+ table_column_map = {
+ "queue.job.function": ["retry_pattern", "related_action"],
+ "queue.job": ["records", "args", "kwargs"],
+ }
+
+ for table, columns in table_column_map.items():
+ for column in columns:
+ if openupgrade.column_exists(cr, table, column):
+ cr.execute(
+ SQL(
+ """
+ UPDATE %(table)s
+ SET %(column)s = %(column)s::jsonb
+ WHERE %(column)s IS NOT NULL
+ """,
+ table=SQL.identifier(table),
+ column=SQL.identifier(column),
+ )
+ )
diff --git a/odex30_base/queue_job/migrations/18.0.1.7.0/pre-migration.py b/odex30_base/queue_job/migrations/18.0.1.7.0/pre-migration.py
new file mode 100644
index 0000000..931c336
--- /dev/null
+++ b/odex30_base/queue_job/migrations/18.0.1.7.0/pre-migration.py
@@ -0,0 +1,11 @@
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+from openupgradelib import openupgrade
+
+
+@openupgrade.migrate()
+def migrate(env, version):
+ # Remove cron garbage collector
+ openupgrade.delete_records_safely_by_xml_id(
+ env,
+ ["queue_job.ir_cron_queue_job_garbage_collector"],
+ )
diff --git a/odex30_base/queue_job/models/__init__.py b/odex30_base/queue_job/models/__init__.py
new file mode 100644
index 0000000..6265dfe
--- /dev/null
+++ b/odex30_base/queue_job/models/__init__.py
@@ -0,0 +1,6 @@
+from . import base
+from . import ir_model_fields
+from . import queue_job
+from . import queue_job_channel
+from . import queue_job_function
+from . import queue_job_lock
diff --git a/odex30_base/queue_job/models/base.py b/odex30_base/queue_job/models/base.py
new file mode 100644
index 0000000..3a68ffd
--- /dev/null
+++ b/odex30_base/queue_job/models/base.py
@@ -0,0 +1,270 @@
+# Copyright 2016 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+import functools
+
+from odoo import api, models
+
+from ..delay import Delayable, DelayableRecordset
+from ..utils import must_run_without_delay
+
+
+class Base(models.AbstractModel):
+ """The base model, which is implicitly inherited by all models.
+
+ A new :meth:`~with_delay` method is added on all Odoo Models, allowing to
+ postpone the execution of a job method in an asynchronous process.
+ """
+
+ _inherit = "base"
+
+ def with_delay(
+ self,
+ priority=None,
+ eta=None,
+ max_retries=None,
+ description=None,
+ channel=None,
+ identity_key=None,
+ ):
+ """Return a ``DelayableRecordset``
+
+ It is a shortcut for the longer form as shown below::
+
+ self.with_delay(priority=20).action_done()
+ # is equivalent to:
+ self.delayable().set(priority=20).action_done().delay()
+
+ ``with_delay()`` accepts job properties which specify how the job will
+ be executed.
+
+ Usage with job properties::
+
+ env['a.model'].with_delay(priority=30, eta=60*60*5).action_done()
+ delayable.export_one_thing(the_thing_to_export)
+ # => the job will be executed with a low priority and not before a
+ # delay of 5 hours from now
+
+ When using :meth:``with_delay``, the final ``delay()`` is implicit.
+ See the documentation of :meth:``delayable`` for more details.
+
+ :return: instance of a DelayableRecordset
+ :rtype: :class:`odoo.addons.queue_job.job.DelayableRecordset`
+ """
+ return DelayableRecordset(
+ self,
+ priority=priority,
+ eta=eta,
+ max_retries=max_retries,
+ description=description,
+ channel=channel,
+ identity_key=identity_key,
+ )
+
+ def delayable(
+ self,
+ priority=None,
+ eta=None,
+ max_retries=None,
+ description=None,
+ channel=None,
+ identity_key=None,
+ ):
+ """Return a ``Delayable``
+
+ The returned instance allows to enqueue any method of the recordset's
+ Model.
+
+ Usage::
+
+ delayable = self.env["res.users"].browse(10).delayable(priority=20)
+ delayable.do_work(name="test"}).delay()
+
+ In this example, the ``do_work`` method will not be executed directly.
+ It will be executed in an asynchronous job.
+
+ Method calls on a Delayable generally return themselves, so calls can
+ be chained together::
+
+ delayable.set(priority=15).do_work(name="test"}).delay()
+
+ The order of the calls that build the job is not relevant, beside
+ the call to ``delay()`` that must happen at the very end. This is
+ equivalent to the example above::
+
+ delayable.do_work(name="test"}).set(priority=15).delay()
+
+ Very importantly, ``delay()`` must be called on the top-most parent
+ of a chain of jobs, so if you have this::
+
+ job1 = record1.delayable().do_work()
+ job2 = record2.delayable().do_work()
+ job1.on_done(job2)
+
+ The ``delay()`` call must be made on ``job1``, otherwise ``job2`` will
+ be delayed, but ``job1`` will never be. When done on ``job1``, the
+ ``delay()`` call will traverse the graph of jobs and delay all of
+ them::
+
+ job1.delay()
+
+ For more details on the graph dependencies, read the documentation of
+ :module:`~odoo.addons.queue_job.delay`.
+
+ :param priority: Priority of the job, 0 being the higher priority.
+ Default is 10.
+ :param eta: Estimated Time of Arrival of the job. It will not be
+ executed before this date/time.
+ :param max_retries: maximum number of retries before giving up and set
+ the job state to 'failed'. A value of 0 means
+ infinite retries. Default is 5.
+ :param description: human description of the job. If None, description
+ is computed from the function doc or name
+ :param channel: the complete name of the channel to use to process
+ the function. If specified it overrides the one
+ defined on the function
+ :param identity_key: key uniquely identifying the job, if specified
+ and a job with the same key has not yet been run,
+ the new job will not be added. It is either a
+ string, either a function that takes the job as
+ argument (see :py:func:`..job.identity_exact`).
+ the new job will not be added.
+ :return: instance of a Delayable
+ :rtype: :class:`odoo.addons.queue_job.job.Delayable`
+ """
+ return Delayable(
+ self,
+ priority=priority,
+ eta=eta,
+ max_retries=max_retries,
+ description=description,
+ channel=channel,
+ identity_key=identity_key,
+ )
+
+ def _patch_job_auto_delay(self, method_name, context_key=None):
+ """Patch a method to be automatically delayed as job method when called
+
+ This patch method has to be called in ``_register_hook`` (example
+ below).
+
+ When a method is patched, any call to the method will not directly
+ execute the method's body, but will instead enqueue a job.
+
+ When a ``context_key`` is set when calling ``_patch_job_auto_delay``,
+ the patched method is automatically delayed only when this key is
+ ``True`` in the caller's context. It is advised to patch the method
+ with a ``context_key``, because making the automatic delay *in any
+ case* can produce nasty and unexpected side effects (e.g. another
+ module calls the method and expects it to be computed before doing
+ something else, expecting a result, ...).
+
+ A typical use case is when a method in a module we don't control is
+ called synchronously in the middle of another method, and we'd like all
+ the calls to this method become asynchronous.
+
+ The options of the job usually passed to ``with_delay()`` (priority,
+ description, identity_key, ...) can be returned in a dictionary by a
+ method named after the name of the method suffixed by ``_job_options``
+ which takes the same parameters as the initial method.
+
+ 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, f"{method_name}_job_options", None)
+ job_options = {}
+ if job_options_method:
+ job_options.update(job_options_method(*args, **kwargs))
+ delayed = self.with_delay(**job_options)
+ return getattr(delayed, method_name)(*args, **kwargs)
+
+ origin = getattr(self, method_name)
+ return functools.update_wrapper(auto_delay_wrapper, origin)
+
+ @api.model
+ def _job_store_values(self, job):
+ """Hook for manipulating job stored values.
+
+ You can define a more specific hook for a job function
+ by defining a method name with this pattern:
+
+ `_queue_job_store_values_${func_name}`
+
+ NOTE: values will be stored only if they match stored fields on `queue.job`.
+
+ :param job: current queue_job.job.Job instance.
+ :return: dictionary for setting job values.
+ """
+ return {}
+
+ @api.model
+ def _job_prepare_context_before_enqueue_keys(self):
+ """Keys to keep in context of stored jobs
+ Empty by default for backward compatibility.
+ """
+ return ("tz", "lang", "allowed_company_ids", "force_company", "active_test")
+
+ def _job_prepare_context_before_enqueue(self):
+ """Return the context to store in the jobs
+ Can be used to keep only safe keys.
+ """
+ return {
+ key: value
+ for key, value in self.env.context.items()
+ if key in self._job_prepare_context_before_enqueue_keys()
+ }
+
+ @classmethod
+ def _patch_method(cls, name, method):
+ origin = getattr(cls, name)
+ method.origin = origin
+ # propagate decorators from origin to method, and apply api decorator
+ wrapped = api.propagate(origin, method)
+ wrapped.origin = origin
+ setattr(cls, name, wrapped)
diff --git a/odex30_base/queue_job/models/ir_model_fields.py b/odex30_base/queue_job/models/ir_model_fields.py
new file mode 100644
index 0000000..5a31fcd
--- /dev/null
+++ b/odex30_base/queue_job/models/ir_model_fields.py
@@ -0,0 +1,13 @@
+# Copyright 2020 Camptocamp
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from odoo import fields, models
+
+
+class IrModelFields(models.Model):
+ _inherit = "ir.model.fields"
+
+ ttype = fields.Selection(
+ selection_add=[("job_serialized", "Job Serialized")],
+ ondelete={"job_serialized": "cascade"},
+ )
diff --git a/odex30_base/queue_job/models/queue_job.py b/odex30_base/queue_job/models/queue_job.py
new file mode 100644
index 0000000..411ae43
--- /dev/null
+++ b/odex30_base/queue_job/models/queue_job.py
@@ -0,0 +1,448 @@
+# Copyright 2013-2020 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+import logging
+import random
+from datetime import datetime, timedelta
+
+from odoo import _, api, exceptions, fields, models
+from odoo.tools import config, html_escape, index_exists
+
+from odoo.addons.base_sparse_field.models.fields import Serialized
+
+from ..delay import Graph
+from ..exception import JobError
+from ..fields import JobSerialized
+from ..job import (
+ CANCELLED,
+ DONE,
+ FAILED,
+ PENDING,
+ STARTED,
+ STATES,
+ WAIT_DEPENDENCIES,
+ Job,
+)
+
+_logger = logging.getLogger(__name__)
+
+
+class QueueJob(models.Model):
+ """Model storing the jobs to be executed."""
+
+ _name = "queue.job"
+ _description = "Queue Job"
+ _inherit = ["mail.thread", "mail.activity.mixin"]
+ _log_access = False
+
+ _order = "date_created DESC, date_done DESC"
+
+ _removal_interval = 30 # days
+ _default_related_action = "related_action_open_record"
+
+ # This must be passed in a context key "_job_edit_sentinel" to write on
+ # protected fields. It protects against crafting "queue.job" records from
+ # RPC (e.g. on internal methods). When ``with_delay`` is used, the sentinel
+ # is set.
+ EDIT_SENTINEL = object()
+ _protected_fields = (
+ "uuid",
+ "name",
+ "date_created",
+ "model_name",
+ "method_name",
+ "func_string",
+ "channel_method_name",
+ "job_function_id",
+ "records",
+ "args",
+ "kwargs",
+ )
+
+ uuid = fields.Char(string="UUID", readonly=True, index=True, required=True)
+ graph_uuid = fields.Char(
+ string="Graph UUID",
+ readonly=True,
+ index=True,
+ help="Single shared identifier of a Graph. Empty for a single job.",
+ )
+ user_id = fields.Many2one(comodel_name="res.users", string="User ID")
+ company_id = fields.Many2one(
+ comodel_name="res.company", string="Company", index=True
+ )
+ name = fields.Char(string="Description", readonly=True)
+
+ model_name = fields.Char(string="Model", readonly=True)
+ method_name = fields.Char(readonly=True)
+ records = JobSerialized(
+ string="Record(s)",
+ readonly=True,
+ base_type=models.BaseModel,
+ )
+ dependencies = Serialized(readonly=True)
+ # dependency graph as expected by the field widget
+ dependency_graph = Serialized(compute="_compute_dependency_graph")
+ graph_jobs_count = fields.Integer(compute="_compute_graph_jobs_count")
+ args = JobSerialized(readonly=True, base_type=tuple)
+ kwargs = JobSerialized(readonly=True, base_type=dict)
+ func_string = fields.Char(string="Task", readonly=True)
+
+ state = fields.Selection(STATES, readonly=True, required=True, index=True)
+ priority = fields.Integer(aggregator=False)
+ exc_name = fields.Char(string="Exception", readonly=True)
+ exc_message = fields.Char(string="Exception Message", readonly=True, tracking=True)
+ exc_info = fields.Text(string="Exception Info", readonly=True)
+ result = fields.Text(readonly=True)
+
+ date_created = fields.Datetime(string="Created Date", readonly=True)
+ date_started = fields.Datetime(string="Start Date", readonly=True)
+ date_enqueued = fields.Datetime(string="Enqueue Time", readonly=True)
+ date_done = fields.Datetime(readonly=True)
+ exec_time = fields.Float(
+ string="Execution Time (avg)",
+ aggregator="avg",
+ help="Time required to execute this job in seconds. Average when grouped.",
+ )
+ date_cancelled = fields.Datetime(readonly=True)
+
+ eta = fields.Datetime(string="Execute only after")
+ retry = fields.Integer(string="Current try")
+ max_retries = fields.Integer(
+ string="Max. retries",
+ help="The job will fail if the number of tries reach the "
+ "max. retries.\n"
+ "Retries are infinite when empty.",
+ )
+ # FIXME the name of this field is very confusing
+ channel_method_name = fields.Char(string="Complete Method Name", readonly=True)
+ job_function_id = fields.Many2one(
+ comodel_name="queue.job.function",
+ string="Job Function",
+ readonly=True,
+ )
+
+ channel = fields.Char(index=True)
+
+ identity_key = fields.Char(readonly=True)
+ worker_pid = fields.Integer(readonly=True)
+
+ def init(self):
+ index_1 = "queue_job_identity_key_state_partial_index"
+ index_2 = "queue_job_channel_date_done_date_created_index"
+ if not index_exists(self._cr, index_1):
+ # Used by Job.job_record_with_same_identity_key
+ self._cr.execute(
+ "CREATE INDEX queue_job_identity_key_state_partial_index "
+ "ON queue_job (identity_key) WHERE state in ('pending', "
+ "'enqueued', 'wait_dependencies') AND identity_key IS NOT NULL;"
+ )
+ if not index_exists(self._cr, index_2):
+ # Used by .autovacuum
+ self._cr.execute(
+ "CREATE INDEX queue_job_channel_date_done_date_created_index "
+ "ON queue_job (channel, date_done, date_created);"
+ )
+
+ @api.depends("dependencies")
+ def _compute_dependency_graph(self):
+ jobs_groups = self.env["queue.job"].read_group(
+ [
+ (
+ "graph_uuid",
+ "in",
+ [uuid for uuid in self.mapped("graph_uuid") if uuid],
+ )
+ ],
+ ["graph_uuid", "ids:array_agg(id)"],
+ ["graph_uuid"],
+ )
+ ids_per_graph_uuid = {
+ group["graph_uuid"]: group["ids"] for group in jobs_groups
+ }
+ for record in self:
+ if not record.graph_uuid:
+ record.dependency_graph = {}
+ continue
+
+ graph_jobs = self.browse(ids_per_graph_uuid.get(record.graph_uuid) or [])
+ if not graph_jobs:
+ record.dependency_graph = {}
+ continue
+
+ graph_ids = {graph_job.uuid: graph_job.id for graph_job in graph_jobs}
+ graph_jobs_by_ids = {graph_job.id: graph_job for graph_job in graph_jobs}
+
+ graph = Graph()
+ for graph_job in graph_jobs:
+ graph.add_vertex(graph_job.id)
+ for parent_uuid in graph_job.dependencies["depends_on"]:
+ parent_id = graph_ids.get(parent_uuid)
+ if not parent_id:
+ continue
+ graph.add_edge(parent_id, graph_job.id)
+ for child_uuid in graph_job.dependencies["reverse_depends_on"]:
+ child_id = graph_ids.get(child_uuid)
+ if not child_id:
+ continue
+ graph.add_edge(graph_job.id, child_id)
+
+ record.dependency_graph = {
+ # list of ids
+ "nodes": [
+ graph_jobs_by_ids[graph_id]._dependency_graph_vis_node()
+ for graph_id in graph.vertices()
+ ],
+ # list of tuples (from, to)
+ "edges": graph.edges(),
+ }
+
+ def _dependency_graph_vis_node(self):
+ """Return the node as expected by the JobDirectedGraph widget"""
+ default = ("#D2E5FF", "#2B7CE9")
+ colors = {
+ DONE: ("#C2FABC", "#4AD63A"),
+ FAILED: ("#FB7E81", "#FA0A10"),
+ STARTED: ("#FFFF00", "#FFA500"),
+ }
+ return {
+ "id": self.id,
+ "title": (
+ f"{html_escape(self.display_name)} "
+ f"{html_escape(self.func_string)}"
+ ),
+ "color": colors.get(self.state, default)[0],
+ "border": colors.get(self.state, default)[1],
+ "shadow": True,
+ }
+
+ def _compute_graph_jobs_count(self):
+ jobs_groups = self.env["queue.job"].read_group(
+ [
+ (
+ "graph_uuid",
+ "in",
+ [uuid for uuid in self.mapped("graph_uuid") if uuid],
+ )
+ ],
+ ["graph_uuid"],
+ ["graph_uuid"],
+ )
+ count_per_graph_uuid = {
+ group["graph_uuid"]: group["graph_uuid_count"] for group in jobs_groups
+ }
+ for record in self:
+ record.graph_jobs_count = count_per_graph_uuid.get(record.graph_uuid) or 0
+
+ @api.model_create_multi
+ @api.private
+ def create(self, 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:
+ write_on_protected_fields = [
+ fieldname for fieldname in vals if fieldname in self._protected_fields
+ ]
+ if write_on_protected_fields:
+ raise exceptions.AccessError(
+ _("Not allowed to change field(s): {}").format(
+ write_on_protected_fields
+ )
+ )
+
+ different_user_jobs = self.browse()
+ if vals.get("user_id"):
+ different_user_jobs = self.filtered(
+ lambda records: records.env.user.id != vals["user_id"]
+ )
+
+ if vals.get("state") == "failed":
+ self._message_post_on_failure()
+
+ result = super().write(vals)
+
+ for record in different_user_jobs:
+ # the user is stored in the env of the record, but we still want to
+ # have a stored user_id field to be able to search/groupby, so
+ # synchronize the env of records with user_id
+ super(QueueJob, record).write(
+ {"records": record.records.with_user(vals["user_id"])}
+ )
+ return result
+
+ def open_related_action(self):
+ """Open the related action associated to the job"""
+ self.ensure_one()
+ job = Job.load(self.env, self.uuid)
+ action = job.related_action()
+ if action is None:
+ raise exceptions.UserError(_("No action available for this job"))
+ return action
+
+ def open_graph_jobs(self):
+ """Return action that opens all jobs of the same graph"""
+ self.ensure_one()
+ jobs = self.env["queue.job"].search([("graph_uuid", "=", self.graph_uuid)])
+
+ action = self.env["ir.actions.act_window"]._for_xml_id(
+ "queue_job.action_queue_job"
+ )
+ action.update(
+ {
+ "name": _("Jobs for graph %s") % (self.graph_uuid),
+ "context": {},
+ "domain": [("id", "in", jobs.ids)],
+ }
+ )
+ return action
+
+ def _change_job_state(self, state, result=None):
+ """Change the state of the `Job` object
+
+ Changing the state of the Job will automatically change some fields
+ (date, result, ...).
+ """
+ for record in self:
+ job_ = Job.load(record.env, record.uuid)
+ if state == DONE:
+ job_.set_done(result=result)
+ job_.store()
+ record.env["queue.job"].flush_model()
+ job_.enqueue_waiting()
+ elif state == PENDING:
+ job_.set_pending(result=result)
+ job_.store()
+ elif state == CANCELLED:
+ job_.set_cancelled(result=result)
+ job_.store()
+ record.env["queue.job"].flush_model()
+ job_.cancel_dependent_jobs()
+ else:
+ raise ValueError(f"State not supported: {state}")
+
+ def button_done(self):
+ result = _("Manually set to done by {}").format(self.env.user.name)
+ self._change_job_state(DONE, result=result)
+ return True
+
+ def button_cancelled(self):
+ result = _("Cancelled by {}").format(self.env.user.name)
+ self._change_job_state(CANCELLED, result=result)
+ return True
+
+ def requeue(self):
+ jobs_to_requeue = self.filtered(lambda job_: job_.state != WAIT_DEPENDENCIES)
+ jobs_to_requeue._change_job_state(PENDING)
+ return 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()
+ base_users = self.env["res.users"].search(domain)
+ for record in self:
+ users = base_users | record.user_id
+ record.message_subscribe(partner_ids=users.mapped("partner_id").ids)
+ msg = record._message_failed_job()
+ if msg:
+ record.message_post(body=msg, subtype_xmlid="queue_job.mt_job_failed")
+
+ def _subscribe_users_domain(self):
+ """Subscribe all users having the 'Queue Job Manager' group"""
+ group = self.env.ref("queue_job.group_queue_job_manager")
+ if not group:
+ return None
+ companies = self.mapped("company_id")
+ domain = [("groups_id", "=", group.id)]
+ if companies:
+ domain.append(("company_id", "in", companies.ids))
+ return domain
+
+ def _message_failed_job(self):
+ """Return a message which will be posted on the job when it is failed.
+
+ It can be inherited to allow more precise messages based on the
+ exception informations.
+
+ If nothing is returned, no message will be posted.
+ """
+ self.ensure_one()
+ return _(
+ "Something bad happened during the execution of the job. "
+ "More details in the 'Exception Information' section."
+ )
+
+ def _needaction_domain_get(self):
+ """Returns the domain to filter records that require an action
+
+ :return: domain or False is no action
+ """
+ return [("state", "=", "failed")]
+
+ def autovacuum(self):
+ """Delete all jobs done based on the removal interval defined on the
+ channel
+
+ Called from a cron.
+ """
+ for channel in self.env["queue.job.channel"].search([]):
+ deadline = datetime.now() - timedelta(days=int(channel.removal_interval))
+ while True:
+ jobs = self.search(
+ [
+ "|",
+ ("date_done", "<=", deadline),
+ ("date_cancelled", "<=", deadline),
+ ("channel", "=", channel.complete_name),
+ ],
+ order="date_done, date_created",
+ limit=1000,
+ )
+ if jobs:
+ jobs.unlink()
+ if not config["test_enable"]:
+ self.env.cr.commit() # pylint: disable=E8102
+ else:
+ break
+ return True
+
+ def related_action_open_record(self):
+ """Open a form view with the record(s) of the job.
+
+ For instance, for a job on a ``product.product``, it will open a
+ ``product.product`` form view with the product record(s) concerned by
+ the job. If the job concerns more than one record, it opens them in a
+ list.
+
+ This is the default related action.
+
+ """
+ self.ensure_one()
+ records = self.records.exists()
+ if not records:
+ return None
+ action = {
+ "name": _("Related Record"),
+ "type": "ir.actions.act_window",
+ "view_mode": "form",
+ "res_model": records._name,
+ }
+ if len(records) == 1:
+ action["res_id"] = records.id
+ else:
+ action.update(
+ {
+ "name": _("Related Records"),
+ "view_mode": "list,form",
+ "domain": [("id", "in", records.ids)],
+ }
+ )
+ return action
+
+ def _test_job(self, failure_rate=0):
+ _logger.info("Running test job.")
+ if random.random() <= failure_rate:
+ raise JobError("Job failed")
diff --git a/odex30_base/queue_job/models/queue_job_channel.py b/odex30_base/queue_job/models/queue_job_channel.py
new file mode 100644
index 0000000..4aabb01
--- /dev/null
+++ b/odex30_base/queue_job/models/queue_job_channel.py
@@ -0,0 +1,89 @@
+# Copyright 2013-2020 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+
+from odoo import _, api, exceptions, fields, models
+
+
+class QueueJobChannel(models.Model):
+ _name = "queue.job.channel"
+ _description = "Job Channels"
+ _rec_name = "complete_name"
+
+ name = fields.Char()
+ complete_name = fields.Char(
+ compute="_compute_complete_name", store=True, readonly=True, recursive=True
+ )
+ parent_id = fields.Many2one(
+ comodel_name="queue.job.channel", string="Parent Channel", ondelete="restrict"
+ )
+ job_function_ids = fields.One2many(
+ comodel_name="queue.job.function",
+ inverse_name="channel_id",
+ string="Job Functions",
+ )
+ removal_interval = fields.Integer(
+ default=lambda self: self.env["queue.job"]._removal_interval, required=True
+ )
+
+ _sql_constraints = [
+ ("name_uniq", "unique(complete_name)", "Channel complete name must be unique")
+ ]
+
+ @api.depends("name", "parent_id.complete_name")
+ def _compute_complete_name(self):
+ for record in self:
+ if not record.name:
+ complete_name = "" # new record
+ elif record.parent_id:
+ complete_name = ".".join([record.parent_id.complete_name, record.name])
+ else:
+ complete_name = record.name
+ record.complete_name = complete_name
+
+ @api.constrains("parent_id", "name")
+ def parent_required(self):
+ for record in self:
+ if record.name != "root" and not record.parent_id:
+ raise exceptions.ValidationError(_("Parent channel required."))
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = self.browse()
+ if self.env.context.get("install_mode"):
+ # installing a module that creates a channel: rebinds the channel
+ # to an existing one (likely we already had the channel created by
+ # the @job decorator previously)
+ new_vals_list = []
+ for vals in vals_list:
+ name = vals.get("name")
+ parent_id = vals.get("parent_id")
+ if name and parent_id:
+ existing = self.search(
+ [("name", "=", name), ("parent_id", "=", parent_id)]
+ )
+ if existing:
+ if not existing.get_metadata()[0].get("noupdate"):
+ existing.write(vals)
+ records |= existing
+ continue
+ new_vals_list.append(vals)
+ vals_list = new_vals_list
+ records |= super().create(vals_list)
+ return records
+
+ def write(self, values):
+ for channel in self:
+ if (
+ not self.env.context.get("install_mode")
+ and channel.name == "root"
+ and ("name" in values or "parent_id" in values)
+ ):
+ raise exceptions.UserError(_("Cannot change the root channel"))
+ return super().write(values)
+
+ def unlink(self):
+ for channel in self:
+ if channel.name == "root":
+ raise exceptions.UserError(_("Cannot remove the root channel"))
+ return super().unlink()
diff --git a/odex30_base/queue_job/models/queue_job_function.py b/odex30_base/queue_job/models/queue_job_function.py
new file mode 100644
index 0000000..7cf73ea
--- /dev/null
+++ b/odex30_base/queue_job/models/queue_job_function.py
@@ -0,0 +1,273 @@
+# Copyright 2013-2020 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+import ast
+import logging
+import re
+from collections import namedtuple
+
+from odoo import _, api, exceptions, fields, models, tools
+
+from ..fields import JobSerialized
+
+_logger = logging.getLogger(__name__)
+
+
+regex_job_function_name = re.compile(r"^<([0-9a-z_\.]+)>\.([0-9a-zA-Z_]+)$")
+
+
+class QueueJobFunction(models.Model):
+ _name = "queue.job.function"
+ _description = "Job Functions"
+ _log_access = False
+
+ JobConfig = namedtuple(
+ "JobConfig",
+ "channel "
+ "retry_pattern "
+ "related_action_enable "
+ "related_action_func_name "
+ "related_action_kwargs "
+ "job_function_id ",
+ )
+
+ def _default_channel(self):
+ return self.env.ref("queue_job.channel_root")
+
+ name = fields.Char(
+ compute="_compute_name",
+ inverse="_inverse_name",
+ index=True,
+ store=True,
+ )
+
+ # model and method should be required, but the required flag doesn't
+ # let a chance to _inverse_name to be executed
+ model_id = fields.Many2one(
+ comodel_name="ir.model", string="Model", ondelete="cascade"
+ )
+ method = fields.Char()
+
+ channel_id = fields.Many2one(
+ comodel_name="queue.job.channel",
+ string="Channel",
+ required=True,
+ default=lambda r: r._default_channel(),
+ )
+ channel = fields.Char(related="channel_id.complete_name", store=True, readonly=True)
+ retry_pattern = JobSerialized(string="Retry Pattern (serialized)", base_type=dict)
+ edit_retry_pattern = fields.Text(
+ string="Retry Pattern",
+ compute="_compute_edit_retry_pattern",
+ inverse="_inverse_edit_retry_pattern",
+ help="Pattern expressing from the count of retries on retryable errors,"
+ " the number of of seconds to postpone the next execution. Setting the "
+ "number of seconds to a 2-element tuple or list will randomize the "
+ "retry interval between the 2 values.\n"
+ "Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+ "Example: {1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}.\n"
+ "See the module description for details.",
+ )
+ related_action = JobSerialized(string="Related Action (serialized)", base_type=dict)
+ edit_related_action = fields.Text(
+ string="Related Action",
+ compute="_compute_edit_related_action",
+ inverse="_inverse_edit_related_action",
+ help="The action when the button *Related Action* is used on a job. "
+ "The default action is to open the view of the record related "
+ "to the job. Configured as a dictionary with optional keys: "
+ "enable, func_name, kwargs.\n"
+ "See the module description for details.",
+ )
+
+ @api.depends("model_id.model", "method")
+ def _compute_name(self):
+ for record in self:
+ if not (record.model_id and record.method):
+ record.name = ""
+ continue
+ record.name = self.job_function_name(record.model_id.model, record.method)
+
+ def _inverse_name(self):
+ groups = regex_job_function_name.match(self.name)
+ if not groups:
+ raise exceptions.UserError(_("Invalid job function: {}").format(self.name))
+ model_name = groups[1]
+ method = groups[2]
+ model = (
+ self.env["ir.model"].sudo().search([("model", "=", model_name)], limit=1)
+ )
+ if not model:
+ raise exceptions.UserError(_("Model {} not found").format(model_name))
+ self.model_id = model.id
+ self.method = method
+
+ @api.depends("retry_pattern")
+ def _compute_edit_retry_pattern(self):
+ for record in self:
+ retry_pattern = record._parse_retry_pattern()
+ record.edit_retry_pattern = str(retry_pattern)
+
+ def _inverse_edit_retry_pattern(self):
+ try:
+ edited = (self.edit_retry_pattern or "").strip()
+ if edited:
+ self.retry_pattern = ast.literal_eval(edited)
+ else:
+ self.retry_pattern = {}
+ except (ValueError, TypeError, SyntaxError) as ex:
+ raise exceptions.UserError(
+ self._retry_pattern_format_error_message()
+ ) from ex
+
+ @api.depends("related_action")
+ def _compute_edit_related_action(self):
+ for record in self:
+ record.edit_related_action = str(record.related_action)
+
+ def _inverse_edit_related_action(self):
+ try:
+ edited = (self.edit_related_action or "").strip()
+ if edited:
+ self.related_action = ast.literal_eval(edited)
+ else:
+ self.related_action = {}
+ except (ValueError, TypeError, SyntaxError) as ex:
+ raise exceptions.UserError(
+ self._related_action_format_error_message()
+ ) from ex
+
+ @staticmethod
+ def job_function_name(model_name, method_name):
+ return f"<{model_name}>.{method_name}"
+
+ def job_default_config(self):
+ return self.JobConfig(
+ channel="root",
+ retry_pattern={},
+ related_action_enable=True,
+ related_action_func_name=None,
+ related_action_kwargs={},
+ job_function_id=None,
+ )
+
+ def _parse_retry_pattern(self):
+ try:
+ # as json can't have integers as keys and the field is stored
+ # as json, convert back to int
+ retry_pattern = {}
+ for try_count, postpone_value in self.retry_pattern.items():
+ if isinstance(postpone_value, int):
+ retry_pattern[int(try_count)] = postpone_value
+ else:
+ retry_pattern[int(try_count)] = tuple(postpone_value)
+ except ValueError:
+ _logger.error(
+ "Invalid retry pattern for job function %s,"
+ " keys could not be parsed as integers, fallback"
+ " to the default retry pattern.",
+ self.name,
+ )
+ retry_pattern = {}
+ return retry_pattern
+
+ @tools.ormcache("name")
+ def job_config(self, name):
+ config = self.search([("name", "=", name)], limit=1)
+ if not config:
+ return self.job_default_config()
+ retry_pattern = config._parse_retry_pattern()
+ return self.JobConfig(
+ channel=config.channel,
+ retry_pattern=retry_pattern,
+ related_action_enable=config.related_action.get("enable", True),
+ related_action_func_name=config.related_action.get("func_name"),
+ related_action_kwargs=config.related_action.get("kwargs", {}),
+ job_function_id=config.id,
+ )
+
+ def _retry_pattern_format_error_message(self):
+ return _(
+ "Unexpected format of Retry Pattern for {}.\n"
+ "Example of valid formats:\n"
+ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}\n"
+ "{{1: (1, 10), 5: (11, 20), 10: (21, 30), 15: (100, 300)}}"
+ ).format(self.name)
+
+ @api.constrains("retry_pattern")
+ def _check_retry_pattern(self):
+ for record in self:
+ retry_pattern = record.retry_pattern
+ if not retry_pattern:
+ continue
+
+ all_values = list(retry_pattern) + list(retry_pattern.values())
+ for value in all_values:
+ try:
+ self._retry_value_type_check(value)
+ except ValueError as ex:
+ raise exceptions.UserError(
+ record._retry_pattern_format_error_message()
+ ) from ex
+
+ def _retry_value_type_check(self, value):
+ if isinstance(value, (tuple | list)):
+ if len(value) != 2:
+ raise ValueError
+ [self._retry_value_type_check(element) for element in value]
+ return
+ int(value)
+
+ def _related_action_format_error_message(self):
+ return _(
+ "Unexpected format of Related Action for {}.\n"
+ "Example of valid format:\n"
+ '{{"enable": True, "func_name": "related_action_foo",'
+ ' "kwargs" {{"limit": 10}}}}'
+ ).format(self.name)
+
+ @api.constrains("related_action")
+ def _check_related_action(self):
+ valid_keys = ("enable", "func_name", "kwargs")
+ for record in self:
+ related_action = record.related_action
+ if not related_action:
+ continue
+
+ if any(key not in valid_keys for key in related_action):
+ raise exceptions.UserError(
+ record._related_action_format_error_message()
+ )
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = self.browse()
+ if self.env.context.get("install_mode"):
+ # installing a module that creates a job function: rebinds the record
+ # to an existing one (likely we already had the job function created by
+ # the @job decorator previously)
+ new_vals_list = []
+ for vals in vals_list:
+ name = vals.get("name")
+ if name:
+ existing = self.search([("name", "=", name)], limit=1)
+ if existing:
+ if not existing.get_metadata()[0].get("noupdate"):
+ existing.write(vals)
+ records |= existing
+ continue
+ new_vals_list.append(vals)
+ vals_list = new_vals_list
+ records |= super().create(vals_list)
+ self.env.registry.clear_cache()
+ return records
+
+ def write(self, values):
+ res = super().write(values)
+ self.env.registry.clear_cache()
+ return res
+
+ def unlink(self):
+ res = super().unlink()
+ self.env.registry.clear_cache()
+ return res
diff --git a/odex30_base/queue_job/models/queue_job_lock.py b/odex30_base/queue_job/models/queue_job_lock.py
new file mode 100644
index 0000000..b01c7f3
--- /dev/null
+++ b/odex30_base/queue_job/models/queue_job_lock.py
@@ -0,0 +1,16 @@
+# Copyright 2025 ACSONE SA/NV
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class QueueJobLock(models.Model):
+ _name = "queue.job.lock"
+ _description = "Queue Job Lock"
+
+ queue_job_id = fields.Many2one(
+ comodel_name="queue.job",
+ required=True,
+ ondelete="cascade",
+ index=True,
+ )
diff --git a/odex30_base/queue_job/post_init_hook.py b/odex30_base/queue_job/post_init_hook.py
new file mode 100644
index 0000000..c0c807a
--- /dev/null
+++ b/odex30_base/queue_job/post_init_hook.py
@@ -0,0 +1,33 @@
+# Copyright 2020 ACSONE SA/NV
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def post_init_hook(env):
+ # this is the trigger that sends notifications when jobs change
+ logger.info("Create queue_job_notify trigger")
+ env.cr.execute(
+ """
+ DROP TRIGGER IF EXISTS queue_job_notify ON queue_job;
+ CREATE OR REPLACE
+ FUNCTION queue_job_notify() RETURNS trigger AS $$
+ BEGIN
+ IF TG_OP = 'DELETE' THEN
+ IF OLD.state != 'done' THEN
+ PERFORM pg_notify('queue_job', OLD.uuid);
+ END IF;
+ ELSE
+ PERFORM pg_notify('queue_job', NEW.uuid);
+ END IF;
+ RETURN NULL;
+ END;
+ $$ LANGUAGE plpgsql;
+ CREATE TRIGGER queue_job_notify
+ AFTER INSERT OR UPDATE OR DELETE
+ ON queue_job
+ FOR EACH ROW EXECUTE PROCEDURE queue_job_notify();
+ """
+ )
diff --git a/odex30_base/queue_job/post_load.py b/odex30_base/queue_job/post_load.py
new file mode 100644
index 0000000..f0c1df8
--- /dev/null
+++ b/odex30_base/queue_job/post_load.py
@@ -0,0 +1,25 @@
+import logging
+
+from odoo import http
+
+_logger = logging.getLogger(__name__)
+
+
+def post_load():
+ _logger.info(
+ "Apply Request._get_session_and_dbname monkey patch to capture db"
+ " from request with multiple databases"
+ )
+ _get_session_and_dbname_orig = http.Request._get_session_and_dbname
+
+ def _get_session_and_dbname(self):
+ session, dbname = _get_session_and_dbname_orig(self)
+ if (
+ not dbname
+ and self.httprequest.path == "/queue_job/runjob"
+ and self.httprequest.args.get("db")
+ ):
+ dbname = self.httprequest.args["db"]
+ return session, dbname
+
+ http.Request._get_session_and_dbname = _get_session_and_dbname
diff --git a/odex30_base/queue_job/pyproject.toml b/odex30_base/queue_job/pyproject.toml
new file mode 100644
index 0000000..4231d0c
--- /dev/null
+++ b/odex30_base/queue_job/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/odex30_base/queue_job/readme/CONFIGURE.md b/odex30_base/queue_job/readme/CONFIGURE.md
new file mode 100644
index 0000000..216b535
--- /dev/null
+++ b/odex30_base/queue_job/readme/CONFIGURE.md
@@ -0,0 +1,40 @@
+- Using environment variables and command line:
+ - Adjust environment variables (optional):
+ - `ODOO_QUEUE_JOB_CHANNELS=root:4` or any other channels
+ configuration. The default is `root:1`
+ - if `xmlrpc_port` is not set: `ODOO_QUEUE_JOB_PORT=8069`
+ - Start Odoo with `--load=web,queue_job` and `--workers` greater than
+ 1.[^1]
+- Using the Odoo configuration file:
+
+``` ini
+[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:
+
+```
+...INFO...queue_job.jobrunner.runner: starting
+...INFO...queue_job.jobrunner.runner: initializing database connections
+...INFO...queue_job.jobrunner.runner: queue job runner ready for db
+...INFO...queue_job.jobrunner.runner: database connections ready
+```
+
+- Create jobs (eg using `base_import_async`) and observe they start
+ immediately and in parallel.
+- Tip: to enable debug logging for the queue job, use
+ `--log-handler=odoo.addons.queue_job:DEBUG`
+
+[^1]: It works with the threaded Odoo server too, although this way of
+ running Odoo is obviously not for production purposes.
+
+* Jobs that remain in `enqueued` or `started` state (because, for instance,
+ their worker has been killed) will be automatically re-queued.
diff --git a/odex30_base/queue_job/readme/CONTRIBUTORS.md b/odex30_base/queue_job/readme/CONTRIBUTORS.md
new file mode 100644
index 0000000..9f92cfb
--- /dev/null
+++ b/odex30_base/queue_job/readme/CONTRIBUTORS.md
@@ -0,0 +1,15 @@
+- Guewen Baconnier \<\>
+- Stéphane Bidoul \<\>
+- Matthieu Dietrich \<\>
+- Jos De Graeve \<\>
+- David Lefever \<
\>
+- Laurent Mignon \<\>
+- Laetitia Gangloff \<\>
+- Cédric Pigeon \<\>
+- Tatiana Deribina \<\>
+- Souheil Bejaoui \<\>
+- Eric Antones \<\>
+- Simone Orsi \<\>
+- Nguyen Minh Chien \<\>
+- Tran Quoc Duong \<>
+- Vo Hong Thien \<>
diff --git a/odex30_base/queue_job/readme/CREDITS.md b/odex30_base/queue_job/readme/CREDITS.md
new file mode 100644
index 0000000..83b3ec9
--- /dev/null
+++ b/odex30_base/queue_job/readme/CREDITS.md
@@ -0,0 +1 @@
+The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp.
diff --git a/odex30_base/queue_job/readme/DESCRIPTION.md b/odex30_base/queue_job/readme/DESCRIPTION.md
new file mode 100644
index 0000000..8617673
--- /dev/null
+++ b/odex30_base/queue_job/readme/DESCRIPTION.md
@@ -0,0 +1,47 @@
+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:
+
+``` python
+from odoo import models, fields, api
+
+class MyModel(models.Model):
+ _name = 'my.model'
+
+ def my_method(self, a, k=None):
+ _logger.info('executed with a: %s and k: %s', a, k)
+
+
+class MyOtherModel(models.Model):
+ _name = 'my.other.model'
+
+ def button_do_stuff(self):
+ self.env['my.model'].with_delay().my_method('a', k=2)
+```
+
+In the snippet of code above, when we call `button_do_stuff`, a job
+**capturing the method and arguments** will be postponed. It will be
+executed as soon as the Jobrunner has a free bucket, which can be
+instantaneous if no other job is running.
+
+Features:
+
+- Views for jobs, jobs are stored in PostgreSQL
+- Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL's
+ NOTIFY
+- Channels: give a capacity for the root channel and its sub-channels
+ and segregate jobs in them. Allow for instance to restrict heavy jobs
+ to be executed one at a time while little ones are executed 4 at a
+ times.
+- Retries: Ability to retry jobs by raising a type of exception
+- Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next
+ tries, retry after 1 minutes, ...
+- Job properties: priorities, estimated time of arrival (ETA), custom
+ description, number of retries
+- Related Actions: link an action on the job view, such as open the
+ record concerned by the job
diff --git a/odex30_base/queue_job/readme/HISTORY.md b/odex30_base/queue_job/readme/HISTORY.md
new file mode 100644
index 0000000..e1f2513
--- /dev/null
+++ b/odex30_base/queue_job/readme/HISTORY.md
@@ -0,0 +1,7 @@
+## Next
+
+- \[ADD\] Run jobrunner as a worker process instead of a thread in the
+ main process (when running with --workers \> 0)
+- \[REF\] `@job` and `@related_action` deprecated, any method can be
+ delayed, and configured using `queue.job.function` records
+- \[MIGRATION\] from 13.0 branched at rev. e24ff4b
diff --git a/odex30_base/queue_job/readme/INSTALL.md b/odex30_base/queue_job/readme/INSTALL.md
new file mode 100644
index 0000000..cdf9b90
--- /dev/null
+++ b/odex30_base/queue_job/readme/INSTALL.md
@@ -0,0 +1 @@
+Be sure to have the `requests` library.
diff --git a/odex30_base/queue_job/readme/ROADMAP.md b/odex30_base/queue_job/readme/ROADMAP.md
new file mode 100644
index 0000000..a13be6b
--- /dev/null
+++ b/odex30_base/queue_job/readme/ROADMAP.md
@@ -0,0 +1,17 @@
+- After creating a new database or installing `queue_job` on an existing
+ database, Odoo must be restarted for the runner to detect it.
+- When Odoo shuts down normally, it waits for running jobs to finish.
+ However, when the Odoo server crashes or is otherwise force-stopped,
+ running jobs are interrupted while the runner has no chance to know
+ they have been aborted. In such situations, jobs may remain in
+ `started` or `enqueued` state after the Odoo server is halted. Since
+ the runner has no way to know if they are actually running or not, and
+ does not know for sure if it is safe to restart the jobs, it does not
+ attempt to restart them automatically. Such stale jobs therefore fill
+ the running queue and prevent other jobs to start. You must therefore
+ requeue them manually, either from the Jobs view, or by running the
+ following SQL statement *before starting Odoo*:
+
+``` sql
+update queue_job set state='pending' where state in ('started', 'enqueued')
+```
diff --git a/odex30_base/queue_job/readme/USAGE.md b/odex30_base/queue_job/readme/USAGE.md
new file mode 100644
index 0000000..deb6fe2
--- /dev/null
+++ b/odex30_base/queue_job/readme/USAGE.md
@@ -0,0 +1,468 @@
+To use this module, you need to:
+
+1. Go to `Job Queue` menu
+
+## Developers
+
+### Delaying jobs
+
+The fast way to enqueue a job for a method is to use `with_delay()` on a
+record or model:
+
+``` 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:
+
+``` 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:
+
+``` 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:
+
+``` 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:
+
+``` 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:
+
+``` 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:
+
+``` python
+def button_increment_var(self):
+ (
+ self
+ .delayable()
+ .increment_counter()
+ .split(1, chain=True) # Will exceute the jobs one after the other
+ .delay()
+ )
+```
+
+### Enqueing Job Options
+
+- priority: default is 10, the closest it is to 0, the faster it will be
+ executed
+- eta: Estimated Time of Arrival of the job. It will not be executed
+ before this date/time
+- max_retries: default is 5, maximum number of retries before giving up
+ and set the job state to 'failed'. A value of 0 means infinite
+ retries.
+- description: human description of the job. If not set, description is
+ computed from the function doc or method name
+- channel: the complete name of the channel to use to process the
+ function. If specified it overrides the one defined on the function
+- identity_key: key uniquely identifying the job, if specified and a job
+ with the same key has not yet been run, the new job will not be
+ created
+
+### Configure default options for jobs
+
+In earlier versions, jobs could be configured using the `@job`
+decorator. This is now obsolete, they can be configured using optional
+`queue.job.function` and `queue.job.channel` XML records.
+
+Example of channel:
+
+``` XML
+
+ sale
+
+
+```
+
+Example of job function:
+
+``` XML
+
+
+ action_done
+
+
+
+
+```
+
+The general form for the `name` is: `.method`.
+
+The channel, related action and retry pattern options are optional, they
+are documented below.
+
+When writing modules, if 2+ modules add a job function or channel with
+the same name (and parent for channels), they'll be merged in the same
+record, even if they have different xmlids. On uninstall, the merged
+record is deleted when all the modules using it are uninstalled.
+
+**Job function: model**
+
+If the function is defined in an abstract model, you can not write
+`` but
+you have to define a function for each model that inherits from the
+abstract model.
+
+**Job function: channel**
+
+The channel where the job will be delayed. The default channel is
+`root`.
+
+**Job function: related action**
+
+The *Related Action* appears as a button on the Job's view. The button
+will execute the defined action.
+
+The default one is to open the view of the record related to the job
+(form view when there is a single record, list view for several
+records). In many cases, the default related action is enough and
+doesn't need customization, but it can be customized by providing a
+dictionary on the job function:
+
+``` python
+{
+ "enable": False,
+ "func_name": "related_action_partner",
+ "kwargs": {"name": "Partner"},
+}
+```
+
+- `enable`: when `False`, the button has no effect (default: `True`)
+- `func_name`: name of the method on `queue.job` that returns an action
+- `kwargs`: extra arguments to pass to the related action method
+
+Example of related action code:
+
+``` python
+class QueueJob(models.Model):
+ _inherit = 'queue.job'
+
+ def related_action_partner(self, name):
+ self.ensure_one()
+ model = self.model_name
+ partner = self.records
+ action = {
+ 'name': name,
+ 'type': 'ir.actions.act_window',
+ 'res_model': model,
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_id': partner.id,
+ }
+ return action
+```
+
+**Job function: retry pattern**
+
+When a job fails with a retryable error type, it is automatically
+retried later. By default, the retry is always 10 minutes later.
+
+A retry pattern can be configured on the job function. What a pattern
+represents is "from X tries, postpone to Y seconds". It is expressed as
+a dictionary where keys are tries and values are seconds to postpone as
+integers:
+
+``` python
+{
+ 1: 10,
+ 5: 20,
+ 10: 30,
+ 15: 300,
+}
+```
+
+Based on this configuration, we can tell that:
+
+- 5 first retries are postponed 10 seconds later
+- retries 5 to 10 postponed 20 seconds later
+- retries 10 to 15 postponed 30 seconds later
+- all subsequent retries postponed 5 minutes later
+
+**Job Context**
+
+The context of the recordset of the job, or any recordset passed in
+arguments of a job, is transferred to the job according to an
+allow-list.
+
+The default allow-list is ("tz", "lang", "allowed_company_ids",
+"force_company", "active_test"). It can be customized in
+`Base._job_prepare_context_before_enqueue_keys`. **Bypass jobs on
+running Odoo**
+
+When you are developing (ie: connector modules) you might want to bypass
+the queue job and run your code immediately.
+
+To do so you can set QUEUE_JOB\_\_NO_DELAY=1 in your environment.
+
+**Bypass jobs in tests**
+
+When writing tests on job-related methods is always tricky to deal with
+delayed recordsets. To make your testing life easier you can set
+queue_job\_\_no_delay=True in the context.
+
+Tip: you can do this at test case level like this
+
+``` python
+@classmethod
+def setUpClass(cls):
+ super().setUpClass()
+ cls.env = cls.env(context=dict(
+ cls.env.context,
+ queue_job__no_delay=True, # no jobs thanks
+ ))
+```
+
+Then all your tests execute the job methods synchronously without
+delaying any jobs.
+
+### Testing
+
+**Asserting enqueued jobs**
+
+The recommended way to test jobs, rather than running them directly and
+synchronously is to split the tests in two parts:
+
+> - one test where the job is mocked (trap jobs with `trap_jobs()` and
+> the test only verifies that the job has been delayed with the
+> expected arguments
+> - one test that only calls the method of the job synchronously, to
+> validate the proper behavior of this method only
+
+Proceeding this way means that you can prove that jobs will be enqueued
+properly at runtime, and it ensures your code does not have a different
+behavior in tests and in production (because running your jobs
+synchronously may have a different behavior as they are in the same
+transaction / in the middle of the method). Additionally, it gives more
+control on the arguments you want to pass when calling the job's method
+(synchronously, this time, in the second type of tests), and it makes
+tests smaller.
+
+The best way to run such assertions on the enqueued jobs is to use
+`odoo.addons.queue_job.tests.common.trap_jobs()`.
+
+A very small example (more details in `tests/common.py`):
+
+``` 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.
+
+``` 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
+
+``` 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**
+ (): 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/odex30_base/queue_job/security/ir.model.access.csv b/odex30_base/queue_job/security/ir.model.access.csv
new file mode 100644
index 0000000..4def7dc
--- /dev/null
+++ b/odex30_base/queue_job/security/ir.model.access.csv
@@ -0,0 +1,8 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_queue_job_manager,queue job manager,queue_job.model_queue_job,queue_job.group_queue_job_manager,1,1,1,1
+access_queue_job_lock_manager,queue job lock manager,queue_job.model_queue_job_lock,queue_job.group_queue_job_manager,1,0,0,0
+access_queue_job_function_manager,queue job functions manager,queue_job.model_queue_job_function,queue_job.group_queue_job_manager,1,1,1,1
+access_queue_job_channel_manager,queue job channel manager,queue_job.model_queue_job_channel,queue_job.group_queue_job_manager,1,1,1,1
+access_queue_requeue_job,queue requeue job manager,queue_job.model_queue_requeue_job,queue_job.group_queue_job_manager,1,1,1,1
+access_queue_jobs_to_done,queue jobs to done manager,queue_job.model_queue_jobs_to_done,queue_job.group_queue_job_manager,1,1,1,1
+access_queue_jobs_to_cancelled,queue jobs to cancelled manager,queue_job.model_queue_jobs_to_cancelled,queue_job.group_queue_job_manager,1,1,1,1
diff --git a/odex30_base/queue_job/security/security.xml b/odex30_base/queue_job/security/security.xml
new file mode 100644
index 0000000..947644e
--- /dev/null
+++ b/odex30_base/queue_job/security/security.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ Job Queue
+ 20
+
+
+ Job Queue Manager
+
+
+
+
+
+
+ Job Queue multi-company
+
+
+ ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
+
+
+
diff --git a/odex30_base/queue_job/static/description/icon.png b/odex30_base/queue_job/static/description/icon.png
new file mode 100644
index 0000000..c20efae
Binary files /dev/null and b/odex30_base/queue_job/static/description/icon.png differ
diff --git a/odex30_base/queue_job/static/description/icon.svg b/odex30_base/queue_job/static/description/icon.svg
new file mode 100644
index 0000000..cbe183a
--- /dev/null
+++ b/odex30_base/queue_job/static/description/icon.svg
@@ -0,0 +1,77 @@
+
+
diff --git a/odex30_base/queue_job/static/description/index.html b/odex30_base/queue_job/static/description/index.html
new file mode 100644
index 0000000..6cc2121
--- /dev/null
+++ b/odex30_base/queue_job/static/description/index.html
@@ -0,0 +1,1020 @@
+
+
+
+
+
+README.rst
+
+
+
+
In the snippet of code above, when we call button_do_stuff, a job
+capturing the method and arguments will be postponed. It will be
+executed as soon as the Jobrunner has a free bucket, which can be
+instantaneous if no other job is running.
+
Features:
+
+
Views for jobs, jobs are stored in PostgreSQL
+
Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL’s
+NOTIFY
+
Channels: give a capacity for the root channel and its sub-channels
+and segregate jobs in them. Allow for instance to restrict heavy jobs
+to be executed one at a time while little ones are executed 4 at a
+times.
+
Retries: Ability to retry jobs by raising a type of exception
+
Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next
+tries, retry after 1 minutes, …
+
Job properties: priorities, estimated time of arrival (ETA), custom
+description, number of retries
+
Related Actions: link an action on the job view, such as open the
+record concerned by the job
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:
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:
+
+defbutton_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:
+
+defbutton_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:
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:
+
+defbutton_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:
+
+defbutton_increment_var(self):
+(
+self
+.delayable()
+.increment_counter()
+.split(1,chain=True)# Will exceute the jobs one after the other
+.delay()
+)
+
In earlier versions, jobs could be configured using the @job
+decorator. This is now obsolete, they can be configured using optional
+queue.job.function and queue.job.channel XML records.
The general form for the name is: <model.name>.method.
+
The channel, related action and retry pattern options are optional, they
+are documented below.
+
When writing modules, if 2+ modules add a job function or channel with
+the same name (and parent for channels), they’ll be merged in the same
+record, even if they have different xmlids. On uninstall, the merged
+record is deleted when all the modules using it are uninstalled.
+
Job function: model
+
If the function is defined in an abstract model, you can not write
+<field name="model_id"ref="xml_id_of_the_abstract_model"</field>
+but you have to define a function for each model that inherits from the
+abstract model.
+
Job function: channel
+
The channel where the job will be delayed. The default channel is
+root.
+
Job function: related action
+
The Related Action appears as a button on the Job’s view. The button
+will execute the defined action.
+
The default one is to open the view of the record related to the job
+(form view when there is a single record, list view for several
+records). In many cases, the default related action is enough and
+doesn’t need customization, but it can be customized by providing a
+dictionary on the job function:
When a job fails with a retryable error type, it is automatically
+retried later. By default, the retry is always 10 minutes later.
+
A retry pattern can be configured on the job function. What a pattern
+represents is “from X tries, postpone to Y seconds”. It is expressed as
+a dictionary where keys are tries and values are seconds to postpone as
+integers:
+
+{
+1:10,
+5:20,
+10:30,
+15:300,
+}
+
+
Based on this configuration, we can tell that:
+
+
5 first retries are postponed 10 seconds later
+
retries 5 to 10 postponed 20 seconds later
+
retries 10 to 15 postponed 30 seconds later
+
all subsequent retries postponed 5 minutes later
+
+
Job Context
+
The context of the recordset of the job, or any recordset passed in
+arguments of a job, is transferred to the job according to an
+allow-list.
+
The default allow-list is (“tz”, “lang”, “allowed_company_ids”,
+“force_company”, “active_test”). It can be customized in
+Base._job_prepare_context_before_enqueue_keys. Bypass jobs on
+running Odoo
+
When you are developing (ie: connector modules) you might want to bypass
+the queue job and run your code immediately.
+
To do so you can set QUEUE_JOB__NO_DELAY=1 in your environment.
+
Bypass jobs in tests
+
When writing tests on job-related methods is always tricky to deal with
+delayed recordsets. To make your testing life easier you can set
+queue_job__no_delay=True in the context.
+
Tip: you can do this at test case level like this
+
+@classmethod
+defsetUpClass(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.
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().
+
A very small example (more details in tests/common.py):
+
+# code
+defmy_job_method(self,name,count):
+self.write({"name":" ".join([name]*count)
+
+defmethod_to_test(self):
+count=self.env["other.model"].search_count([])
+self.with_delay(priority=15).my_job_method("Hi!",count=count)
+returncount
+
+# tests
+fromodoo.addons.queue_job.tests.commonimporttrap_jobs
+
+# first test only check the expected behavior of the method and the proper
+# enqueuing of jobs
+deftest_method_to_test(self):
+withtrap_jobs()astrap:
+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
+deftest_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.
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
+
+@classmethod
+defsetUpClass(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 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
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.
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
+
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.
After creating a new database or installing queue_job on an
+existing database, Odoo must be restarted for the runner to detect it.
+
When Odoo shuts down normally, it waits for running jobs to finish.
+However, when the Odoo server crashes or is otherwise force-stopped,
+running jobs are interrupted while the runner has no chance to know
+they have been aborted. In such situations, jobs may remain in
+started or enqueued state after the Odoo server is halted.
+Since the runner has no way to know if they are actually running or
+not, and does not know for sure if it is safe to restart the jobs, it
+does not attempt to restart them automatically. Such stale jobs
+therefore fill the running queue and prevent other jobs to start. You
+must therefore requeue them manually, either from the Jobs view, or by
+running the following SQL statement before starting Odoo:
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.
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.