\n"
"Language-Team: German (http://www.transifex.com/oca/OCA-connector-8-0/language/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -36,7 +37,7 @@ msgstr "Kein Datensatz zum Überprüfen.
\n Wenn ein Con
#: code:addons/connector/checkpoint/checkpoint.py:136
#, python-format
msgid "A %s needs a review."
-msgstr ""
+msgstr "Ein %s erfordert Sichtung"
#. module: connector
#: field:connector.config.settings,module_portal:0
@@ -61,7 +62,7 @@ msgstr "Erlaube Dokumentenfreigabe"
#. module: connector
#: field:connector.config.settings,module_google_calendar:0
msgid "Allow the users to synchronize their calendar with Google Calendar"
-msgstr ""
+msgstr "Ermöglichen Sie Benutzern ihren Kalender mit Google-Kalender zu synchronisieren"
#. module: connector
#: field:connector.config.settings,module_base_import:0
@@ -76,7 +77,7 @@ msgstr "anwenden"
#. module: connector
#: field:connector.config.settings,module_google_drive:0
msgid "Attach Google documents to any record"
-msgstr ""
+msgstr "Google-Dokumente an beliebige Datensätze anhängen"
#. module: connector
#: view:connector.config.settings:connector.view_connector_config_settings
@@ -91,34 +92,34 @@ msgid "Cancel"
msgstr "Abbrechen"
#. module: connector
-#: code:addons/connector/queue/job.py:571
+#: code:addons/connector/queue/job.py:574
#, python-format
msgid "Canceled. Nothing to do."
-msgstr ""
+msgstr "Storniert. Nicht zu tun."
#. module: connector
-#: code:addons/connector/queue/model.py:456
+#: code:addons/connector/queue/model.py:457
#, python-format
msgid "Cannot change the root channel"
-msgstr ""
+msgstr "Kann den Hauptkanal nicht ändern"
#. module: connector
-#: code:addons/connector/queue/model.py:463
+#: code:addons/connector/queue/model.py:464
#, python-format
msgid "Cannot remove the root channel"
-msgstr ""
+msgstr "Kann den Hauptkanal nicht entfernen"
#. module: connector
#: view:queue.job:connector.view_queue_job_search field:queue.job,channel:0
#: view:queue.job.function:connector.view_queue_job_function_search
#: field:queue.job.function,channel_id:0
msgid "Channel"
-msgstr ""
+msgstr "Kanal"
#. module: connector
#: sql_constraint:queue.job.channel:0
msgid "Channel complete name must be unique"
-msgstr ""
+msgstr "Vollständige Bezeichnung muss eindeutig sein"
#. module: connector
#: model:ir.actions.act_window,name:connector.action_queue_job_channel
@@ -127,7 +128,7 @@ msgstr ""
#: view:queue.job.channel:connector.view_queue_job_channel_search
#: view:queue.job.channel:connector.view_queue_job_channel_tree
msgid "Channels"
-msgstr ""
+msgstr "Kanäle"
#. module: connector
#: model:ir.ui.menu,name:connector.menu_checkpoint
@@ -152,12 +153,12 @@ msgstr "Klicken Sie auf den"
#. module: connector
#: field:queue.job,company_id:0
msgid "Company"
-msgstr ""
+msgstr "Unternehmen"
#. module: connector
#: field:queue.job.channel,complete_name:0 field:queue.job.function,channel:0
msgid "Complete Name"
-msgstr ""
+msgstr "Vollständige Bezeichnung"
#. module: connector
#: view:connector.config.settings:connector.view_connector_config_settings
@@ -174,7 +175,7 @@ msgstr "Connector"
#. module: connector
#: model:ir.model,name:connector.model_connector_backend
msgid "Connector Backend"
-msgstr ""
+msgstr "Connector Backend"
#. module: connector
#: view:connector.checkpoint:connector.view_connector_checkpoint_form
@@ -188,7 +189,7 @@ msgstr "Connector Checkpunkt"
#. module: connector
#: model:ir.model,name:connector.model_connector_config_settings
msgid "Connector Configuration"
-msgstr ""
+msgstr "Connector-Einstellungen"
#. module: connector
#: model:res.groups,name:connector.group_connector_manager
@@ -213,7 +214,7 @@ msgstr "Anlagedatum"
#: field:connector.config.settings,create_uid:0
#: field:queue.job.channel,create_uid:0 field:queue.requeue.job,create_uid:0
msgid "Created by"
-msgstr ""
+msgstr "Angelegt durch"
#. module: connector
#: field:connector.checkpoint,create_date:0
@@ -221,7 +222,7 @@ msgstr ""
#: field:connector.config.settings,create_date:0
#: field:queue.job.channel,create_date:0 field:queue.requeue.job,create_date:0
msgid "Created on"
-msgstr ""
+msgstr "Angelegt am"
#. module: connector
#: field:queue.job,retry:0
@@ -242,7 +243,7 @@ msgstr "Erledigt am"
#: help:connector.checkpoint,message_last_post:0
#: help:queue.job,message_last_post:0
msgid "Date of the last message posted on the record."
-msgstr ""
+msgstr "Datum der letzten Nachricht zu diesem Datensatz"
#. module: connector
#: field:queue.job,name:0
@@ -298,7 +299,7 @@ msgstr "Follower"
#. module: connector
#: field:queue.job,func_name:0
msgid "Func name"
-msgstr ""
+msgstr "Funk.bezeichnung"
#. module: connector
#: help:connector.config.settings,module_portal:0
@@ -309,7 +310,7 @@ msgstr "Erteilen Sie den Kunden Zugriff auf Ihre eigenen Dokumente."
#: view:queue.job:connector.view_queue_job_search
#: view:queue.job.function:connector.view_queue_job_function_search
msgid "Group By"
-msgstr ""
+msgstr "Gruppieren"
#. module: connector
#: view:connector.checkpoint:connector.view_connector_checkpoint_search
@@ -331,7 +332,7 @@ msgstr "Beinhaltet die Chatter Zusammenfassung (Anzahl der Nachrichten, ...). Di
#: field:queue.job.channel,id:0 field:queue.job.function,id:0
#: field:queue.requeue.job,id:0 field:queue.worker,id:0
msgid "ID"
-msgstr ""
+msgstr "ID"
#. module: connector
#: help:connector.checkpoint,message_unread:0 help:queue.job,message_unread:0
@@ -348,7 +349,7 @@ msgstr "Wenn die Anzahl der maximalen Versuche 0 ist, ist die maximal Anzahl der
msgid ""
"If you have setup a catch-all email domain redirected to the Odoo server, "
"enter the domain name here."
-msgstr ""
+msgstr "Wenn Sie eine Catch-All-EMail-Domäne eingerichtet haben, die auf den Oddo-Server weiterleitet, geben Sie hier diesen Domänennamen ein."
#. module: connector
#: field:connector.checkpoint,backend_id:0
@@ -364,12 +365,13 @@ msgstr "Ist ein Follower"
#. module: connector
#: model:ir.model,name:connector.model_queue_job_channel
msgid "Job Channels"
-msgstr ""
+msgstr "Job-Kanäle"
#. module: connector
#: view:queue.job:connector.view_queue_job_search
+#: field:queue.job,job_function_id:0
msgid "Job Function"
-msgstr ""
+msgstr "Job-Funktion"
#. module: connector
#: model:ir.actions.act_window,name:connector.action_queue_job_function
@@ -380,18 +382,13 @@ msgstr ""
#: view:queue.job.function:connector.view_queue_job_function_search
#: view:queue.job.function:connector.view_queue_job_function_tree
msgid "Job Functions"
-msgstr ""
+msgstr "Job-Funktionen"
#. module: connector
#: model:mail.message.subtype,name:connector.mt_job_failed
msgid "Job failed"
msgstr "Auftrag fehlgeschlagen"
-#. module: connector
-#: field:queue.job,job_function_id:0
-msgid "Job function id"
-msgstr ""
-
#. module: connector
#: model:ir.actions.act_window,name:connector.action_queue_job
#: model:ir.ui.menu,name:connector.menu_queue_job
@@ -411,7 +408,7 @@ msgstr "Letzte Statusüberprüfung"
#: field:connector.checkpoint,message_last_post:0
#: field:queue.job,message_last_post:0
msgid "Last Message Date"
-msgstr ""
+msgstr "Letztes Meldungsdatum"
#. module: connector
#: field:connector.checkpoint,write_uid:0
@@ -419,7 +416,7 @@ msgstr ""
#: field:connector.config.settings,write_uid:0
#: field:queue.job.channel,write_uid:0 field:queue.requeue.job,write_uid:0
msgid "Last Updated by"
-msgstr ""
+msgstr "Zuletzt aktualisiert durch"
#. module: connector
#: field:connector.checkpoint,write_date:0
@@ -427,7 +424,7 @@ msgstr ""
#: field:connector.config.settings,write_date:0
#: field:queue.job.channel,write_date:0 field:queue.requeue.job,write_date:0
msgid "Last Updated on"
-msgstr ""
+msgstr "Zuletzt aktualisiert am"
#. module: connector
#: field:external.binding,sync_date:0
@@ -440,10 +437,10 @@ msgid "Manage multiple companies"
msgstr "Multi-Company Verwaltung"
#. module: connector
-#: code:addons/connector/queue/model.py:140
+#: code:addons/connector/queue/model.py:141
#, python-format
msgid "Manually set to done by %s"
-msgstr ""
+msgstr "Manuell auf erledigt gesetzt durch %s"
#. module: connector
#: field:queue.job,max_retries:0
@@ -466,11 +463,6 @@ msgstr "Nachrichten und Kommunikations-Historie"
msgid "Model"
msgstr "Modell"
-#. module: connector
-#: model:ir.model,name:connector.model_ir_module_module
-msgid "Module"
-msgstr ""
-
#. module: connector
#: field:connector.backend,name:0 field:queue.job.channel,name:0
#: field:queue.job.function,name:0
@@ -484,10 +476,10 @@ msgid "Need Review"
msgstr "Benötigt Prüfung"
#. module: connector
-#: code:addons/connector/queue/model.py:116
+#: code:addons/connector/queue/model.py:117
#, python-format
msgid "No action available for this job"
-msgstr ""
+msgstr "Kein Vorgang für diesen Job verfügbar"
#. module: connector
#: model:ir.actions.client,name:connector.action_client_connector_menu
@@ -502,18 +494,18 @@ msgstr "Prozess-ID"
#. module: connector
#: field:queue.job.channel,parent_id:0
msgid "Parent Channel"
-msgstr ""
+msgstr "Übergeordneter Kanal"
#. module: connector
-#: code:addons/connector/queue/model.py:448
+#: code:addons/connector/queue/model.py:449
#, python-format
msgid "Parent channel required."
-msgstr ""
+msgstr "Übergeordneter Kanal erforderlich."
#. module: connector
#: view:queue.job:connector.view_queue_job_search selection:queue.job,state:0
msgid "Pending"
-msgstr "Warteschlange"
+msgstr "Wartend"
#. module: connector
#: field:queue.job,func:0
@@ -538,7 +530,7 @@ msgstr "Aufgabe in Warteschlange einreihen"
#. module: connector
#: model:ir.model,name:connector.model_queue_worker
msgid "Queue Worker"
-msgstr ""
+msgstr "Warteschlangenarbeiter"
#. module: connector
#: field:connector.checkpoint,record_id:0
@@ -548,18 +540,18 @@ msgstr "Datensatz Nr."
#. module: connector
#: view:queue.job:connector.view_queue_job_form
msgid "Related"
-msgstr ""
+msgstr "Zugehörig"
#. module: connector
#: code:addons/connector/related_action.py:48
#, python-format
msgid "Related Record"
-msgstr ""
+msgstr "Zugehöriger Datensatz"
#. module: connector
#: field:connector.config.settings,font:0
msgid "Report Font"
-msgstr ""
+msgstr "Berichtsschriftart"
#. module: connector
#: view:queue.requeue.job:connector.view_requeue_job
@@ -606,7 +598,7 @@ msgstr "Als Überprüft markieren"
msgid ""
"Set the font into the report header, it will be used as default font in the "
"RML reports of the user company"
-msgstr ""
+msgstr "Bestimmen Sie die Schriftart im Berichtskopf, diese wird als Vorgabeschriftart für RML-Berichte zum Unternehmen des Benutzers verwendet"
#. module: connector
#: view:queue.job:connector.view_queue_job_form
@@ -616,15 +608,15 @@ msgstr "Setzte auf 'Fertig'"
#. module: connector
#: help:connector.config.settings,module_share:0
msgid "Share or embbed any screen of Odoo."
-msgstr ""
+msgstr "Teilen oder Einbetten eines jeden Bildschirms von Odoo."
#. module: connector
-#: code:addons/connector/queue/model.py:186
+#: code:addons/connector/queue/model.py:187
#, python-format
msgid ""
"Something bad happened during the execution of the job. More details in the "
"'Exception Information' section."
-msgstr ""
+msgstr "Etwas schlechtes ist bei der Ausführung des Jobs passiert. Mehr Details finden Sie im Abschnitt 'Ausnahmeninformation'."
#. module: connector
#: field:queue.job,date_started:0 field:queue.worker,date_start:0
@@ -682,12 +674,12 @@ msgstr "Die ausgewählten Arbeitsschritte werden nochmal eingereiht."
#. module: connector
#: help:connector.config.settings,module_google_calendar:0
msgid "This installs the module google_calendar."
-msgstr ""
+msgstr "Das Modul google_calendar wird installiert."
#. module: connector
#: help:connector.config.settings,module_google_drive:0
msgid "This installs the module google_docs."
-msgstr ""
+msgstr "Das Modul google_docs wird installiert."
#. module: connector
#: field:queue.job,uuid:0 field:queue.worker,uuid:0
@@ -726,7 +718,7 @@ msgstr "Wizard um eine Auswahl an Aufgaben neu in die Warteschlange einzureihen"
msgid ""
"Work in multi-company environments, with appropriate security access between companies.\n"
"-This installs the module multi_company."
-msgstr ""
+msgstr "Arbeiten in Umgebungen mit mehreren Unternehmen, mit entsprechenden Zugriffsbeschränkungen zwischen Unternehmen.\n- Das Modul multi_company wird installiert."
#. module: connector
#: field:queue.job,worker_id:0
@@ -751,4 +743,4 @@ msgstr "oder"
#. module: connector
#: view:connector.checkpoint:connector.view_connector_checkpoint_form
msgid "to verify it:"
-msgstr "Überprüfe:"
+msgstr "es zu überprüfen:"
diff --git a/connector/i18n/en.po b/connector/i18n/en.po
index 9d3e205c1..f45609171 100644
--- a/connector/i18n/en.po
+++ b/connector/i18n/en.po
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: connector (8.0)\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-09-11 07:47+0000\n"
-"PO-Revision-Date: 2015-09-10 13:20+0000\n"
+"POT-Creation-Date: 2015-10-16 06:23+0000\n"
+"PO-Revision-Date: 2015-10-15 13:38+0000\n"
"Last-Translator: OCA Transbot \n"
"Language-Team: English (http://www.transifex.com/oca/OCA-connector-8-0/language/en/)\n"
"MIME-Version: 1.0\n"
@@ -90,7 +90,7 @@ msgid "Cancel"
msgstr "Cancel"
#. module: connector
-#: code:addons/connector/queue/job.py:571
+#: code:addons/connector/queue/job.py:574
#, python-format
msgid "Canceled. Nothing to do."
msgstr "Canceled. Nothing to do."
@@ -461,11 +461,6 @@ msgstr "Messages and communication history"
msgid "Model"
msgstr "Model"
-#. module: connector
-#: model:ir.model,name:connector.model_ir_module_module
-msgid "Module"
-msgstr "Module"
-
#. module: connector
#: field:connector.backend,name:0 field:queue.job.channel,name:0
#: field:queue.job.function,name:0
diff --git a/connector/i18n/es_MX.po b/connector/i18n/es_MX.po
index cdda996bc..1939da97b 100644
--- a/connector/i18n/es_MX.po
+++ b/connector/i18n/es_MX.po
@@ -8,9 +8,9 @@ msgid ""
msgstr ""
"Project-Id-Version: connector (8.0)\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-10-09 10:06+0000\n"
-"PO-Revision-Date: 2015-10-09 16:14+0000\n"
-"Last-Translator: Jesus Alan Ramos Rodriguez \n"
+"POT-Creation-Date: 2015-10-16 06:23+0000\n"
+"PO-Revision-Date: 2015-10-15 13:38+0000\n"
+"Last-Translator: OCA Transbot \n"
"Language-Team: Spanish (Mexico) (http://www.transifex.com/oca/OCA-connector-8-0/language/es_MX/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -91,7 +91,7 @@ msgid "Cancel"
msgstr "Cancelar"
#. module: connector
-#: code:addons/connector/queue/job.py:571
+#: code:addons/connector/queue/job.py:574
#, python-format
msgid "Canceled. Nothing to do."
msgstr ""
@@ -462,11 +462,6 @@ msgstr ""
msgid "Model"
msgstr "Modelo"
-#. module: connector
-#: model:ir.model,name:connector.model_ir_module_module
-msgid "Module"
-msgstr "Módulo"
-
#. module: connector
#: field:connector.backend,name:0 field:queue.job.channel,name:0
#: field:queue.job.function,name:0
diff --git a/connector/i18n/fr.po b/connector/i18n/fr.po
index c24ace5c4..f83d30dd7 100644
--- a/connector/i18n/fr.po
+++ b/connector/i18n/fr.po
@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: connector (8.0)\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-09-11 07:47+0000\n"
-"PO-Revision-Date: 2015-09-10 13:20+0000\n"
+"POT-Creation-Date: 2015-10-16 06:23+0000\n"
+"PO-Revision-Date: 2015-10-15 13:38+0000\n"
"Last-Translator: OCA Transbot \n"
"Language-Team: French (http://www.transifex.com/oca/OCA-connector-8-0/language/fr/)\n"
"MIME-Version: 1.0\n"
@@ -92,7 +92,7 @@ msgid "Cancel"
msgstr "Annuler"
#. module: connector
-#: code:addons/connector/queue/job.py:571
+#: code:addons/connector/queue/job.py:574
#, python-format
msgid "Canceled. Nothing to do."
msgstr "Annulé. Rien à faire."
@@ -463,11 +463,6 @@ msgstr "Historique des messages et des communications"
msgid "Model"
msgstr "Modèle"
-#. module: connector
-#: model:ir.model,name:connector.model_ir_module_module
-msgid "Module"
-msgstr "Module"
-
#. module: connector
#: field:connector.backend,name:0 field:queue.job.channel,name:0
#: field:queue.job.function,name:0
diff --git a/connector/i18n/pt_BR.po b/connector/i18n/pt_BR.po
index 9858551d0..429f3f193 100644
--- a/connector/i18n/pt_BR.po
+++ b/connector/i18n/pt_BR.po
@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: connector (8.0)\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-09-11 07:47+0000\n"
-"PO-Revision-Date: 2015-09-10 13:20+0000\n"
+"POT-Creation-Date: 2015-10-16 06:23+0000\n"
+"PO-Revision-Date: 2015-10-15 13:38+0000\n"
"Last-Translator: OCA Transbot \n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/oca/OCA-connector-8-0/language/pt_BR/)\n"
"MIME-Version: 1.0\n"
@@ -92,7 +92,7 @@ msgid "Cancel"
msgstr "Cancelar"
#. module: connector
-#: code:addons/connector/queue/job.py:571
+#: code:addons/connector/queue/job.py:574
#, python-format
msgid "Canceled. Nothing to do."
msgstr "Cancelado. Nada a se fazer"
@@ -463,11 +463,6 @@ msgstr "Mensagens e histórico de comunicação"
msgid "Model"
msgstr "Modelo"
-#. module: connector
-#: model:ir.model,name:connector.model_ir_module_module
-msgid "Module"
-msgstr "Módulo"
-
#. module: connector
#: field:connector.backend,name:0 field:queue.job.channel,name:0
#: field:queue.job.function,name:0
diff --git a/connector/i18n/ro.po b/connector/i18n/ro.po
new file mode 100644
index 000000000..3b9bcbd9c
--- /dev/null
+++ b/connector/i18n/ro.po
@@ -0,0 +1,745 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * connector
+#
+# Translators:
+# Dorin Hongu , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: connector (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-11-06 19:37+0000\n"
+"PO-Revision-Date: 2015-11-03 17:25+0000\n"
+"Last-Translator: Dorin Hongu \n"
+"Language-Team: Romanian (http://www.transifex.com/oca/OCA-connector-8-0/language/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#. module: connector
+#: model:ir.actions.act_window,help:connector.action_connector_checkpoint
+msgid ""
+"No record to check.
\n"
+" When a connector imports new records which have\n"
+" configuration or reviews to do manually, they\n"
+" will appear in this list. Once a record has been\n"
+" checked, click on the 'Reviewed' button.
\n"
+" The connectors list the new records to verify\n"
+" based on their type. Only some need a manual\n"
+" review.
\n"
+" "
+msgstr ""
+
+#. module: connector
+#: code:addons/connector/checkpoint/checkpoint.py:136
+#, python-format
+msgid "A %s needs a review."
+msgstr ""
+
+#. module: connector
+#: field:connector.config.settings,module_portal:0
+msgid "Activate the customer portal"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,active:0
+msgid "Active"
+msgstr "Activ"
+
+#. module: connector
+#: field:connector.config.settings,alias_domain:0
+msgid "Alias Domain"
+msgstr ""
+
+#. module: connector
+#: field:connector.config.settings,module_share:0
+msgid "Allow documents sharing"
+msgstr ""
+
+#. module: connector
+#: field:connector.config.settings,module_google_calendar:0
+msgid "Allow the users to synchronize their calendar with Google Calendar"
+msgstr ""
+
+#. module: connector
+#: field:connector.config.settings,module_base_import:0
+msgid "Allow users to import data from CSV files"
+msgstr ""
+
+#. module: connector
+#: view:connector.config.settings:connector.view_connector_config_settings
+msgid "Apply"
+msgstr "Aplică"
+
+#. module: connector
+#: field:connector.config.settings,module_google_drive:0
+msgid "Attach Google documents to any record"
+msgstr ""
+
+#. module: connector
+#: view:connector.config.settings:connector.view_connector_config_settings
+msgid "Backends"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint.review:connector.view_connector_checkpoint_review
+#: view:connector.config.settings:connector.view_connector_config_settings
+#: view:queue.requeue.job:connector.view_requeue_job
+msgid "Cancel"
+msgstr ""
+
+#. module: connector
+#: code:addons/connector/queue/job.py:574
+#, python-format
+msgid "Canceled. Nothing to do."
+msgstr ""
+
+#. module: connector
+#: code:addons/connector/queue/model.py:457
+#, python-format
+msgid "Cannot change the root channel"
+msgstr ""
+
+#. module: connector
+#: code:addons/connector/queue/model.py:464
+#, python-format
+msgid "Cannot remove the root channel"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_search field:queue.job,channel:0
+#: view:queue.job.function:connector.view_queue_job_function_search
+#: field:queue.job.function,channel_id:0
+msgid "Channel"
+msgstr ""
+
+#. module: connector
+#: sql_constraint:queue.job.channel:0
+msgid "Channel complete name must be unique"
+msgstr ""
+
+#. module: connector
+#: model:ir.actions.act_window,name:connector.action_queue_job_channel
+#: model:ir.ui.menu,name:connector.menu_queue_job_channel
+#: view:queue.job.channel:connector.view_queue_job_channel_form
+#: view:queue.job.channel:connector.view_queue_job_channel_search
+#: view:queue.job.channel:connector.view_queue_job_channel_tree
+msgid "Channels"
+msgstr ""
+
+#. module: connector
+#: model:ir.ui.menu,name:connector.menu_checkpoint
+msgid "Checkpoint"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint.review,checkpoint_ids:0
+msgid "Checkpoints"
+msgstr ""
+
+#. module: connector
+#: model:ir.model,name:connector.model_connector_checkpoint_review
+msgid "Checkpoints Review"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint:connector.view_connector_checkpoint_form
+msgid "Click on the"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,company_id:0
+msgid "Company"
+msgstr ""
+
+#. module: connector
+#: field:queue.job.channel,complete_name:0 field:queue.job.function,channel:0
+msgid "Complete Name"
+msgstr ""
+
+#. module: connector
+#: view:connector.config.settings:connector.view_connector_config_settings
+#: model:ir.actions.act_window,name:connector.action_connector_config_settings
+msgid "Configure Connector"
+msgstr ""
+
+#. module: connector
+#: model:ir.module.category,name:connector.module_category_connector
+#: model:ir.ui.menu,name:connector.menu_connector_config_settings
+msgid "Connector"
+msgstr ""
+
+#. module: connector
+#: model:ir.model,name:connector.model_connector_backend
+msgid "Connector Backend"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint:connector.view_connector_checkpoint_form
+#: view:connector.checkpoint:connector.view_connector_checkpoint_search
+#: view:connector.checkpoint:connector.view_connector_checkpoint_tree
+#: model:ir.actions.act_window,name:connector.action_connector_checkpoint
+#: model:ir.model,name:connector.model_connector_checkpoint
+msgid "Connector Checkpoint"
+msgstr ""
+
+#. module: connector
+#: model:ir.model,name:connector.model_connector_config_settings
+msgid "Connector Configuration"
+msgstr ""
+
+#. module: connector
+#: model:res.groups,name:connector.group_connector_manager
+msgid "Connector Manager"
+msgstr ""
+
+#. module: connector
+#: model:ir.ui.menu,name:connector.menu_connector
+#: model:ir.ui.menu,name:connector.menu_connector_root
+#: view:res.partner:connector.view_partner_connector_form
+msgid "Connectors"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,date_created:0
+msgid "Created Date"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,create_uid:0
+#: field:connector.checkpoint.review,create_uid:0
+#: field:connector.config.settings,create_uid:0
+#: field:queue.job.channel,create_uid:0 field:queue.requeue.job,create_uid:0
+msgid "Created by"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,create_date:0
+#: field:connector.checkpoint.review,create_date:0
+#: field:connector.config.settings,create_date:0
+#: field:queue.job.channel,create_date:0 field:queue.requeue.job,create_date:0
+msgid "Created on"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,retry:0
+msgid "Current try"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_form
+msgid "Current try / max. retries"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,date_done:0
+msgid "Date Done"
+msgstr ""
+
+#. module: connector
+#: help:connector.checkpoint,message_last_post:0
+#: help:queue.job,message_last_post:0
+msgid "Date of the last message posted on the record."
+msgstr ""
+
+#. module: connector
+#: field:queue.job,name:0
+msgid "Description"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_search selection:queue.job,state:0
+msgid "Done"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,date_enqueued:0
+msgid "Enqueue Time"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_search selection:queue.job,state:0
+msgid "Enqueued"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,exc_info:0
+msgid "Exception Info"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_form
+msgid "Exception Information"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,eta:0
+msgid "Execute only after"
+msgstr ""
+
+#. module: connector
+#: model:ir.model,name:connector.model_external_binding
+msgid "External Binding (abstract)"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_search selection:queue.job,state:0
+msgid "Failed"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,message_follower_ids:0
+#: field:queue.job,message_follower_ids:0
+msgid "Followers"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,func_name:0
+msgid "Func name"
+msgstr ""
+
+#. module: connector
+#: help:connector.config.settings,module_portal:0
+msgid "Give your customers access to their documents."
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_search
+#: view:queue.job.function:connector.view_queue_job_function_search
+msgid "Group By"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint:connector.view_connector_checkpoint_search
+msgid "Group By..."
+msgstr ""
+
+#. module: connector
+#: help:connector.checkpoint,message_summary:0
+#: help:queue.job,message_summary:0
+msgid ""
+"Holds the Chatter summary (number of messages, ...). This summary is "
+"directly in html format in order to be inserted in kanban views."
+msgstr ""
+
+#. module: connector
+#: field:connector.backend,id:0 field:connector.checkpoint,id:0
+#: field:connector.checkpoint.review,id:0 field:connector.config.settings,id:0
+#: field:external.binding,id:0 field:queue.job,id:0
+#: field:queue.job.channel,id:0 field:queue.job.function,id:0
+#: field:queue.requeue.job,id:0 field:queue.worker,id:0
+msgid "ID"
+msgstr ""
+
+#. module: connector
+#: help:connector.checkpoint,message_unread:0 help:queue.job,message_unread:0
+msgid "If checked new messages require your attention."
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_form
+msgid "If the max. retries is 0, the number of retries is infinite."
+msgstr ""
+
+#. module: connector
+#: help:connector.config.settings,alias_domain:0
+msgid ""
+"If you have setup a catch-all email domain redirected to the Odoo server, "
+"enter the domain name here."
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,backend_id:0
+msgid "Imported from"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,message_is_follower:0
+#: field:queue.job,message_is_follower:0
+msgid "Is a Follower"
+msgstr ""
+
+#. module: connector
+#: model:ir.model,name:connector.model_queue_job_channel
+msgid "Job Channels"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_search
+#: field:queue.job,job_function_id:0
+msgid "Job Function"
+msgstr ""
+
+#. module: connector
+#: model:ir.actions.act_window,name:connector.action_queue_job_function
+#: model:ir.model,name:connector.model_queue_job_function
+#: model:ir.ui.menu,name:connector.menu_queue_job_function
+#: field:queue.job.channel,job_function_ids:0
+#: view:queue.job.function:connector.view_queue_job_function_form
+#: view:queue.job.function:connector.view_queue_job_function_search
+#: view:queue.job.function:connector.view_queue_job_function_tree
+msgid "Job Functions"
+msgstr ""
+
+#. module: connector
+#: model:mail.message.subtype,name:connector.mt_job_failed
+msgid "Job failed"
+msgstr ""
+
+#. module: connector
+#: model:ir.actions.act_window,name:connector.action_queue_job
+#: model:ir.ui.menu,name:connector.menu_queue_job
+#: view:queue.job:connector.view_queue_job_form
+#: view:queue.job:connector.view_queue_job_search
+#: view:queue.job:connector.view_queue_job_tree
+#: field:queue.requeue.job,job_ids:0 field:queue.worker,job_ids:0
+msgid "Jobs"
+msgstr ""
+
+#. module: connector
+#: field:queue.worker,date_alive:0
+msgid "Last Alive Check"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,message_last_post:0
+#: field:queue.job,message_last_post:0
+msgid "Last Message Date"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,write_uid:0
+#: field:connector.checkpoint.review,write_uid:0
+#: field:connector.config.settings,write_uid:0
+#: field:queue.job.channel,write_uid:0 field:queue.requeue.job,write_uid:0
+msgid "Last Updated by"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,write_date:0
+#: field:connector.checkpoint.review,write_date:0
+#: field:connector.config.settings,write_date:0
+#: field:queue.job.channel,write_date:0 field:queue.requeue.job,write_date:0
+msgid "Last Updated on"
+msgstr ""
+
+#. module: connector
+#: field:external.binding,sync_date:0
+msgid "Last synchronization date"
+msgstr ""
+
+#. module: connector
+#: field:connector.config.settings,module_multi_company:0
+msgid "Manage multiple companies"
+msgstr ""
+
+#. module: connector
+#: code:addons/connector/queue/model.py:141
+#, python-format
+msgid "Manually set to done by %s"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,max_retries:0
+msgid "Max. retries"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,message_ids:0 field:queue.job,message_ids:0
+msgid "Messages"
+msgstr ""
+
+#. module: connector
+#: help:connector.checkpoint,message_ids:0 help:queue.job,message_ids:0
+msgid "Messages and communication history"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint:connector.view_connector_checkpoint_search
+#: field:connector.checkpoint,model_id:0 field:queue.job,model_name:0
+msgid "Model"
+msgstr ""
+
+#. module: connector
+#: field:connector.backend,name:0 field:queue.job.channel,name:0
+#: field:queue.job.function,name:0
+msgid "Name"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint:connector.view_connector_checkpoint_search
+#: selection:connector.checkpoint,state:0
+msgid "Need Review"
+msgstr ""
+
+#. module: connector
+#: code:addons/connector/queue/model.py:117
+#, python-format
+msgid "No action available for this job"
+msgstr ""
+
+#. module: connector
+#: model:ir.actions.client,name:connector.action_client_connector_menu
+msgid "Open Connector Menu"
+msgstr ""
+
+#. module: connector
+#: field:queue.worker,pid:0
+msgid "PID"
+msgstr ""
+
+#. module: connector
+#: field:queue.job.channel,parent_id:0
+msgid "Parent Channel"
+msgstr ""
+
+#. module: connector
+#: code:addons/connector/queue/model.py:449
+#, python-format
+msgid "Parent channel required."
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_search selection:queue.job,state:0
+msgid "Pending"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,func:0
+msgid "Pickled Function"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,priority:0
+msgid "Priority"
+msgstr ""
+
+#. module: connector
+#: model:ir.ui.menu,name:connector.menu_queue
+msgid "Queue"
+msgstr ""
+
+#. module: connector
+#: model:ir.model,name:connector.model_queue_job
+msgid "Queue Job"
+msgstr ""
+
+#. module: connector
+#: model:ir.model,name:connector.model_queue_worker
+msgid "Queue Worker"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,record_id:0
+msgid "Record ID"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_form
+msgid "Related"
+msgstr ""
+
+#. module: connector
+#: code:addons/connector/related_action.py:48
+#, python-format
+msgid "Related Record"
+msgstr ""
+
+#. module: connector
+#: field:connector.config.settings,font:0
+msgid "Report Font"
+msgstr ""
+
+#. module: connector
+#: view:queue.requeue.job:connector.view_requeue_job
+msgid "Requeue"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_form
+msgid "Requeue Job"
+msgstr ""
+
+#. module: connector
+#: model:ir.actions.act_window,name:connector.action_requeue_job
+#: view:queue.requeue.job:connector.view_requeue_job
+msgid "Requeue Jobs"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_form field:queue.job,result:0
+msgid "Result"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint.review:connector.view_connector_checkpoint_review
+#: model:ir.actions.act_window,name:connector.action_connector_checkpoint_review
+msgid "Review Checkpoints"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint:connector.view_connector_checkpoint_form
+#: view:connector.checkpoint:connector.view_connector_checkpoint_search
+#: view:connector.checkpoint:connector.view_connector_checkpoint_tree
+#: selection:connector.checkpoint,state:0
+msgid "Reviewed"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint.review:connector.view_connector_checkpoint_review
+msgid "Set as reviewed"
+msgstr ""
+
+#. module: connector
+#: help:connector.config.settings,font:0
+msgid ""
+"Set the font into the report header, it will be used as default font in the "
+"RML reports of the user company"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_form
+msgid "Set to 'Done'"
+msgstr ""
+
+#. module: connector
+#: help:connector.config.settings,module_share:0
+msgid "Share or embbed any screen of Odoo."
+msgstr ""
+
+#. module: connector
+#: code:addons/connector/queue/model.py:187
+#, python-format
+msgid ""
+"Something bad happened during the execution of the job. More details in the "
+"'Exception Information' section."
+msgstr ""
+
+#. module: connector
+#: field:queue.job,date_started:0 field:queue.worker,date_start:0
+msgid "Start Date"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_search selection:queue.job,state:0
+msgid "Started"
+msgstr ""
+
+#. module: connector
+#: view:queue.job:connector.view_queue_job_search field:queue.job,state:0
+msgid "State"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,state:0
+msgid "Status"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,message_summary:0
+#: field:queue.job,message_summary:0
+msgid "Summary"
+msgstr ""
+
+#. module: connector
+#: field:queue.job,func_string:0
+msgid "Task"
+msgstr ""
+
+#. module: connector
+#: help:queue.job,max_retries:0
+msgid ""
+"The job will fail if the number of tries reach the max. retries.\n"
+"Retries are infinite when empty."
+msgstr ""
+
+#. module: connector
+#: help:connector.checkpoint,backend_id:0
+msgid "The record has been imported from this backend"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint.review:connector.view_connector_checkpoint_review
+msgid "The selected checkpoints will be set as reviewed."
+msgstr ""
+
+#. module: connector
+#: view:queue.requeue.job:connector.view_requeue_job
+msgid "The selected jobs will be requeued."
+msgstr ""
+
+#. module: connector
+#: help:connector.config.settings,module_google_calendar:0
+msgid "This installs the module google_calendar."
+msgstr ""
+
+#. module: connector
+#: help:connector.config.settings,module_google_drive:0
+msgid "This installs the module google_docs."
+msgstr ""
+
+#. module: connector
+#: field:queue.job,uuid:0 field:queue.worker,uuid:0
+msgid "UUID"
+msgstr ""
+
+#. module: connector
+#: field:connector.checkpoint,message_unread:0
+#: field:queue.job,message_unread:0
+msgid "Unread Messages"
+msgstr ""
+
+#. module: connector
+#: field:connector.config.settings,module_auth_oauth:0
+msgid ""
+"Use external authentication providers, sign in with google, facebook, ..."
+msgstr ""
+
+#. module: connector
+#: field:queue.job,user_id:0
+msgid "User ID"
+msgstr ""
+
+#. module: connector
+#: field:connector.backend,version:0
+msgid "Version"
+msgstr ""
+
+#. module: connector
+#: model:ir.model,name:connector.model_queue_requeue_job
+msgid "Wizard to requeue a selection of jobs"
+msgstr ""
+
+#. module: connector
+#: help:connector.config.settings,module_multi_company:0
+msgid ""
+"Work in multi-company environments, with appropriate security access between companies.\n"
+"-This installs the module multi_company."
+msgstr ""
+
+#. module: connector
+#: field:queue.job,worker_id:0
+#: view:queue.worker:connector.view_queue_worker_form
+#: view:queue.worker:connector.view_queue_worker_tree
+msgid "Worker"
+msgstr ""
+
+#. module: connector
+#: model:ir.actions.act_window,name:connector.action_queue_worker
+#: model:ir.ui.menu,name:connector.menu_queue_worker
+msgid "Workers"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint.review:connector.view_connector_checkpoint_review
+#: view:connector.config.settings:connector.view_connector_config_settings
+#: view:queue.requeue.job:connector.view_requeue_job
+msgid "or"
+msgstr ""
+
+#. module: connector
+#: view:connector.checkpoint:connector.view_connector_checkpoint_form
+msgid "to verify it:"
+msgstr ""
diff --git a/connector/i18n/sl.po b/connector/i18n/sl.po
index 4cda46329..17582f1a6 100644
--- a/connector/i18n/sl.po
+++ b/connector/i18n/sl.po
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: connector (8.0)\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-09-11 07:47+0000\n"
-"PO-Revision-Date: 2015-09-10 13:20+0000\n"
+"POT-Creation-Date: 2015-10-16 06:23+0000\n"
+"PO-Revision-Date: 2015-10-15 13:38+0000\n"
"Last-Translator: OCA Transbot \n"
"Language-Team: Slovenian (http://www.transifex.com/oca/OCA-connector-8-0/language/sl/)\n"
"MIME-Version: 1.0\n"
@@ -91,7 +91,7 @@ msgid "Cancel"
msgstr "Preklic"
#. module: connector
-#: code:addons/connector/queue/job.py:571
+#: code:addons/connector/queue/job.py:574
#, python-format
msgid "Canceled. Nothing to do."
msgstr "Preklicano. Ni opravkov."
@@ -462,11 +462,6 @@ msgstr "Sporočila in kronologija komunikacij"
msgid "Model"
msgstr "Model"
-#. module: connector
-#: model:ir.model,name:connector.model_ir_module_module
-msgid "Module"
-msgstr "Modul"
-
#. module: connector
#: field:connector.backend,name:0 field:queue.job.channel,name:0
#: field:queue.job.function,name:0
diff --git a/connector/ir_module_module.py b/connector/ir_module_module.py
deleted file mode 100644
index cc1d07444..000000000
--- a/connector/ir_module_module.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# Author: Guewen Baconnier
-# Copyright 2012-2013 Camptocamp SA
-# Copyright 2015 anybox SA
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-from openerp import models, api
-from openerp.tools.cache import ormcache
-
-
-class IrModuleModule(models.Model):
- """Overwrite ir.module.module to add cached method 'is_module_installed'.
- This method is cached, because connector will always check if a module is
- installed before do action.
-
- All update (write method) on the field state, invalidate the cache
- """
- _inherit = 'ir.module.module'
-
- @ormcache(skiparg=1)
- def is_module_installed(self, module_name):
- states = ('installed', 'to upgrade')
- return bool(len(self.search([('name', '=', module_name),
- ('state', 'in', states)])))
-
- @api.multi
- def write(self, values):
- res = super(IrModuleModule, self).write(values)
- if 'state' in values:
- self.clear_caches()
-
- return res
diff --git a/connector/jobrunner/runner.py b/connector/jobrunner/runner.py
index 80688d632..d692f13a6 100644
--- a/connector/jobrunner/runner.py
+++ b/connector/jobrunner/runner.py
@@ -26,16 +26,8 @@
"""
What is the job runner?
-----------------------
-This is an alternative to connector workers, with the goal
-of resolving issues due to the polling nature of workers:
-
-* jobs do not start immediately even if there is a free connector worker,
-* connector workers may starve while other workers have too many jobs enqueued,
-* connector workers require another startup script,
- making deployment more difficult
-
-It is fully compatible with the connector mechanism and only
-replaces workers.
+The job runner is the main process managing the dispatch of delayed jobs to
+available Odoo workers
How does it work?
-----------------
@@ -68,10 +60,6 @@
...INFO...connector.jobrunner.runner: connector runner ready for db
...INFO...connector.jobrunner.runner: database connections ready
-* Disable the "Enqueue Jobs" cron.
-
-* Do NOT start openerp-connector-worker.
-
* Create jobs (eg using base_import_async) and observe they
start immediately and in parallel.
@@ -112,7 +100,6 @@
from contextlib import closing
import logging
import os
-import re
import select
import threading
import time
@@ -123,7 +110,7 @@
import openerp
-from .channels import ChannelManager, ENQUEUED, NOT_DONE
+from .channels import ChannelManager, PENDING, ENQUEUED, NOT_DONE
SELECT_TIMEOUT = 60
ERROR_RECOVERY_DELAY = 5
@@ -131,19 +118,34 @@
_logger = logging.getLogger(__name__)
-def _async_http_get(url):
+def _async_http_get(port, db_name, job_uuid):
+ # Method to set failed job (due to timeout, etc) as pending,
+ # to avoid keeping it as enqueued.
+ def set_job_pending():
+ conn = psycopg2.connect(openerp.sql_db.dsn(db_name)[1])
+ conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
+ with closing(conn.cursor()) as cr:
+ cr.execute(
+ "UPDATE queue_job SET state=%s, "
+ "date_enqueued=NULL, date_started=NULL "
+ "WHERE uuid=%s and state=%s", (PENDING, job_uuid, ENQUEUED)
+ )
+
# 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 = ('http://localhost:%s/connector/runjob?db=%s&job_uuid=%s' %
+ (port, db_name, job_uuid))
try:
# 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
requests.get(url, timeout=1)
except requests.Timeout:
- pass
+ set_job_pending()
except:
_logger.exception("exception in GET %s", url)
+ set_job_pending()
thread = threading.Thread(target=urlopen)
thread.daemon = True
thread.start()
@@ -218,10 +220,11 @@ def _initialize(self):
cr.execute("LISTEN connector")
def select_jobs(self, where, args):
- query = "SELECT %s, uuid, id as seq, date_created, priority, eta, state " \
- "FROM queue_job WHERE %s" % \
- ('channel' if self.has_channel else 'NULL',
- where)
+ query = ("SELECT %s, uuid, id as seq, date_created, "
+ "priority, eta, state "
+ "FROM queue_job WHERE %s" %
+ ('channel' if self.has_channel else 'NULL',
+ where))
with closing(self.conn.cursor()) as cr:
cr.execute(query, args)
return list(cr.fetchall())
@@ -249,10 +252,7 @@ def get_db_names(self):
if openerp.tools.config['db_name']:
db_names = openerp.tools.config['db_name'].split(',')
else:
- db_names = openerp.service.db.exp_list()
- dbfilter = openerp.tools.config['dbfilter']
- if dbfilter:
- db_names = [d for d in db_names if re.match(dbfilter, d)]
+ db_names = openerp.service.db.exp_list(True)
return db_names
def close_databases(self, remove_jobs=True):
@@ -285,9 +285,7 @@ def run_jobs(self):
_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('http://localhost:%s'
- '/connector/runjob?db=%s&job_uuid=%s' %
- (self.port, job.db_name, job.uuid))
+ _async_http_get(self.port, job.db_name, job.uuid)
def process_notifications(self):
for db in self.db_by_name.values():
diff --git a/connector/openerp-connector-worker b/connector/openerp-connector-worker
deleted file mode 100755
index 5bdde75ac..000000000
--- a/connector/openerp-connector-worker
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/usr/bin/env python
-import re
-import sys
-import logging
-import os
-import signal
-import time
-import threading
-from contextlib import closing
-from psycopg2 import ProgrammingError
-
-import openerp
-from openerp.cli import server as servercli
-import openerp.service.server as workers
-from openerp.modules.registry import RegistryManager
-from openerp.tools import config
-
-_logger = logging.getLogger('openerp-connector-worker')
-
-MAX_JOBS = 50
-
-
-class Multicornnector(workers.PreforkServer):
-
- def __init__(self, app):
- super(Multicornnector, self).__init__(app)
- self.address = ('0.0.0.0', 0)
- self.population = config['workers'] or 1
- self.workers_connector = {}
-
- def process_spawn(self):
- while len(self.workers_connector) < self.population:
- self.worker_spawn(WorkerConnector, self.workers_connector)
-
- def worker_pop(self, pid):
- if pid in self.workers:
- _logger.debug("Worker (%s) unregistered", pid)
- try:
- self.workers_connector.pop(pid, None)
- u = self.workers.pop(pid)
- u.close()
- except OSError:
- return
-
-
-class WorkerConnector(workers.Worker):
- """ HTTP Request workers """
-
- def __init__(self, multi):
- super(WorkerConnector, self).__init__(multi)
- self.db_index = 0
-
- def _work_database(self, cr):
- db_name = cr.dbname
- try:
- cr.execute("SELECT 1 FROM ir_module_module "
- "WHERE name = %s "
- "AND state = %s", ('connector', 'installed'),
- log_exceptions=False)
- except ProgrammingError as err:
- if unicode(err).startswith('relation "ir_module_module" does not exist'):
- _logger.debug('Database %s is not an OpenERP database,'
- ' connector worker not started', db_name)
- else:
- raise
- else:
- if cr.fetchone():
- RegistryManager.check_registry_signaling(db_name)
- registry = openerp.registry(db_name)
- if registry:
- queue_worker = registry['queue.worker']
- queue_worker.assign_then_enqueue(cr,
- openerp.SUPERUSER_ID,
- max_jobs=MAX_JOBS)
- RegistryManager.signal_caches_change(db_name)
-
- def process_work(self):
- with openerp.api.Environment.manage():
- if config['db_name']:
- db_names = config['db_name'].split(',')
- else:
- db_names = openerp.service.db.exp_list(True)
- dbfilter = config['dbfilter']
- if dbfilter and db_names:
- db_names = [d for d in db_names if re.match(dbfilter, d)]
- if len(db_names):
- self.db_index = (self.db_index + 1) % len(db_names)
- db_name = db_names[self.db_index]
- self.setproctitle(db_name)
- db = openerp.sql_db.db_connect(db_name)
- threading.current_thread().dbname = db_name
- with closing(db.cursor()) as cr:
- self._work_database(cr)
- else:
- self.db_index = 0
-
- def sleep(self):
- # Really sleep once all the databases have been processed.
- if self.db_index == 0:
- interval = 15 + self.pid % self.multi.population # chorus effect
- time.sleep(interval)
-
- def start(self):
- workers.Worker.start(self)
-
-
-if __name__ == "__main__":
- args = sys.argv[1:]
- servercli.check_root_user()
- config.parse_config(args)
-
- servercli.check_postgres_user()
- openerp.netsvc.init_logger()
- servercli.report_configuration()
-
- openerp.multi_process = True
- openerp.worker_connector = True
- Multicornnector(openerp.service.wsgi_server.application).run([], False)
diff --git a/connector/queue/__init__.py b/connector/queue/__init__.py
index 2bc97de03..643bee7ab 100644
--- a/connector/queue/__init__.py
+++ b/connector/queue/__init__.py
@@ -20,4 +20,3 @@
##############################################################################
from . import model
-from . import worker
diff --git a/connector/queue/job.py b/connector/queue/job.py
index 2558cc11d..7ea90d5ce 100644
--- a/connector/queue/job.py
+++ b/connector/queue/job.py
@@ -25,7 +25,8 @@
import uuid
import sys
from datetime import datetime, timedelta, MINYEAR
-from pickle import loads, dumps, UnpicklingError
+from cPickle import dumps, UnpicklingError, Unpickler
+from cStringIO import StringIO
import openerp
from openerp.tools.translate import _
@@ -54,6 +55,31 @@
_logger = logging.getLogger(__name__)
+_UNPICKLE_WHITELIST = set()
+
+
+def whitelist_unpickle_global(fn_or_class):
+ """ Allow a function or class to be used in jobs
+
+ By default, the only types allowed to be used in job arguments are:
+
+ * the builtins: str/unicode, int/long, float, bool, tuple, list, dict, None
+ * the pre-registered: datetime.datetime datetime.timedelta
+
+ If you need to use an argument in a job which is not in this whitelist,
+ you can add it by using::
+
+ whitelist_unpickle_global(fn_or_class_to_register)
+
+ """
+ _UNPICKLE_WHITELIST.add(fn_or_class)
+
+
+# register common types that might be used in job arguments
+whitelist_unpickle_global(datetime)
+whitelist_unpickle_global(timedelta)
+
+
def _unpickle(pickled):
""" Unpickles a string and catch all types of errors it can throw,
to raise only NotReadableJobError in case of error.
@@ -63,9 +89,26 @@ def _unpickle(pickled):
`loads()` may raises many types of exceptions (AttributeError,
IndexError, TypeError, KeyError, ...). They are all catched and
raised as `NotReadableJobError`).
+
+ Pickle could be exploited by an attacker who would write a value in a job
+ that would run arbitrary code when unpickled. This is why we set a custom
+ ``find_global`` method on the ``Unpickler``, only jobs and a whitelist of
+ classes/functions are allowed to be unpickled (plus the builtins types).
"""
+ def restricted_find_global(mod_name, fn_name):
+ __import__(mod_name)
+ mod = sys.modules[mod_name]
+ fn = getattr(mod, fn_name)
+ if not (fn in JOB_REGISTRY or fn in _UNPICKLE_WHITELIST):
+ raise UnpicklingError(
+ '{}.{} is not allowed in jobs'.format(mod_name, fn_name)
+ )
+ return fn
+
+ unpickler = Unpickler(StringIO(pickled))
+ unpickler.find_global = restricted_find_global
try:
- unpickled = loads(pickled)
+ unpickled = unpickler.load()
except (StandardError, UnpicklingError):
raise NotReadableJobError('Could not unpickle.', pickled)
return unpickled
@@ -91,13 +134,11 @@ class OpenERPJobStorage(JobStorage):
""" Store a job on OpenERP """
_job_model_name = 'queue.job'
- _worker_model_name = 'queue.worker'
def __init__(self, session):
super(OpenERPJobStorage, self).__init__()
self.session = session
self.job_model = self.session.env[self._job_model_name]
- self.worker_model = self.session.env[self._worker_model_name]
assert self.job_model is not None, (
"Model %s not found" % self._job_model_name)
@@ -120,7 +161,7 @@ def enqueue(self, func, model_name=None, args=None, kwargs=None,
company_model = company_model.sudo(new_job.user_id)
company_id = company_model._company_default_get(
object='queue.job',
- field='company_id')
+ field='company_id').id
new_job.company_id = company_id
self.store(new_job)
return new_job.uuid
@@ -148,18 +189,11 @@ def db_record_from_uuid(self, job_uuid):
model = self.job_model.sudo().with_context(active_test=False)
record = model.search([('uuid', '=', job_uuid)], limit=1)
if record:
- return record
+ return record.with_env(self.job_model.env)
def db_record(self, job_):
return self.db_record_from_uuid(job_.uuid)
- def _worker_id(self, worker_uuid):
- worker = self.worker_model.sudo().search(
- [('uuid', '=', worker_uuid)],
- limit=1)
- if worker:
- return worker.id
-
def store(self, job_):
""" Store the Job """
vals = {'state': job_.state,
@@ -190,11 +224,6 @@ def store(self, job_):
if job_.canceled:
vals['active'] = False
- if job_.worker_uuid:
- vals['worker_id'] = self._worker_id(job_.worker_uuid)
- else:
- vals['worker_id'] = False
-
db_record = self.db_record(job_)
if db_record:
db_record.write(vals)
@@ -254,8 +283,6 @@ def load(self, job_uuid):
job_.model_name = stored.model_name if stored.model_name else None
job_.retry = stored.retry
job_.max_retries = stored.max_retries
- if stored.worker_id:
- job_.worker_uuid = stored.worker_id.uuid
if stored.company_id:
job_.company_id = stored.company_id.id
return job_
@@ -268,10 +295,6 @@ class Job(object):
Id (UUID) of the job.
- .. attribute:: worker_uuid
-
- When the job is enqueued, UUID of the worker.
-
.. attribute:: state
State of the job, can pending, enqueued, started, done or failed.
@@ -441,7 +464,6 @@ def __init__(self, func=None, model_name=None,
self._eta = None
self.eta = eta
self.canceled = False
- self.worker_uuid = None
def __cmp__(self, other):
if not isinstance(other, Job):
@@ -465,8 +487,11 @@ def perform(self, session):
self.retry += 1
try:
self.result = self.func(session, *self.args, **self.kwargs)
- except RetryableJobError:
- if not self.max_retries: # infinite retries
+ 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()
@@ -533,17 +558,15 @@ def set_pending(self, result=None, reset_retry=True):
self.state = PENDING
self.date_enqueued = None
self.date_started = None
- self.worker_uuid = None
if reset_retry:
self.retry = 0
if result is not None:
self.result = result
- def set_enqueued(self, worker):
+ def set_enqueued(self):
self.state = ENQUEUED
self.date_enqueued = datetime.now()
self.date_started = None
- self.worker_uuid = worker.uuid
def set_started(self):
self.state = STARTED
@@ -553,13 +576,11 @@ def set_done(self, result=None):
self.state = DONE
self.exc_info = None
self.date_done = datetime.now()
- self.worker_uuid = None
if result is not None:
self.result = result
def set_failed(self, exc_info=None):
self.state = FAILED
- self.worker_uuid = None
if exc_info is not None:
self.exc_info = exc_info
@@ -691,7 +712,7 @@ def retryable_example(session):
# retries 5 to 10 postponed 20 minutes later
# retries 10 to 15 postponed 30 minutes later
# all subsequent retries postponed 12 hours later
- raise RetryableJobError
+ raise RetryableJobError('Must be retried later')
retryable_example.delay(session)
diff --git a/connector/queue/model.py b/connector/queue/model.py
index e215693ed..136a189b0 100644
--- a/connector/queue/model.py
+++ b/connector/queue/model.py
@@ -19,16 +19,13 @@
#
##############################################################################
-import os
import logging
from datetime import datetime, timedelta
from openerp import models, fields, api, exceptions, _
from .job import STATES, DONE, PENDING, OpenERPJobStorage, JOB_REGISTRY
-from .worker import WORKER_TIMEOUT
from ..session import ConnectorSession
-from .worker import watcher
from ..connector import get_openerp_module, is_module_installed
_logger = logging.getLogger(__name__)
@@ -45,11 +42,6 @@ class QueueJob(models.Model):
_removal_interval = 30 # days
- worker_id = fields.Many2one(comodel_name='queue.worker',
- string='Worker',
- ondelete='set null',
- select=True,
- readonly=True)
uuid = fields.Char(string='UUID',
readonly=True,
select=True,
@@ -209,181 +201,6 @@ def autovacuum(self):
return True
-class QueueWorker(models.Model):
- """ Worker """
- _name = 'queue.worker'
- _description = 'Queue Worker'
- _log_access = False
- _rec_name = 'uuid'
-
- worker_timeout = WORKER_TIMEOUT
-
- uuid = fields.Char(string='UUID',
- readonly=True,
- select=True,
- required=True)
- pid = fields.Char(string='PID', readonly=True)
- date_start = fields.Datetime(string='Start Date', readonly=True)
- date_alive = fields.Datetime(string='Last Alive Check', readonly=True)
- job_ids = fields.One2many(comodel_name='queue.job',
- inverse_name='worker_id',
- string='Jobs',
- readonly=True)
-
- @api.model
- def _notify_alive(self, worker):
- workers = self.search([('uuid', '=', worker.uuid)])
-
- now = fields.Datetime.now()
- if not workers:
- self.create({'uuid': worker.uuid,
- 'pid': os.getpid(),
- 'date_start': now,
- 'date_alive': now,
- })
- else:
- workers.write({'date_alive': now})
-
- @api.model
- def _purge_dead_workers(self):
- deadline = datetime.now() - timedelta(seconds=self.worker_timeout)
- deads = self.search(
- [('date_alive', '<', fields.Datetime.to_string(deadline))],
- )
- for worker in deads:
- _logger.debug('Worker %s is dead', worker.uuid)
- try:
- deads.unlink()
- except Exception:
- _logger.debug("Failed attempt to unlink a dead worker, likely due "
- "to another transaction in progress.")
-
- @api.model
- def _worker(self):
- worker = watcher.worker_for_db(self.env.cr.dbname)
- assert worker
- workers = self.search([('uuid', '=', worker.uuid)])
- assert len(workers) == 1, ("%s worker found in database instead "
- "of 1" % len(workers))
- return workers
-
- @api.model
- def assign_then_enqueue(self, max_jobs=None):
- """ Assign all the jobs not already assigned to a worker.
- Then enqueue all the jobs having a worker but not enqueued.
-
- Each operation is atomic.
-
- .. warning:: commit transaction
- ``cr.commit()`` is called, so please always call
- this method in your own transaction, not in the main
- OpenERP's transaction
-
- :param max_jobs: maximal limit of jobs to assign on a worker
- :type max_jobs: int
- """
- self.assign_jobs(max_jobs=max_jobs)
- self.env.cr.commit()
- self.enqueue_jobs()
- self.env.cr.commit()
- return True
-
- @api.model
- def assign_jobs(self, max_jobs=None):
- """ Assign ``n`` jobs to the worker of the current process
-
- ``n`` is ``max_jobs`` or unlimited if ``max_jobs`` is None
-
- :param max_jobs: maximal limit of jobs to assign on a worker
- :type max_jobs: int
- """
- worker = watcher.worker_for_db(self.env.cr.dbname)
- if worker:
- self._assign_jobs(max_jobs=max_jobs)
- else:
- _logger.debug('No worker started for process %s', os.getpid())
- return True
-
- @api.model
- def enqueue_jobs(self):
- """ Enqueue all the jobs assigned to the worker of the current
- process
- """
- worker = watcher.worker_for_db(self.env.cr.dbname)
- if worker:
- self._enqueue_jobs()
- else:
- _logger.debug('No worker started for process %s', os.getpid())
- return True
-
- @api.model
- def _assign_jobs(self, max_jobs=None):
- sql = ("SELECT id FROM queue_job "
- "WHERE worker_id IS NULL "
- "AND state not in ('failed', 'done') "
- "AND active = true "
- "ORDER BY eta NULLS LAST, priority, date_created ")
- if max_jobs is not None:
- sql += ' LIMIT %d' % max_jobs
- sql += ' FOR UPDATE NOWAIT'
- # use a SAVEPOINT to be able to rollback this part of the
- # transaction without failing the whole transaction if the LOCK
- # cannot be acquired
- worker = watcher.worker_for_db(self.env.cr.dbname)
- self.env.cr.execute("SAVEPOINT queue_assign_jobs")
- try:
- self.env.cr.execute(sql, log_exceptions=False)
- except Exception:
- # Here it's likely that the FOR UPDATE NOWAIT failed to get
- # the LOCK, so we ROLLBACK to the SAVEPOINT to restore the
- # transaction to its earlier state. The assign will be done
- # the next time.
- self.env.cr.execute("ROLLBACK TO queue_assign_jobs")
- _logger.debug("Failed attempt to assign jobs, likely due to "
- "another transaction in progress. "
- "Trace of the failed assignment of jobs on worker "
- "%s attempt: ", worker.uuid, exc_info=True)
- return
- job_rows = self.env.cr.fetchall()
- if not job_rows:
- _logger.debug('No job to assign to worker %s', worker.uuid)
- return
- job_ids = [id for id, in job_rows]
-
- try:
- worker_id = self._worker().id
- except AssertionError as e:
- _logger.exception(e)
- return
- _logger.debug('Assign %d jobs to worker %s', len(job_ids),
- worker.uuid)
- # ready to be enqueued in the worker
- try:
- self.env['queue.job'].browse(job_ids).write(
- {'state': 'pending',
- 'worker_id': worker_id,
- }
- )
- except Exception:
- pass # will be assigned to another worker
-
- def _enqueue_jobs(self):
- """ Add to the queue of the worker all the jobs not
- yet queued but already assigned."""
- job_model = self.env['queue.job']
- try:
- db_worker_id = self._worker().id
- except AssertionError as e:
- _logger.exception(e)
- return
- jobs = job_model.search([('worker_id', '=', db_worker_id),
- ('state', '=', 'pending')],
- )
- worker = watcher.worker_for_db(self.env.cr.dbname)
- for job in jobs:
- worker.enqueue_job_uuid(job.uuid)
-
-
class RequeueJob(models.TransientModel):
_name = 'queue.requeue.job'
_description = 'Wizard to requeue a selection of jobs'
diff --git a/connector/queue/model_view.xml b/connector/queue/model_view.xml
index 22047999e..c6dee32f9 100644
--- a/connector/queue/model_view.xml
+++ b/connector/queue/model_view.xml
@@ -2,50 +2,6 @@
-
- queue.worker.form
- queue.worker
-
-
-
-
-
-
- queue.worker.tree
- queue.worker
-
-
-
-
-
-
-
-
-
-
-
- Workers
- queue.worker
- form
- tree,form
- {}
-
-
-
queue.job.form
queue.job
@@ -83,7 +39,6 @@
-
diff --git a/connector/queue/queue_data.xml b/connector/queue/queue_data.xml
index 93465583a..4e4d11707 100644
--- a/connector/queue/queue_data.xml
+++ b/connector/queue/queue_data.xml
@@ -9,24 +9,6 @@
-
-
- Enqueue Jobs
-
-
- 1
- minutes
- -1
-
-
-
-
- 1
-
-
AutoVacuum Queue Jobs
diff --git a/connector/queue/worker.py b/connector/queue/worker.py
index e6a47b220..e9a5873f2 100644
--- a/connector/queue/worker.py
+++ b/connector/queue/worker.py
@@ -19,7 +19,6 @@
#
##############################################################################
-import re
import logging
import os
import threading
@@ -137,7 +136,7 @@ def retry_postpone(job, message, seconds=None):
except RetryableJobError as err:
# delay the job later, requeue
- retry_postpone(job, unicode(err))
+ retry_postpone(job, unicode(err), seconds=err.seconds)
_logger.debug('%s postponed', job)
except OperationalError as err:
@@ -261,9 +260,6 @@ def available_db_names():
db_names = config['db_name'].split(',')
else:
db_names = db.exp_list(True)
- dbfilter = config['dbfilter']
- if dbfilter and db_names:
- db_names = [d for d in db_names if re.match(dbfilter, d)]
available_db_names = []
for db_name in db_names:
session_hdl = ConnectorSessionHandler(db_name,
diff --git a/connector/security/ir.model.access.csv b/connector/security/ir.model.access.csv
index 35407b03a..377c11fb1 100644
--- a/connector/security/ir.model.access.csv
+++ b/connector/security/ir.model.access.csv
@@ -1,5 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_connector_queue_worker_manager,connector worker manager,connector.model_queue_worker,connector.group_connector_manager,1,1,1,1
access_connector_queue_job_manager,connector job manager,connector.model_queue_job,connector.group_connector_manager,1,1,1,1
access_connector_checkpoint_manager,connector checkpoint manager,connector.model_connector_checkpoint,connector.group_connector_manager,1,1,1,1
access_connector_queue_job_function_manager,connector job functions manager,connector.model_queue_job_function,connector.group_connector_manager,1,1,1,1
diff --git a/connector/session.py b/connector/session.py
index 93d6392ba..eecdf9f0a 100644
--- a/connector/session.py
+++ b/connector/session.py
@@ -27,7 +27,6 @@
from openerp.modules.registry import RegistryManager
from .connector import is_module_installed
-from .deprecate import log_deprecate
_logger = logging.getLogger(__name__)
@@ -77,9 +76,10 @@ def session(self):
context=self.context)
try:
- RegistryManager.check_registry_signaling(self.db_name)
- yield session
- RegistryManager.signal_caches_change(self.db_name)
+ with session.env.clear_upon_failure():
+ RegistryManager.check_registry_signaling(self.db_name)
+ yield session
+ RegistryManager.signal_caches_change(self.db_name)
except:
session.rollback()
raise
@@ -204,50 +204,6 @@ def close(self):
""" Close the cursor """
self.cr.close()
- def search(self, model, domain, limit=None, offset=0, order=None):
- """ Shortcut to :py:class:`openerp.models.BaseModel.search` """
- log_deprecate("'Session(...).search(...)' has been deprecated in "
- "favor of 'Session(...).env['model'].search(...)'")
- return self.pool[model].search(self.cr, self.uid, domain,
- limit=limit, offset=offset,
- order=order, context=self.context)
-
- def browse(self, model, ids):
- """ Shortcut to :py:class:`openerp.models.BaseModel.browse` """
- model_obj = self.pool[model]
- log_deprecate("'Session(...).browse(...)' has been deprecated in "
- "favor of 'Session(...).env['model'].browse(...)'")
- return model_obj.browse(self.cr, self.uid, ids, context=self.context)
-
- def read(self, model, ids, fields):
- """ Shortcut to :py:class:`openerp.models.BaseModel.read` """
- log_deprecate("'Session(...).read(...)' has been deprecated in "
- "favor of 'Session(...).env['model'].read(...)'")
- return self.pool[model].read(self.cr, self.uid, ids, fields,
- context=self.context)
-
- def create(self, model, values):
- """ Shortcut to :py:class:`openerp.models.BaseModel.create` """
- log_deprecate("'Session(...).create(...)' has been deprecated in "
- "favor of 'Session(...).env['model'].create(...)'")
- return self.pool[model].create(self.cr, self.uid, values,
- context=self.context)
-
- def write(self, model, ids, values):
- """ Shortcut to :py:class:`openerp.models.BaseModel.write` """
- log_deprecate("'Session(...).write(...)' has been deprecated in "
- "favor of 'Session(...).env['model'].write(...)'")
- return self.pool[model].write(self.cr, self.uid, ids, values,
- context=self.context)
-
- def unlink(self, model, ids):
- model_obj = self.pool[model]
- _logger.warning("'Session.unlink()' has been deprecated, prefer "
- "'self.env['model'].unlink()' or "
- "self.recordset().unlink() if you are in a "
- "ConnectorUnit.")
- return model_obj.unlink(self.cr, self.uid, ids, context=self.context)
-
def __repr__(self):
return '' % (self.cr.dbname,
self.uid,
diff --git a/connector/tests/common.py b/connector/tests/common.py
new file mode 100644
index 000000000..30034dc9a
--- /dev/null
+++ b/connector/tests/common.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+#
+#
+# Authors: Guewen Baconnier
+# Copyright 2015 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+#
+
+import importlib
+from contextlib import contextmanager
+
+import mock
+
+
+@contextmanager
+def mock_job_delay_to_direct(job_path):
+ """ Replace the .delay() of a job by a direct call
+
+ job_path is the python path as string, such as::
+
+ 'openerp.addons.magentoerpconnect.stock_picking.export_picking_done'
+
+ This is a context manager, all the calls made to the job function in
+ job_path inside the context manager will be executed synchronously.
+
+ .. note:: It uses :meth:`mock.patch` so it has the same pitfall
+ regarding the python path. If the mock seems to have no
+ effect, read `Where to patch
+ `_
+ in the mock documentation.
+
+ """
+ job_module, job_name = job_path.rsplit('.', 1)
+ module = importlib.import_module(job_module)
+ job_func = getattr(module, job_name, None)
+ assert job_func, "The function %s must exist in %s" % (job_name,
+ job_module)
+
+ def clean_args_for_func(*args, **kwargs):
+ # remove the special args reserved to '.delay()'
+ kwargs.pop('priority', None)
+ kwargs.pop('eta', None)
+ kwargs.pop('model_name', None)
+ kwargs.pop('max_retries', None)
+ kwargs.pop('description', None)
+ job_func(*args, **kwargs)
+
+ with mock.patch(job_path) as patched_job:
+ # call the function directly instead of '.delay()'
+ patched_job.delay.side_effect = clean_args_for_func
+ yield patched_job
diff --git a/connector/tests/test_backend.py b/connector/tests/test_backend.py
index 7a025bbeb..1655f8b4c 100644
--- a/connector/tests/test_backend.py
+++ b/connector/tests/test_backend.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-import unittest2
+import unittest
import openerp.tests.common as common
from openerp.addons.connector.backend import (Backend,
@@ -14,7 +14,7 @@
from openerp.addons.connector.session import ConnectorSession
-class test_backend(unittest2.TestCase):
+class test_backend(unittest.TestCase):
""" Test Backend """
def setUp(self):
diff --git a/connector/tests/test_connector.py b/connector/tests/test_connector.py
index 0d5393cf0..e5093cd04 100644
--- a/connector/tests/test_connector.py
+++ b/connector/tests/test_connector.py
@@ -1,16 +1,34 @@
# -*- coding: utf-8 -*-
import mock
-import unittest2
+import unittest
+from openerp import api
+from openerp.modules.registry import RegistryManager
from openerp.tests import common
from openerp.addons.connector import connector
-from openerp.addons.connector.connector import (ConnectorUnit,
- ConnectorEnvironment)
+from openerp.addons.connector.exception import RetryableJobError
+from openerp.addons.connector.connector import (
+ ConnectorEnvironment,
+ ConnectorUnit,
+ pg_try_advisory_lock,
+)
from openerp.addons.connector.session import ConnectorSession
-class ConnectorHelpers(unittest2.TestCase):
+def mock_connector_unit(env):
+ session = ConnectorSession(env.cr, env.uid,
+ context=env.context)
+ backend_record = mock.Mock(name='BackendRecord')
+ backend = mock.Mock(name='Backend')
+ backend_record.get_backend.return_value = backend
+ connector_env = connector.ConnectorEnvironment(backend_record,
+ session,
+ 'res.users')
+ return ConnectorUnit(connector_env)
+
+
+class ConnectorHelpers(unittest.TestCase):
def test_openerp_module_name(self):
name = connector._get_openerp_module_name('openerp.addons.sale')
@@ -19,7 +37,7 @@ def test_openerp_module_name(self):
self.assertEqual(name, 'sale')
-class TestConnectorUnit(unittest2.TestCase):
+class TestConnectorUnit(unittest.TestCase):
""" Test Connector Unit """
def test_connector_unit_for_model_names(self):
@@ -122,21 +140,13 @@ def test_instance(self):
class ModelUnit(ConnectorUnit):
_model_name = 'res.users'
- session = ConnectorSession(self.env.cr, self.env.uid,
- context=self.env.context)
- backend_record = mock.Mock(name='BackendRecord')
- backend = mock.Mock(name='Backend')
- backend_record.get_backend.return_value = backend
- connector_env = connector.ConnectorEnvironment(backend_record,
- session,
- 'res.users')
- unit = ConnectorUnit(connector_env)
+ unit = mock_connector_unit(self.env)
self.assertEqual(unit.model, self.env['res.users'])
self.assertEqual(unit.env, self.env)
self.assertEqual(unit.localcontext, self.env.context)
-class TestConnectorEnvironment(unittest2.TestCase):
+class TestConnectorEnvironment(unittest.TestCase):
def test_create_environment_no_connector_env(self):
session = mock.MagicMock(name='Session')
@@ -177,3 +187,47 @@ def __init__(self, backend_record, session, model_name, api=None):
self.assertEqual(type(new_env), MyConnectorEnvironment)
self.assertEqual(new_env.api, api)
+
+
+class TestAdvisoryLock(common.TransactionCase):
+
+ def setUp(self):
+ super(TestAdvisoryLock, self).setUp()
+ self.registry2 = RegistryManager.get(common.get_db_name())
+ self.cr2 = self.registry2.cursor()
+ self.env2 = api.Environment(self.cr2, self.env.uid, {})
+
+ @self.addCleanup
+ def reset_cr2():
+ # rollback and close the cursor, and reset the environments
+ self.env2.reset()
+ self.cr2.rollback()
+ self.cr2.close()
+
+ def test_concurrent_lock(self):
+ """ 2 concurrent transactions cannot acquire the same lock """
+ lock = 'import_record({}, {}, {}, {})'.format(
+ 'backend.name',
+ 1,
+ 'res.partner',
+ '999999',
+ )
+ acquired = pg_try_advisory_lock(self.env, lock)
+ self.assertTrue(acquired)
+ inner_acquired = pg_try_advisory_lock(self.env2, lock)
+ self.assertFalse(inner_acquired)
+
+ def test_concurrent_import_lock(self):
+ """ A 2nd concurrent transaction must retry """
+ lock = 'import_record({}, {}, {}, {})'.format(
+ 'backend.name',
+ 1,
+ 'res.partner',
+ '999999',
+ )
+ connector_unit = mock_connector_unit(self.env)
+ connector_unit.advisory_lock_or_retry(lock)
+ connector_unit2 = mock_connector_unit(self.env2)
+ with self.assertRaises(RetryableJobError) as cm:
+ connector_unit2.advisory_lock_or_retry(lock, retry_seconds=3)
+ self.assertEquals(cm.exception.seconds, 3)
diff --git a/connector/tests/test_job.py b/connector/tests/test_job.py
index 0d0e072cf..b456f2409 100644
--- a/connector/tests/test_job.py
+++ b/connector/tests/test_job.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
+import cPickle
import mock
-import unittest2
+import unittest
from datetime import datetime, timedelta
from openerp import SUPERUSER_ID, exceptions
@@ -48,10 +49,19 @@ def dummy_task_args(session, model_name, a, b, c=None):
def retryable_error_task(session):
- raise RetryableJobError
+ raise RetryableJobError('Must be retried later')
-class TestJobs(unittest2.TestCase):
+def pickle_forbidden_function(session):
+ pass
+
+
+@job
+def pickle_allowed_function(session):
+ pass
+
+
+class TestJobs(unittest.TestCase):
""" Test Job """
def setUp(self):
@@ -233,24 +243,19 @@ def test_set_pending(self):
self.assertEquals(job_a.state, PENDING)
self.assertFalse(job_a.date_enqueued)
self.assertFalse(job_a.date_started)
- self.assertFalse(job_a.worker_uuid)
self.assertEquals(job_a.retry, 0)
self.assertEquals(job_a.result, 'test')
def test_set_enqueued(self):
job_a = Job(func=task_a)
- worker = mock.Mock(name='Worker')
- uuid = 'ae7d1161-dc34-40b1-af06-8057c049133e'
- worker.uuid = 'ae7d1161-dc34-40b1-af06-8057c049133e'
datetime_path = 'openerp.addons.connector.queue.job.datetime'
with mock.patch(datetime_path, autospec=True) as mock_datetime:
mock_datetime.now.return_value = datetime(2015, 3, 15, 16, 41, 0)
- job_a.set_enqueued(worker)
+ job_a.set_enqueued()
self.assertEquals(job_a.state, ENQUEUED)
self.assertEquals(job_a.date_enqueued,
datetime(2015, 3, 15, 16, 41, 0))
- self.assertEquals(job_a.worker_uuid, uuid)
self.assertFalse(job_a.date_started)
def test_set_started(self):
@@ -275,7 +280,6 @@ def test_set_done(self):
self.assertEquals(job_a.result, 'test')
self.assertEquals(job_a.date_done,
datetime(2015, 3, 15, 16, 41, 0))
- self.assertFalse(job_a.worker_uuid)
self.assertFalse(job_a.exc_info)
def test_set_failed(self):
@@ -283,7 +287,6 @@ def test_set_failed(self):
job_a.set_failed(exc_info='failed test')
self.assertEquals(job_a.state, FAILED)
self.assertEquals(job_a.exc_info, 'failed test')
- self.assertFalse(job_a.worker_uuid)
def test_cancel(self):
job_a = Job(func=task_a)
@@ -310,6 +313,23 @@ def test_unpickle(self):
'a small cucumber preserved in vinegar, '
'brine, or a similar solution.')
+ def test_unpickle_unsafe(self):
+ """ unpickling function not decorated by @job is forbidden """
+ pickled = cPickle.dumps(pickle_forbidden_function)
+ with self.assertRaises(NotReadableJobError):
+ _unpickle(pickled)
+
+ def test_unpickle_safe(self):
+ """ unpickling function decorated by @job is allowed """
+ pickled = cPickle.dumps(pickle_allowed_function)
+ self.assertEqual(_unpickle(pickled), pickle_allowed_function)
+
+ def test_unpickle_whitelist(self):
+ """ unpickling function/class that is in the whitelist is allowed """
+ arg = datetime(2016, 2, 10)
+ pickled = cPickle.dumps(arg)
+ self.assertEqual(_unpickle(pickled), arg)
+
def test_unpickle_not_readable(self):
with self.assertRaises(NotReadableJobError):
self.assertEqual(_unpickle('cucumber'))
@@ -394,21 +414,6 @@ def test_read(self):
delta=delta)
self.assertEqual(job_read.canceled, True)
- def test_job_worker(self):
- worker = self.env['queue.worker'].create(
- {'uuid': '57569b99-c2c1-47b6-aad1-72f953c92c87'}
- )
- test_job = Job(func=dummy_task_args,
- model_name='res.users',
- args=('o', 'k'),
- kwargs={'c': '!'})
- test_job.worker_uuid = worker.uuid
- storage = OpenERPJobStorage(self.session)
- self.assertEqual(storage._worker_id(worker.uuid), worker.id)
- storage.store(test_job)
- job_read = storage.load(test_job.uuid)
- self.assertEqual(job_read.worker_uuid, worker.uuid)
-
def test_job_unlinked(self):
test_job = Job(func=dummy_task_args,
model_name='res.users',
@@ -486,12 +491,13 @@ def setUp(self):
super(TestJobModel, self).setUp()
self.session = ConnectorSession(self.cr, self.uid)
self.queue_job = self.env['queue.job']
+ self.user = self.env['res.users']
def _create_job(self):
test_job = Job(func=task_a)
storage = OpenERPJobStorage(self.session)
storage.store(test_job)
- stored = self.queue_job.search([('uuid', '=', test_job.uuid)])
+ stored = storage.db_record_from_uuid(test_job.uuid)
self.assertEqual(len(stored), 1)
return stored
@@ -519,7 +525,6 @@ def test_requeue(self):
stored.write({'state': 'failed'})
stored.requeue()
self.assertEqual(stored.state, PENDING)
- self.assertFalse(stored.worker_id)
def test_message_when_write_fail(self):
stored = self._create_job()
@@ -528,6 +533,25 @@ def test_message_when_write_fail(self):
messages = stored.message_ids
self.assertEqual(len(messages), 2)
+ def test_follower_when_write_fail(self):
+ """Check that inactive users doesn't are not followers even if
+ they are linked to an active partner"""
+ group = self.env.ref('connector.group_connector_manager')
+ vals = {'name': 'xx',
+ 'login': 'xx',
+ 'groups_id': [(6, 0, [group.id])],
+ 'active': False,
+ }
+ inactiveusr = self.user.create(vals)
+ inactiveusr.partner_id.active = True
+ self.assertFalse(inactiveusr in group.users)
+ stored = self._create_job()
+ stored.write({'state': 'failed'})
+ followers = stored.message_follower_ids.mapped('partner_id')
+ self.assertFalse(inactiveusr.partner_id in followers)
+ self.assertFalse(
+ set([u.partner_id for u in group.users]) - set(followers))
+
def test_autovacuum(self):
stored = self._create_job()
stored2 = self._create_job()
@@ -544,7 +568,6 @@ def test_wizard_requeue(self):
active_ids=stored.ids)
model.create({}).requeue()
self.assertEqual(stored.state, PENDING)
- self.assertFalse(stored.worker_id)
class TestJobStorageMultiCompany(common.TransactionCase):
@@ -647,9 +670,10 @@ def test_job_subscription(self):
)
self.assertEqual(len(stored.message_follower_ids), len(users))
expected_partners = [u.partner_id for u in users]
- self.assertSetEqual(set(stored.message_follower_ids),
- set(expected_partners))
- followers_id = [f.id for f in stored.message_follower_ids]
+ self.assertSetEqual(
+ set(stored.message_follower_ids.mapped('partner_id')),
+ set(expected_partners))
+ followers_id = stored.message_follower_ids.mapped('partner_id.id')
self.assertIn(self.other_partner_a.id, followers_id)
self.assertIn(self.other_partner_b.id, followers_id)
# jobs created for a specific company_id are followed only by
@@ -662,9 +686,10 @@ def test_job_subscription(self):
self.assertEqual(len(stored.message_follower_ids), 2)
users = User.browse([SUPERUSER_ID, self.other_user_a.id])
expected_partners = [u.partner_id for u in users]
- self.assertSetEqual(set(stored.message_follower_ids),
- set(expected_partners))
- followers_id = [f.id for f in stored.message_follower_ids]
+ self.assertSetEqual(
+ set(stored.message_follower_ids.mapped('partner_id')),
+ set(expected_partners))
+ followers_id = stored.message_follower_ids.mapped('partner_id.id')
self.assertIn(self.other_partner_a.id, followers_id)
self.assertNotIn(self.other_partner_b.id, followers_id)
diff --git a/connector/tests/test_mapper.py b/connector/tests/test_mapper.py
index e3b0bd605..6777c8ee9 100644
--- a/connector/tests/test_mapper.py
+++ b/connector/tests/test_mapper.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-import unittest2
+import unittest
import mock
import openerp.tests.common as common
@@ -25,7 +25,7 @@
from openerp.addons.connector.session import ConnectorSession
-class test_mapper(unittest2.TestCase):
+class test_mapper(unittest.TestCase):
""" Test Mapper """
def test_mapping_decorator(self):
diff --git a/connector/tests/test_queue.py b/connector/tests/test_queue.py
index 8a56b854f..c533bc9ed 100644
--- a/connector/tests/test_queue.py
+++ b/connector/tests/test_queue.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-import unittest2
+import unittest
from datetime import timedelta
from openerp.addons.connector.queue.queue import JobsQueue
@@ -11,7 +11,7 @@ def dummy_task(session):
pass
-class test_queue(unittest2.TestCase):
+class test_queue(unittest.TestCase):
""" Test Queue """
def setUp(self):
diff --git a/connector/tests/test_related_action.py b/connector/tests/test_related_action.py
index 687b4986a..240299ef2 100644
--- a/connector/tests/test_related_action.py
+++ b/connector/tests/test_related_action.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import mock
-import unittest2
+import unittest
import openerp.tests.common as common
from ..connector import Binder
@@ -47,7 +47,7 @@ def try_unwrap_binding(session, model_name, binding_id):
pass
-class test_related_action(unittest2.TestCase):
+class test_related_action(unittest.TestCase):
""" Test Related Actions """
def setUp(self):
diff --git a/connector/tests/test_session.py b/connector/tests/test_session.py
index 27a389b48..8ded6877a 100644
--- a/connector/tests/test_session.py
+++ b/connector/tests/test_session.py
@@ -149,20 +149,3 @@ def test_is_module_installed_cache_not_propagated(self):
""" Test if the cache is well different for the different modules """
self.assertTrue(self.session.is_module_installed('connector'))
self.assertFalse(self.session.is_module_installed('#dummy#'))
-
- def test_is_module_installed_cache_invalidation(self):
- """ Test on an invalidation of cache about installed modules """
- module = self.env['ir.module.module']
- domain = [('name', '=', 'base')]
- self.assertTrue(self.session.is_module_installed('base'))
- # only to check that the cache works, the in validation is done only
- # if the field state is modified by write method, UGLY but no other
- # solution
- self.env.cr.execute("UPDATE ir_module_module "
- "SET state='uninstalled' "
- "WHERE name='base'")
- self.assertTrue(self.session.is_module_installed('base'))
- module.search(domain).state = 'uninstalled'
- self.assertFalse(self.session.is_module_installed('base'))
- module.search(domain).state = 'installed'
- self.assertTrue(self.session.is_module_installed('base'))
diff --git a/connector/tests/test_worker.py b/connector/tests/test_worker.py
index d55c9eb5a..499ee33ee 100644
--- a/connector/tests/test_worker.py
+++ b/connector/tests/test_worker.py
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
-import unittest2
+import unittest
from openerp.addons.connector.queue.queue import JobsQueue
-class test_worker(unittest2.TestCase):
+class test_worker(unittest.TestCase):
""" Test Worker """
def setUp(self):
diff --git a/connector/unit/synchronizer.py b/connector/unit/synchronizer.py
index 86b3515f3..1302e0086 100644
--- a/connector/unit/synchronizer.py
+++ b/connector/unit/synchronizer.py
@@ -90,20 +90,11 @@ class Exporter(Synchronizer):
_base_mapper = ExportMapper
-ExportSynchronizer = Exporter # deprecated
-
-
class Importer(Synchronizer):
""" Synchronizer for importing data from a backend to OpenERP """
_base_mapper = ImportMapper
-ImportSynchronizer = Importer # deprecated
-
-
class Deleter(Synchronizer):
""" Synchronizer for deleting a record on the backend """
-
-
-DeleteSynchronizer = Deleter # deprecated