diff --git a/openerp-wsgi.py b/openerp-wsgi.py index 90f43ddab21..4a3f34d5764 100644 --- a/openerp-wsgi.py +++ b/openerp-wsgi.py @@ -40,6 +40,8 @@ conf['addons_path'] = '../../addons/trunk,../../web/trunk/addons' #---------------------------------------------------------- application = openerp.service.wsgi_server.application +openerp.service.server.load_server_wide_modules() + #---------------------------------------------------------- # Gunicorn #---------------------------------------------------------- diff --git a/openerp/addons/base/__init__.py b/openerp/addons/base/__init__.py index 26b39de74ad..237e3a22cba 100644 --- a/openerp/addons/base/__init__.py +++ b/openerp/addons/base/__init__.py @@ -24,7 +24,6 @@ import workflow import module import res import report -import test import tests # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index 3c3c290f986..57ce6b31e32 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -57,6 +57,7 @@ The kernel of OpenERP, needed for all installation. 'ir/ir_values_view.xml', 'ir/osv_memory_autovacuum.xml', 'ir/ir_model_report.xml', + 'ir/ir_logging_view.xml', 'workflow/workflow_view.xml', 'module/module_view.xml', 'module/module_data.xml', diff --git a/openerp/addons/base/i18n/ab.po b/openerp/addons/base/i18n/ab.po index 7dfa69f0083..6b28e22586f 100644 --- a/openerp/addons/base/i18n/ab.po +++ b/openerp/addons/base/i18n/ab.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:32+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:30+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/af.po b/openerp/addons/base/i18n/af.po index 5b295dc2bcd..248585cfa4a 100644 --- a/openerp/addons/base/i18n/af.po +++ b/openerp/addons/base/i18n/af.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:32+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:30+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/am.po b/openerp/addons/base/i18n/am.po index e3894d9bc14..dce06ee8cd5 100644 --- a/openerp/addons/base/i18n/am.po +++ b/openerp/addons/base/i18n/am.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:33+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:31+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/ar.po b/openerp/addons/base/i18n/ar.po index 3c37c5afe93..15a0ba086b4 100644 --- a/openerp/addons/base/i18n/ar.po +++ b/openerp/addons/base/i18n/ar.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:33+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:31+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/bg.po b/openerp/addons/base/i18n/bg.po index 05e12422635..87b6fde3494 100644 --- a/openerp/addons/base/i18n/bg.po +++ b/openerp/addons/base/i18n/bg.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:34+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:32+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/bn.po b/openerp/addons/base/i18n/bn.po index 6d91016de62..18d9cb25618 100644 --- a/openerp/addons/base/i18n/bn.po +++ b/openerp/addons/base/i18n/bn.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:34+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:32+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/bs.po b/openerp/addons/base/i18n/bs.po index 0e806d0408b..64560df5bfc 100644 --- a/openerp/addons/base/i18n/bs.po +++ b/openerp/addons/base/i18n/bs.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:34+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:32+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/ca.po b/openerp/addons/base/i18n/ca.po index f262fe3ed8d..5e9a509e2f8 100644 --- a/openerp/addons/base/i18n/ca.po +++ b/openerp/addons/base/i18n/ca.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:35+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:32+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/cs.po b/openerp/addons/base/i18n/cs.po index 74fb7b939dc..d5f8a06d703 100644 --- a/openerp/addons/base/i18n/cs.po +++ b/openerp/addons/base/i18n/cs.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:35+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:33+0000\n" +"X-Generator: Launchpad (build 16967)\n" "X-Poedit-Language: Czech\n" #. module: base @@ -64,7 +64,7 @@ msgstr "Zobrazení architektury" #. module: base #: model:ir.module.module,summary:base.module_sale_stock msgid "Quotation, Sale Orders, Delivery & Invoicing Control" -msgstr "Nabídky, zakázky, řízení dopravy a fakturace" +msgstr "Nabídky, zakázky, řízení dodávek a fakturace" #. module: base #: selection:ir.sequence,implementation:0 @@ -87,8 +87,7 @@ msgid "" "Helps you manage your projects and tasks by tracking them, generating " "plannings, etc..." msgstr "" -"Pomůže vám spravovat projekty a úkoly jejich sledováním, generováním plánů, " -"apod." +"Pomůže vám řídit projekty a úkoly jejich sledováním, generováním plánů, aj." #. module: base #: model:ir.module.module,summary:base.module_point_of_sale @@ -158,7 +157,7 @@ msgstr "" #. module: base #: help:res.partner,employee:0 msgid "Check this box if this contact is an Employee." -msgstr "Zaškrtněte toto pole, pokud je kontakt zaměstnanec." +msgstr "Zaškrtněte toto pole, pokud je kontakt zaměstnancem." #. module: base #: help:ir.model.fields,domain:0 @@ -11056,7 +11055,7 @@ msgstr "Úkoly" #. module: base #: model:ir.module.module,shortdesc:base.module_product_visible_discount msgid "Prices Visible Discounts" -msgstr "" +msgstr "Zobrazení slevy u ceny" #. module: base #: field:ir.attachment,datas:0 diff --git a/openerp/addons/base/i18n/da.po b/openerp/addons/base/i18n/da.po index fdd4121f77b..14414eccc68 100644 --- a/openerp/addons/base/i18n/da.po +++ b/openerp/addons/base/i18n/da.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:35+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:33+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/de.po b/openerp/addons/base/i18n/de.po index 4c9d52d776b..1e0a1c51e4c 100644 --- a/openerp/addons/base/i18n/de.po +++ b/openerp/addons/base/i18n/de.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:37+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:35+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/el.po b/openerp/addons/base/i18n/el.po index 3235391d7a1..2428ce6643d 100644 --- a/openerp/addons/base/i18n/el.po +++ b/openerp/addons/base/i18n/el.po @@ -12,8 +12,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:37+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:35+0000\n" +"X-Generator: Launchpad (build 16967)\n" "X-Poedit-Country: GREECE\n" "X-Poedit-Language: Greek\n" "X-Poedit-SourceCharset: utf-8\n" diff --git a/openerp/addons/base/i18n/en_GB.po b/openerp/addons/base/i18n/en_GB.po index 19199e3984f..3ac3d49d9ad 100644 --- a/openerp/addons/base/i18n/en_GB.po +++ b/openerp/addons/base/i18n/en_GB.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:46+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:44+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/es.po b/openerp/addons/base/i18n/es.po index 94dd68dd4fb..3289f1acd3c 100644 --- a/openerp/addons/base/i18n/es.po +++ b/openerp/addons/base/i18n/es.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:43+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:41+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/es_AR.po b/openerp/addons/base/i18n/es_AR.po index 5dc6f4ff6f1..ff156da38c1 100644 --- a/openerp/addons/base/i18n/es_AR.po +++ b/openerp/addons/base/i18n/es_AR.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:45+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:43+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/es_BO.po b/openerp/addons/base/i18n/es_BO.po index 01752c54c0c..4d8aa3b9f5e 100644 --- a/openerp/addons/base/i18n/es_BO.po +++ b/openerp/addons/base/i18n/es_BO.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:47+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:45+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/es_CL.po b/openerp/addons/base/i18n/es_CL.po index 92f2f2fb43e..e5fb10c6ca3 100644 --- a/openerp/addons/base/i18n/es_CL.po +++ b/openerp/addons/base/i18n/es_CL.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:46+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:44+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/es_CR.po b/openerp/addons/base/i18n/es_CR.po index 5a6c3036f2c..ff0e9053b68 100644 --- a/openerp/addons/base/i18n/es_CR.po +++ b/openerp/addons/base/i18n/es_CR.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:47+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:45+0000\n" +"X-Generator: Launchpad (build 16967)\n" "Language: \n" #. module: base diff --git a/openerp/addons/base/i18n/es_DO.po b/openerp/addons/base/i18n/es_DO.po index 66c6c7dec57..c95ba0fcad8 100644 --- a/openerp/addons/base/i18n/es_DO.po +++ b/openerp/addons/base/i18n/es_DO.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:46+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:44+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/es_EC.po b/openerp/addons/base/i18n/es_EC.po index ee6e0f006bd..6126d7c2a6f 100644 --- a/openerp/addons/base/i18n/es_EC.po +++ b/openerp/addons/base/i18n/es_EC.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:48+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:46+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/es_MX.po b/openerp/addons/base/i18n/es_MX.po index 6dbee6f72f5..332d7384c82 100644 --- a/openerp/addons/base/i18n/es_MX.po +++ b/openerp/addons/base/i18n/es_MX.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:48+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:46+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/es_PE.po b/openerp/addons/base/i18n/es_PE.po index 5c582182184..cbdab27c57d 100644 --- a/openerp/addons/base/i18n/es_PE.po +++ b/openerp/addons/base/i18n/es_PE.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:48+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:46+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/es_VE.po b/openerp/addons/base/i18n/es_VE.po index c77bb1d8597..15260ad574a 100644 --- a/openerp/addons/base/i18n/es_VE.po +++ b/openerp/addons/base/i18n/es_VE.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:45+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:43+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/et.po b/openerp/addons/base/i18n/et.po index 2d49cc78eda..1192725aac4 100644 --- a/openerp/addons/base/i18n/et.po +++ b/openerp/addons/base/i18n/et.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:35+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:33+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/eu.po b/openerp/addons/base/i18n/eu.po index f2e9b88d7f1..6fabc952d95 100644 --- a/openerp/addons/base/i18n/eu.po +++ b/openerp/addons/base/i18n/eu.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:33+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:31+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/fa.po b/openerp/addons/base/i18n/fa.po index b322c074c5f..1b0e025410e 100644 --- a/openerp/addons/base/i18n/fa.po +++ b/openerp/addons/base/i18n/fa.po @@ -9,8 +9,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:41+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:39+0000\n" +"X-Generator: Launchpad (build 16967)\n" "X-Poedit-Country: IRAN, ISLAMIC REPUBLIC OF\n" "X-Poedit-Language: Persian\n" diff --git a/openerp/addons/base/i18n/fa_AF.po b/openerp/addons/base/i18n/fa_AF.po index 06d57f17ed5..af0a0e59729 100644 --- a/openerp/addons/base/i18n/fa_AF.po +++ b/openerp/addons/base/i18n/fa_AF.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:49+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:47+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/fi.po b/openerp/addons/base/i18n/fi.po index 9884ef8b1ff..5646cffb9c9 100644 --- a/openerp/addons/base/i18n/fi.po +++ b/openerp/addons/base/i18n/fi.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:36+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:34+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing @@ -25,7 +25,7 @@ msgid "" " " msgstr "" "\n" -"Moduuli Šekkien kirjoittamisen ja tulostamiseen.\n" +"Moduuli Shekkien kirjoittamisen ja tulostamiseen.\n" "===========================================\n" " " @@ -129,7 +129,7 @@ msgstr "" "Moduuli joka lisää valmistajat ja ominaisuudet tuotelomakkeelle.\n" "=========================================================\n" "\n" -"Voit net määritellä tuotteelle seuraavat tiedot:\n" +"Voit nyt määritellä tuotteelle seuraavat tiedot:\n" "---------------------------------------------------------\n" " * Valmistaja\n" " * Valmistajan tuotenimi\n" @@ -405,6 +405,13 @@ msgid "" "invoices from picking, OpenERP is able to add and compute the shipping " "line.\n" msgstr "" +"\n" +"Sallii toimitustapojen lisäämisen myyntitilauksille ja keräilyyn. \n" +"==============================================================\n" +"\n" +"Voit määritellä oman huolitsijan ja toimitustaulukon hinnoille. Kun laskut " +"luodaan \n" +"keräilyssä, OpenERP lisää lähetysrivit ja laskee nille hinnat.\n" #. module: base #: code:addons/base/ir/ir_filters.py:80 @@ -550,7 +557,7 @@ msgstr "" #. module: base #: view:workflow.transition:0 msgid "Workflow Transition" -msgstr "" +msgstr "Työnkulun eteneminen" #. module: base #: model:res.country,name:base.gf @@ -599,6 +606,20 @@ msgid "" "* Use emails to automatically confirm and send acknowledgements for any " "event registration\n" msgstr "" +"\n" +"Tapahtumien organisointi ja hallinta.\n" +"====================================\n" +"\n" +"Tapahtumamoduuli mahdollistaa tehokkaan tapahtumien organisoinnin ja niihin " +"liittyvien \n" +"töiden hallinnan: suunnittelun, rekisteröintien seurannan, osallistumisen " +"jne. \n" +"\n" +"Pääominaisuudet\n" +"------------\n" +"* Hallitse tapahtumia ja rekisteröintejä\n" +"* Käytä sähköpostia automatisoimaan vahvistuksia ja lähettämään kuittaus " +"jokaisesta rekisteröitymisestä.\n" #. module: base #: selection:base.language.install,lang:0 @@ -688,7 +709,7 @@ msgstr "Palau" #. module: base #: view:res.partner:0 msgid "Sales & Purchases" -msgstr "Myynti ja hankinta" +msgstr "Myynnit & Ostot" #. module: base #: view:ir.translation:0 @@ -1616,6 +1637,11 @@ msgid "" "=============\n" " " msgstr "" +"\n" +"Tämä moduuli lisää palautevalikon ja toimii jos Palauteportaali on " +"asennettu. \n" +"=======================================================================\n" +" " #. module: base #: model:ir.actions.act_window,help:base.action_res_partner_bank_account_form @@ -1815,7 +1841,7 @@ msgstr "" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_project_issue msgid "Portal Issue" -msgstr "Porttaaliongelma" +msgstr "Portaaliongelma" #. module: base #: model:ir.ui.menu,name:base.menu_tools @@ -1950,7 +1976,7 @@ msgstr "Alankomaat" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_event msgid "Portal Event" -msgstr "Porttaalitapahtuma" +msgstr "Portaalitapahtuma" #. module: base #: selection:ir.translation,state:0 @@ -3836,7 +3862,7 @@ msgstr "Raportointi" #: field:res.partner,title:0 #: field:res.partner.title,name:0 msgid "Title" -msgstr "Yhtiömuoto" +msgstr "Titteli" #. module: base #: help:ir.property,res_id:0 @@ -6164,7 +6190,7 @@ msgstr "Ryhmän nimi ei voi alkaa \"-\"" #: view:ir.module.module:0 #: model:ir.ui.menu,name:base.module_mi msgid "Apps" -msgstr "Sovellukset" +msgstr "Sovelluskauppa" #. module: base #: view:ir.ui.view_sc:0 @@ -7726,7 +7752,7 @@ msgstr "Tämä ikkuna" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_anonymous msgid "Anonymous portal" -msgstr "Yleinen porttaali" +msgstr "Anonyymiportaali" #. module: base #: field:base.language.export,format:0 @@ -7784,7 +7810,7 @@ msgstr "Salasana" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_claim msgid "Portal Claim" -msgstr "Reklmaatioporttaali" +msgstr "Palauteportaali" #. module: base #: model:ir.module.module,shortdesc:base.module_l10n_pe @@ -7837,7 +7863,7 @@ msgstr "RML sisäinen ylätunniste" #. module: base #: field:ir.actions.act_window,search_view_id:0 msgid "Search View Ref." -msgstr "Hae näytettävä viite" +msgstr "Hae näytettävä referenssi" #. module: base #: help:res.users,partner_id:0 @@ -8029,7 +8055,7 @@ msgstr "Liike- ja analyyttinen kirjanpito" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_project msgid "Portal Project" -msgstr "Porttaaliprojekti" +msgstr "Portaaliprojekti" #. module: base #: model:res.country,name:base.cc @@ -8538,7 +8564,7 @@ msgstr "Hongkong" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_sale msgid "Portal Sale" -msgstr "Myyntiporttaali" +msgstr "Myyntiportaali" #. module: base #: field:ir.default,ref_id:0 @@ -8662,7 +8688,7 @@ msgstr "Alavalikot" #. module: base #: report:ir.module.reference:0 msgid "Introspection report on objects" -msgstr "Introspektioraportti objekteista" +msgstr "Moduulin tekninen opas" #. module: base #: model:ir.module.module,shortdesc:base.module_web_analytics @@ -8814,7 +8840,7 @@ msgstr "" #. module: base #: model:ir.ui.menu,name:base.menu_view_base_module_update msgid "Update Modules List" -msgstr "Päivitä lista moduuleista" +msgstr "Päivitä moduulilista" #. module: base #: code:addons/base/module/module.py:359 @@ -10021,7 +10047,7 @@ msgstr "Kenttien kuvaukset" #. module: base #: model:ir.module.module,shortdesc:base.module_analytic_contract_hr_expense msgid "Contracts Management: hr_expense link" -msgstr "Sopimushallint: hr_expense link" +msgstr "Sopimushallinat: hr_expense link" #. module: base #: view:ir.attachment:0 @@ -10138,7 +10164,7 @@ msgstr "" #: view:res.groups:0 #: field:res.partner,comment:0 msgid "Notes" -msgstr "Huomautukset" +msgstr "Muistiinpanot" #. module: base #: field:ir.config_parameter,value:0 @@ -10283,7 +10309,7 @@ msgstr "Väliyksikkö" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_stock msgid "Portal Stock" -msgstr "Porttaalivarasto" +msgstr "Portaalivarasto" #. module: base #: field:workflow.activity,kind:0 @@ -10351,7 +10377,7 @@ msgstr "Kohdetietueen ID tietokannassa" #. module: base #: model:ir.module.module,shortdesc:base.module_account_analytic_analysis msgid "Contracts Management" -msgstr "Sopimustenhallinta" +msgstr "Sopimushallinta" #. module: base #: selection:base.language.install,lang:0 @@ -10455,7 +10481,7 @@ msgstr "Moniyritys" #: model:ir.module.category,name:base.module_category_portal #: model:ir.module.module,shortdesc:base.module_portal msgid "Portal" -msgstr "Porttaali" +msgstr "Portaali" #. module: base #: selection:ir.translation,state:0 @@ -10656,7 +10682,7 @@ msgstr "Tehtävät" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_hr_employees msgid "Portal HR employees" -msgstr "Henkilöstöhallinnon työntekijäporttaali" +msgstr "Henkilöstöhallinnon työntekijäportaali" #. module: base #: selection:base.language.install,lang:0 @@ -12137,7 +12163,7 @@ msgstr "Menetelmälle välitettävät argumentit, esim. (uid,...)." #. module: base #: report:ir.module.reference:0 msgid "Reference Guide" -msgstr "Referenssiopaste" +msgstr "Tekninen opas" #. module: base #: model:ir.model,name:base.model_res_partner @@ -12595,7 +12621,7 @@ msgstr "Malediivit" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_crm msgid "Portal CRM" -msgstr "CRM-porttaali" +msgstr "CRM-portaali" #. module: base #: model:ir.ui.menu,name:base.next_id_4 @@ -14878,7 +14904,7 @@ msgstr "Bulgaria / български език" #. module: base #: model:ir.ui.menu,name:base.menu_aftersale msgid "After-Sale Services" -msgstr "Jälkimarkkinapalvelut" +msgstr "Asiakaspalvelut" #. module: base #: field:base.language.import,code:0 @@ -15482,7 +15508,7 @@ msgstr "Matkapuhelinnumero" #: model:ir.model,name:base.model_res_partner_category #: view:res.partner.category:0 msgid "Partner Categories" -msgstr "Kumppanien ryhmät" +msgstr "Kumppaniryhmät" #. module: base #: view:base.module.upgrade:0 @@ -15623,7 +15649,7 @@ msgstr "" #: model:ir.actions.act_window,name:base.open_module_tree #: model:ir.ui.menu,name:base.menu_module_tree msgid "Installed Modules" -msgstr "Asennetut moduulit" +msgstr "Asenna moduulit" #. module: base #: code:addons/base/res/res_users.py:170 @@ -15690,7 +15716,7 @@ msgstr "" #. module: base #: view:res.partner:0 msgid "Internal Notes" -msgstr "Sisäiset huomautukset" +msgstr "Sisäiset muistiinpanot" #. module: base #: model:res.partner.title,name:base.res_partner_title_pvt_ltd diff --git a/openerp/addons/base/i18n/fr.po b/openerp/addons/base/i18n/fr.po index 7b2512d903e..a7cb0c9fd67 100644 --- a/openerp/addons/base/i18n/fr.po +++ b/openerp/addons/base/i18n/fr.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:36+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:34+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/fr_CA.po b/openerp/addons/base/i18n/fr_CA.po index 780bc801f39..c132e8506f3 100644 --- a/openerp/addons/base/i18n/fr_CA.po +++ b/openerp/addons/base/i18n/fr_CA.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:46+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:44+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing @@ -29,7 +29,7 @@ msgstr "" #. module: base #: model:res.country,name:base.sh msgid "Saint Helena" -msgstr "" +msgstr "Sainte Hélène" #. module: base #: view:ir.actions.report.xml:0 @@ -39,7 +39,7 @@ msgstr "" #. module: base #: selection:ir.property,type:0 msgid "DateTime" -msgstr "" +msgstr "Date et heure" #. module: base #: code:addons/fields.py:652 @@ -68,7 +68,7 @@ msgstr "" #. module: base #: selection:base.language.install,lang:0 msgid "Hungarian / Magyar" -msgstr "" +msgstr "Hongrois / Magyar" #. module: base #: selection:base.language.install,lang:0 @@ -81,6 +81,8 @@ msgid "" "Helps you manage your projects and tasks by tracking them, generating " "plannings, etc..." msgstr "" +"Vous aide à gérer vos projets et tâches, en assurant leur suivi, en générant " +"des planifications, etc." #. module: base #: model:ir.module.module,summary:base.module_point_of_sale @@ -221,7 +223,7 @@ msgstr "" #. module: base #: model:res.country,name:base.sz msgid "Swaziland" -msgstr "" +msgstr "Swaziland" #. module: base #: code:addons/orm.py:4485 @@ -352,7 +354,7 @@ msgstr "" #. module: base #: model:ir.module.category,name:base.module_category_customer_relationship_management msgid "Customer Relationship Management" -msgstr "" +msgstr "Gestion de la relation client (CRM)" #. module: base #: model:ir.module.module,description:base.module_delivery @@ -389,14 +391,14 @@ msgstr "" #. module: base #: field:res.partner,credit_limit:0 msgid "Credit Limit" -msgstr "" +msgstr "Limite de crédit" #. module: base #: field:ir.model.constraint,date_update:0 #: field:ir.model.data,date_update:0 #: field:ir.model.relation,date_update:0 msgid "Update Date" -msgstr "" +msgstr "Mettre à jour la date" #. module: base #: model:ir.module.module,shortdesc:base.module_base_action_rule @@ -407,12 +409,12 @@ msgstr "" #: view:ir.attachment:0 #: field:ir.attachment,create_uid:0 msgid "Owner" -msgstr "" +msgstr "Propriétaire" #. module: base #: view:ir.actions.act_window:0 msgid "Source Object" -msgstr "" +msgstr "Objet source" #. module: base #: model:res.partner.bank.type,format_layout:base.bank_normal @@ -434,7 +436,7 @@ msgstr "" #: field:ir.model.access,group_id:0 #: view:res.groups:0 msgid "Group" -msgstr "" +msgstr "Groupe" #. module: base #: constraint:res.lang:0 @@ -442,6 +444,9 @@ msgid "" "Invalid date/time format directive specified. Please refer to the list of " "allowed directives, displayed when you edit a language." msgstr "" +"Vous avez spécifié une directive de format date/temps non valide. Veuillez " +"vous référer à la liste des directives autorisées, qui s'affiche lorsque " +"vous modifiez une langue." #. module: base #: code:addons/orm.py:4153 @@ -555,6 +560,19 @@ msgid "" "* Use emails to automatically confirm and send acknowledgements for any " "event registration\n" msgstr "" +"\n" +"Gestion et organisation d'événements\n" +"====================================\n" +"\n" +"Le module 'Événement' vous permet d'organiser efficacement des événements et " +"toutes les tâches liées : planification, suivi des inscriptions, des " +"présences, etc.\n" +"\n" +"Fonctionnalités clés\n" +"--------------------\n" +"* Gestion des événements et des inscriptions\n" +"* Utilisation d'e-mails pour automatiquement confirmer et envoyer des " +"accusés de réception pour chaque inscription à un événement\n" #. module: base #: selection:base.language.install,lang:0 @@ -567,6 +585,8 @@ msgid "" "If you check this, then the second time the user prints with same attachment " "name, it returns the previous report." msgstr "" +"Si vous cochez ceci, alors la seconde fois que l'utilisateur imprimera avec " +"le même nom de pièce jointe, cela retournera le rapport stocké" #. module: base #: model:ir.module.module,description:base.module_mrp_byproduct @@ -597,7 +617,7 @@ msgstr "" #. module: base #: model:ir.module.module,shortdesc:base.module_hr_timesheet_invoice msgid "Invoice on Timesheets" -msgstr "" +msgstr "Facture sur les feuilles de temps" #. module: base #: view:base.module.upgrade:0 @@ -613,12 +633,12 @@ msgstr "" #. module: base #: field:res.country,name:0 msgid "Country Name" -msgstr "" +msgstr "Nom du pays" #. module: base #: model:res.country,name:base.co msgid "Colombia" -msgstr "" +msgstr "Colombie" #. module: base #: model:res.partner.title,name:base.res_partner_title_mister @@ -640,7 +660,7 @@ msgstr "" #. module: base #: view:res.partner:0 msgid "Sales & Purchases" -msgstr "" +msgstr "Achats et ventes" #. module: base #: view:ir.translation:0 @@ -721,6 +741,33 @@ msgid "" "module named account_voucher.\n" " " msgstr "" +"\n" +"Comptabilité et gestion financière\n" +"==================================\n" +"\n" +"Le module de comptabilité et de finance couvre :\n" +"------------------------------------------------\n" +" * Comptabilité générale\n" +" * Comptabilité de coût / comptabilité analytique\n" +" * Comptabilité de tiers\n" +" * Gestion des taxes\n" +" * Budgets\n" +" * Factures clients et fournisseurs\n" +" * Relevés bancaires\n" +" * Réconciliations par partenaire\n" +"\n" +"Il créé également un tableau de bord pour les comptables qui inclut :\n" +"---------------------------------------------------------------------\n" +" * Une liste des factures clients à approuver\n" +" * L'analyse de la société\n" +" * Un graphe de la trésorerie\n" +"\n" +"Les processus tels que le maintien de la comptabilité générale se fait par " +"le biais des revues financières définies (les lignes d'écriture à regrouper " +"le sont à travers un journal)\n" +"pour un exercice donné et pour la préparation des reçus il ya un module " +"nommé account_voucher.\n" +" " #. module: base #: view:ir.model:0 @@ -763,7 +810,7 @@ msgstr "" #. module: base #: model:res.country,name:base.jo msgid "Jordan" -msgstr "" +msgstr "Jordanie" #. module: base #: model:ir.module.module,shortdesc:base.module_l10n_hr @@ -788,7 +835,7 @@ msgstr "" #. module: base #: sql_constraint:res.company:0 msgid "The company name must be unique !" -msgstr "" +msgstr "Le nom de la société doit être unique !" #. module: base #: model:ir.ui.menu,name:base.menu_base_action_rule_admin @@ -820,6 +867,10 @@ msgid "" "invoice, then `object.invoice_address_id.mobile` is the field which gives " "the correct mobile number" msgstr "" +"Fournit les champs qui seront utilisés pour récupérer le numéro du " +"cellulaire. Exemple: vous sélectionnez une facture, alors, " +"`object.invoice_address_id.mobile` est le champ qui représente le numéro du " +"cellulaire correct." #. module: base #: view:ir.mail_server:0 @@ -840,7 +891,7 @@ msgstr "" #: field:base.language.export,name:0 #: field:ir.attachment,datas_fname:0 msgid "File Name" -msgstr "" +msgstr "Nom du fichier" #. module: base #: model:res.country,name:base.rs @@ -891,6 +942,33 @@ msgid "" "also possible in order to automatically create a meeting when a holiday " "request is accepted by setting up a type of meeting in Leave Type.\n" msgstr "" +"\n" +"Gère les absences et les demandes de congés\n" +"===========================================\n" +"\n" +"Cette application contrôle la programmation des congés de votre société. Il " +"permet aux employés de faire des demandes de congés. Ensuite, les managers " +"peuvent examiner les demandes et les approuver ou les rejeter. De cette " +"façon, vous pouvez contrôler la planification de l'ensemble des congés de " +"votre entreprise ou de votre département.\n" +"\n" +"Vous pouvez configurer plusieurs sortes d'absences (maladie, congés payés, " +"congés sans soldes…) et allouer rapidement ces absences à un employé ou un " +"département en utilisant les demandes de congés. Un employé peut aussi faire " +"une demande de congés pour plus de jours en ajoutant une allocation. Cela va " +"augmenter le total des jours disponibles pour ce type de congés (si la " +"demande est acceptée).\n" +"\n" +"Vous pouvez suivre les absences de différentes façons grâce aux rapports " +"suivants : \n" +"\n" +"* Résumé des absences\n" +"* Absences par département\n" +"* Analyse des absences\n" +"\n" +"Une synchronisation avec les agendas internes (Réunions du module CRM) est " +"aussi possible dans le but de créer automatiquement une réunion quand une " +"demande de congés est accepté en ajoutant une type de réunion Absence.\n" #. module: base #: selection:base.language.install,lang:0 @@ -900,7 +978,7 @@ msgstr "" #. module: base #: model:ir.ui.menu,name:base.menu_crm_config_opportunity msgid "Opportunities" -msgstr "" +msgstr "Opportunités" #. module: base #: model:ir.model,name:base.model_base_language_export @@ -910,7 +988,7 @@ msgstr "" #. module: base #: model:res.country,name:base.pg msgid "Papua New Guinea" -msgstr "" +msgstr "Papouasie-Nouvelle-Guinée" #. module: base #: help:ir.actions.report.xml,report_type:0 @@ -941,7 +1019,7 @@ msgstr "" #. module: base #: model:res.country,name:base.zw msgid "Zimbabwe" -msgstr "" +msgstr "Zimbabwe" #. module: base #: help:ir.model.constraint,type:0 @@ -957,7 +1035,7 @@ msgstr "" #. module: base #: model:res.country,name:base.es msgid "Spain" -msgstr "" +msgstr "Espagne" #. module: base #: help:ir.actions.act_window,domain:0 @@ -978,17 +1056,17 @@ msgstr "" #. module: base #: field:res.partner,mobile:0 msgid "Mobile" -msgstr "" +msgstr "Cellulaire" #. module: base #: model:res.country,name:base.om msgid "Oman" -msgstr "" +msgstr "Oman" #. module: base #: model:ir.module.module,shortdesc:base.module_mrp msgid "MRP" -msgstr "" +msgstr "MRP - PBM" #. module: base #: model:ir.module.module,description:base.module_hr_attendance @@ -1026,12 +1104,12 @@ msgstr "" #: model:ir.actions.act_window,name:base.act_menu_create #: view:wizard.ir.model.menu.create:0 msgid "Create Menu" -msgstr "" +msgstr "Créer un menu" #. module: base #: model:res.country,name:base.in msgid "India" -msgstr "" +msgstr "Inde" #. module: base #: model:ir.actions.act_window,name:base.res_request_link-act @@ -1047,7 +1125,7 @@ msgstr "" #. module: base #: model:ir.module.module,shortdesc:base.module_fleet msgid "Fleet Management" -msgstr "" +msgstr "Gestion de flotte" #. module: base #: help:ir.server.object.lines,value:0 @@ -1107,6 +1185,36 @@ msgid "" "* Planned Revenue by Stage and User (graph)\n" "* Opportunities by Stage (graph)\n" msgstr "" +"\n" +"Le module OpenERP de gestion de la relation client (CRM)\n" +"=============================================\n" +"\n" +"Cette application permet aux utilisateurs de gérer efficacement et " +"intelligemment leurs pistes, opportunités, réunions et appels " +"téléphoniques.\n" +"\n" +"Elle gère les tâches clés comme la communication, l'identification, les " +"assignations, les résolutions et les notifications.\n" +"\n" +"OpenERP assure que toutes les demandes sont suivis par les utilisateurs, les " +"clients et les fournisseurs. Il permet d'envoyer automatiquement des " +"rappels, d'escalader des requêtes, de lancer des actions spécifiques et de " +"nombreuses autres selon les règles de l'entreprise.\n" +"\n" +"Le grand avantage de ce système est que les utilisateurs n'ont rien à faire " +"de spécial. Le module CRM dispose d'une passerelle de courriel permettant de " +"synchroniser les courriels reçus et OpenERP. De cette façon, les " +"utilisateurs peuvent simplement envoyer des courriels au système de suivi " +"des demandes.\n" +"\n" +"OpenERP s'occupera de remercier les utilisateurs de leur message, de diriger " +"cette demande à l'équipe appropriée et de s'assurer que toute les " +"communications futures seront rangées à la bonne place.\n" +"\n" +"\n" +"Le tableau bord de cette application inclus:\n" +"* les revenus prévus par étape de vente et par utilisateur (graphique);\n" +"* les opportunités par étape de vente (graphique).\n" #. module: base #: selection:base.language.export,format:0 @@ -1241,17 +1349,17 @@ msgstr "" #: selection:ir.actions.server,state:0 #: selection:workflow.activity,kind:0 msgid "Dummy" -msgstr "" +msgstr "Factice" #. module: base #: constraint:ir.ui.view:0 msgid "Invalid XML for View Architecture!" -msgstr "" +msgstr "XML invalide pour l'architecture de la vue" #. module: base #: model:res.country,name:base.ky msgid "Cayman Islands" -msgstr "" +msgstr "Îles Caïmans" #. module: base #: view:ir.rule:0 @@ -1287,7 +1395,7 @@ msgstr "" #. module: base #: selection:ir.property,type:0 msgid "Char" -msgstr "" +msgstr "Caractère" #. module: base #: field:ir.module.category,visible:0 @@ -1365,6 +1473,9 @@ msgid "" "decimal number [00,53]. All days in a new year preceding the first Monday " "are considered to be in week 0." msgstr "" +"%W - Numéro de semaine dans l'année (lundi est le premier jour de la " +"semaine) en nombre décimal [00,53]. Tous les jours de la nouvelle année " +"précédant le premier lundi sont considérés faisant partie de la semaine 0." #. module: base #: code:addons/base/module/wizard/base_language_install.py:53 @@ -1390,13 +1501,13 @@ msgstr "" #. module: base #: field:ir.actions.act_url,url:0 msgid "Action URL" -msgstr "" +msgstr "URL de l'action" #. module: base #: field:base.module.import,module_name:0 #: field:ir.module.module,shortdesc:0 msgid "Module Name" -msgstr "" +msgstr "Nom du module" #. module: base #: model:res.country,name:base.mh @@ -1423,7 +1534,7 @@ msgstr "" #: view:ir.ui.view:0 #: selection:ir.ui.view,type:0 msgid "Search" -msgstr "" +msgstr "Recherche" #. module: base #: code:addons/osv.py:154 @@ -1451,12 +1562,12 @@ msgstr "" #: code:addons/base/res/res_users.py:131 #, python-format msgid "Operation Canceled" -msgstr "" +msgstr "Opération annulée" #. module: base #: model:ir.module.module,shortdesc:base.module_document msgid "Document Management System" -msgstr "" +msgstr "Système de gestion des documents" #. module: base #: model:ir.module.module,shortdesc:base.module_crm_claim @@ -1499,7 +1610,7 @@ msgstr "" #: model:ir.module.category,name:base.module_category_purchase_management #: model:ir.ui.menu,name:base.menu_purchase_root msgid "Purchases" -msgstr "" +msgstr "Achats" #. module: base #: model:res.country,name:base.md @@ -1524,12 +1635,12 @@ msgstr "" #. module: base #: view:ir.module.module:0 msgid "Features" -msgstr "" +msgstr "Fonctionnalités" #. module: base #: view:ir.attachment:0 msgid "Data" -msgstr "" +msgstr "Données" #. module: base #: model:ir.module.module,description:base.module_portal_claim @@ -1597,7 +1708,7 @@ msgstr "" #: model:ir.module.category,name:base.module_category_social_network #: model:ir.module.module,shortdesc:base.module_mail msgid "Social Network" -msgstr "" +msgstr "Réseau social" #. module: base #: view:res.lang:0 @@ -12871,7 +12982,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ao msgid "Angola" -msgstr "" +msgstr "Angola" #. module: base #: model:res.country,name:base.tf diff --git a/openerp/addons/base/i18n/gl.po b/openerp/addons/base/i18n/gl.po index f0708a9968f..68ab1d36f65 100644 --- a/openerp/addons/base/i18n/gl.po +++ b/openerp/addons/base/i18n/gl.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:37+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:35+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/gu.po b/openerp/addons/base/i18n/gu.po index a663cc2336f..cf0edaaecf7 100644 --- a/openerp/addons/base/i18n/gu.po +++ b/openerp/addons/base/i18n/gu.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:37+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:35+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/he.po b/openerp/addons/base/i18n/he.po index 546bd34144e..70d012d1998 100644 --- a/openerp/addons/base/i18n/he.po +++ b/openerp/addons/base/i18n/he.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:37+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:35+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/hi.po b/openerp/addons/base/i18n/hi.po index fdb6f8a7733..e790f6b39e1 100644 --- a/openerp/addons/base/i18n/hi.po +++ b/openerp/addons/base/i18n/hi.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:38+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:36+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/hr.po b/openerp/addons/base/i18n/hr.po index 0b634c571a8..44627278dbc 100644 --- a/openerp/addons/base/i18n/hr.po +++ b/openerp/addons/base/i18n/hr.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:42+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:40+0000\n" +"X-Generator: Launchpad (build 16967)\n" "Language: hr\n" #. module: base diff --git a/openerp/addons/base/i18n/hu.po b/openerp/addons/base/i18n/hu.po index 91c34ae6657..b812e7ee525 100644 --- a/openerp/addons/base/i18n/hu.po +++ b/openerp/addons/base/i18n/hu.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:38+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:36+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/hy.po b/openerp/addons/base/i18n/hy.po index b7cd2bb6b60..6275f5896e6 100644 --- a/openerp/addons/base/i18n/hy.po +++ b/openerp/addons/base/i18n/hy.po @@ -9,8 +9,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:33+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:31+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/id.po b/openerp/addons/base/i18n/id.po index dc136feb360..f69a0ae481d 100644 --- a/openerp/addons/base/i18n/id.po +++ b/openerp/addons/base/i18n/id.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:38+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:36+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing @@ -211,6 +211,15 @@ msgid "" "revenue\n" "reports." msgstr "" +"\n" +"Bentuk tagihan anda dari entri Expenses, Timesheet.\n" +"========================================================\n" +"\n" +"Modul untuk menerbitkan tagihan berbasis biaya (sdm, biaya, dsb).\n" +"\n" +"Anda dapat mendefinisikan daftar harga pada akun analitik, membuat laporan " +"anggaran \n" +"penghasilan." #. module: base #: code:addons/base/ir/ir_sequence.py:134 @@ -299,6 +308,12 @@ msgid "" "\n" " " msgstr "" +"\n" +"Lokalisasi skema akunting dan pajak Chili.\n" +"==============================================\n" +"Plan contable chileno e impuestos de acuerdo a disposiciones vigentes\n" +"\n" +" " #. module: base #: model:ir.module.module,shortdesc:base.module_sale @@ -398,6 +413,15 @@ msgid "" "invoices from picking, OpenERP is able to add and compute the shipping " "line.\n" msgstr "" +"\n" +"Mengizinkan anda untuk menambahkan metode deliveri pada order penjualan dan " +"pengambilan.\n" +"==============================================================\n" +"\n" +"Anda dapat menentukan carrier sendiri dan metode antar untuk harga tersebut. " +"Saat membuat\n" +"tagihan dari pengantaran, OpenERP dapat menambah dan menghitung biaya " +"pengiriman.\n" #. module: base #: code:addons/base/ir/ir_filters.py:80 diff --git a/openerp/addons/base/i18n/is.po b/openerp/addons/base/i18n/is.po index 8f82dae30ee..ee314bae98b 100644 --- a/openerp/addons/base/i18n/is.po +++ b/openerp/addons/base/i18n/is.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:38+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:36+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/it.po b/openerp/addons/base/i18n/it.po index af28660269a..d76609aa203 100644 --- a/openerp/addons/base/i18n/it.po +++ b/openerp/addons/base/i18n/it.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:39+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:36+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/ja.po b/openerp/addons/base/i18n/ja.po index abca6dce0dd..03850c59478 100644 --- a/openerp/addons/base/i18n/ja.po +++ b/openerp/addons/base/i18n/ja.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:39+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:37+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing @@ -411,7 +411,7 @@ msgstr "与信限度" #: field:ir.model.data,date_update:0 #: field:ir.model.relation,date_update:0 msgid "Update Date" -msgstr "日付の更新" +msgstr "更新日" #. module: base #: model:ir.module.module,shortdesc:base.module_base_action_rule @@ -2295,7 +2295,7 @@ msgstr "メソッド" #. module: base #: model:ir.module.module,shortdesc:base.module_auth_crypt msgid "Password Encryption" -msgstr "" +msgstr "パスワード暗号化" #. module: base #: view:workflow.activity:0 @@ -3262,7 +3262,7 @@ msgstr "アルメニア" #. module: base #: model:ir.module.module,summary:base.module_hr_evaluation msgid "Periodical Evaluations, Appraisals, Surveys" -msgstr "" +msgstr "定期評価、査定、調査" #. module: base #: model:ir.actions.act_window,name:base.ir_property_form @@ -4582,7 +4582,7 @@ msgstr "スペイン語(ホンジュラス)/ Español (HN)" #. module: base #: view:ir.sequence.type:0 msgid "Sequence Type" -msgstr "順序タイプ" +msgstr "付番タイプ" #. module: base #: view:base.language.export:0 @@ -5141,7 +5141,7 @@ msgstr "銀行口座タイプ" #. module: base #: help:ir.sequence,suffix:0 msgid "Suffix value of the record for the sequence" -msgstr "順序のためのレコードのサフィックス値" +msgstr "この付番定義でレコードに適用するサフィックス値" #. module: base #: help:ir.mail_server,smtp_user:0 @@ -5588,7 +5588,7 @@ msgstr "起動設定ウィザード" #. module: base #: model:ir.module.module,summary:base.module_mrp msgid "Manufacturing Orders, Bill of Materials, Routing" -msgstr "" +msgstr "製造オーダ、部品表、工順" #. module: base #: field:ir.attachment,name:0 @@ -6140,7 +6140,7 @@ msgstr "ショートカット" #. module: base #: field:ir.model.data,date_init:0 msgid "Init Date" -msgstr "整数データ" +msgstr "作成日" #. module: base #: selection:base.language.install,lang:0 @@ -7630,7 +7630,7 @@ msgstr "" #. module: base #: help:ir.sequence,number_next:0 msgid "Next number of this sequence" -msgstr "この順序の次の番号" +msgstr "この付番定義の次の番号" #. module: base #: view:res.partner:0 @@ -7954,7 +7954,7 @@ msgstr "カスタム項目" #. module: base #: model:ir.module.module,summary:base.module_account_accountant msgid "Financial and Analytic Accounting" -msgstr "" +msgstr "財務会計、分析会計" #. module: base #: model:ir.module.module,shortdesc:base.module_portal_project @@ -8016,6 +8016,12 @@ msgid "" "

\n" " " msgstr "" +"

\n" +" クリックしてアドレス帳に連絡先を追加してください。\n" +"

\n" +" OpenERPは顧客、ディスカッション、商機の履歴、文書など関連するすべての活動の追跡を容易にします。\n" +"

\n" +" " #. module: base #: model:ir.actions.act_window,name:base.bank_account_update @@ -8242,7 +8248,7 @@ msgstr "" #. module: base #: model:ir.module.module,summary:base.module_hr_recruitment msgid "Jobs, Recruitment, Applications, Job Interviews" -msgstr "" +msgstr "仕事、求人、応募、面接" #. module: base #: code:addons/base/module/module.py:539 @@ -8651,7 +8657,7 @@ msgstr "グループのユーザは自動的にグループを継承します。 #. module: base #: model:ir.module.module,summary:base.module_note msgid "Sticky notes, Collaborative, Memos" -msgstr "" +msgstr "付箋、コラボレーション、メモ" #. module: base #: model:ir.module.module,shortdesc:base.module_hr_attendance @@ -9789,7 +9795,7 @@ msgstr "ドイツ" #. module: base #: model:ir.module.module,shortdesc:base.module_auth_oauth msgid "OAuth2 Authentication" -msgstr "" +msgstr "OAuth2認証" #. module: base #: view:workflow:0 @@ -9985,7 +9991,7 @@ msgstr "" #. module: base #: model:ir.module.module,summary:base.module_hr_holidays msgid "Holidays, Allocation and Leave Requests" -msgstr "" +msgstr "休暇割当、申請・承認" #. module: base #: model:ir.module.module,description:base.module_web_hello @@ -10032,6 +10038,12 @@ msgid "" "

\n" " " msgstr "" +"

\n" +" クリックしてアドレス帳に連絡先を追加してください。\n" +"

\n" +" OpenERPは仕入先、ディスカッション、購入の履歴、文書など関連するすべての活動の追跡を容易にします。\n" +"

\n" +" " #. module: base #: model:res.country,name:base.lr @@ -10123,7 +10135,7 @@ msgstr "発注分析計画" #: model:ir.actions.act_window,name:base.ir_sequence_type #: model:ir.ui.menu,name:base.menu_ir_sequence_type msgid "Sequence Codes" -msgstr "順序コード" +msgstr "付番コード" #. module: base #: selection:base.language.install,lang:0 @@ -10778,7 +10790,7 @@ msgstr "国の状態" #. module: base #: model:ir.ui.menu,name:base.next_id_5 msgid "Sequences & Identifiers" -msgstr "順序と識別子" +msgstr "付番定義とID" #. module: base #: model:ir.module.module,description:base.module_l10n_th @@ -10913,7 +10925,7 @@ msgstr "連絡先が会社の場合はチェックしてください。チェッ #. module: base #: view:ir.sequence.type:0 msgid "Sequences Type" -msgstr "順序タイプ" +msgstr "付番タイプ" #. module: base #: view:res.partner:0 @@ -11270,7 +11282,7 @@ msgstr "" #: view:ir.sequence:0 #: model:ir.ui.menu,name:base.menu_ir_sequence_form msgid "Sequences" -msgstr "順序" +msgstr "付番" #. module: base #: help:res.lang,code:0 @@ -11712,7 +11724,7 @@ msgstr "月:%(month)s" #: field:multi_company.default,sequence:0 #: field:res.partner.bank,sequence:0 msgid "Sequence" -msgstr "順序" +msgstr "付番" #. module: base #: model:res.country,name:base.tn @@ -12178,7 +12190,7 @@ msgstr "ハード島とマクドナルド諸島" msgid "" "External Key/Identifier that can be used for data integration with third-" "party systems" -msgstr "サードパーティのシステムとのデータ統合のために利用する外部キー / 識別子" +msgstr "サードパーティシステムとのデータ統合のために利用する外部キー / ID" #. module: base #: field:ir.actions.act_window,view_id:0 @@ -12507,7 +12519,7 @@ msgstr "CRMポータル" #. module: base #: model:ir.ui.menu,name:base.next_id_4 msgid "Low Level Objects" -msgstr "低レベルのオブジェクト" +msgstr "低レベルオブジェクト" #. module: base #: help:ir.values,model:0 @@ -12584,7 +12596,7 @@ msgstr "" #: view:ir.model.data:0 #: model:ir.ui.menu,name:base.ir_model_data_menu msgid "External Identifiers" -msgstr "外部識別子" +msgstr "外部ID" #. module: base #: selection:base.language.install,lang:0 @@ -12886,7 +12898,7 @@ msgstr "ガボン" #. module: base #: model:ir.module.module,summary:base.module_stock msgid "Inventory, Logistic, Storage" -msgstr "" +msgstr "在庫、経路、保管場所" #. module: base #: view:ir.actions.act_window:0 @@ -13602,7 +13614,7 @@ msgstr "" #: view:ir.model.data:0 #: field:ir.model.data,name:0 msgid "External Identifier" -msgstr "外部識別子" +msgstr "外部ID" #. module: base #: model:ir.module.module,description:base.module_audittrail @@ -14036,9 +14048,7 @@ msgid "" "Two sequence object implementations are offered: Standard and 'No gap'. The " "later is slower than the former but forbids any gap in the sequence (while " "they are possible in the former)." -msgstr "" -"2つのシーケンスオブジェクトの実装が提供されます:標準と\"ギャップなし\"です。後者は前者よりも遅いですが、どんな順序のギャップを禁じます(前者は可能で" -"す)。" +msgstr "「標準」と「ギャップなし」の2種類の付番定義が可能です。後者の場合、付番ごとにギャップを設けることができません。" #. module: base #: model:res.country,name:base.gn @@ -14237,7 +14247,7 @@ msgstr "かつ" #: help:ir.values,res_id:0 msgid "" "Database identifier of the record to which this applies. 0 = for all records" -msgstr "これを適用するためのレコードのデータベース識別子。0 = 全てのレコードのため" +msgstr "" #. module: base #: field:ir.model.fields,relation:0 @@ -14664,7 +14674,7 @@ msgstr "高度なレポート" #. module: base #: model:ir.module.module,summary:base.module_purchase msgid "Purchase Orders, Receptions, Supplier Invoices" -msgstr "" +msgstr "発注、入荷、仕入先請求" #. module: base #: model:ir.module.module,description:base.module_hr_payroll @@ -14891,7 +14901,7 @@ msgstr "クック諸島" #. module: base #: field:ir.model.data,noupdate:0 msgid "Non Updatable" -msgstr "更新可能でない" +msgstr "更新不可" #. module: base #: selection:base.language.install,lang:0 @@ -15299,7 +15309,7 @@ msgstr "" #. module: base #: help:ir.sequence,prefix:0 msgid "Prefix value of the record for the sequence" -msgstr "順序のためのレコードのプレフィックス値" +msgstr "この付番定義でレコードに適用するプレフィックス値" #. module: base #: model:res.country,name:base.sc @@ -15341,7 +15351,7 @@ msgstr "" #. module: base #: field:ir.model.data,complete_name:0 msgid "Complete ID" -msgstr "完了ID" +msgstr "完全ID" #. module: base #: model:ir.module.module,description:base.module_stock @@ -15435,7 +15445,7 @@ msgstr "製造プロセスを管理し、それらのプロセスのレポート #. module: base #: help:ir.sequence,number_increment:0 msgid "The next number of the sequence will be incremented by this number" -msgstr "順序の次の番号はこの数値だけ増加します。" +msgstr "付番毎にこの数値分だけ番号が増加します。" #. module: base #: selection:workflow.activity,kind:0 @@ -15563,6 +15573,12 @@ msgid "" "

\n" " " msgstr "" +"

\n" +" クリックしてアドレス帳に連絡先を追加してください。\n" +"

\n" +" OpenERPは顧客、ディスカッション、商機の履歴、文書など関連するすべての活動の追跡を容易にします。\n" +"

\n" +" " #. module: base #: model:res.partner.category,name:base.res_partner_category_2 diff --git a/openerp/addons/base/i18n/ka.po b/openerp/addons/base/i18n/ka.po index 23febc50180..837f7516cc2 100644 --- a/openerp/addons/base/i18n/ka.po +++ b/openerp/addons/base/i18n/ka.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:36+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:34+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/kk.po b/openerp/addons/base/i18n/kk.po index be14353dbbb..5592883f098 100644 --- a/openerp/addons/base/i18n/kk.po +++ b/openerp/addons/base/i18n/kk.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:39+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:37+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/ko.po b/openerp/addons/base/i18n/ko.po index 7f8a6294d67..50fb6e38537 100644 --- a/openerp/addons/base/i18n/ko.po +++ b/openerp/addons/base/i18n/ko.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:39+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:37+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/lt.po b/openerp/addons/base/i18n/lt.po index 41bf6184d54..55518a7b577 100644 --- a/openerp/addons/base/i18n/lt.po +++ b/openerp/addons/base/i18n/lt.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:40+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:38+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/lv.po b/openerp/addons/base/i18n/lv.po index 2dffa10010b..f64156a0738 100644 --- a/openerp/addons/base/i18n/lv.po +++ b/openerp/addons/base/i18n/lv.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:39+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:37+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/mk.po b/openerp/addons/base/i18n/mk.po index 959625eb326..46cfd2f956a 100644 --- a/openerp/addons/base/i18n/mk.po +++ b/openerp/addons/base/i18n/mk.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:40+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:38+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/mn.po b/openerp/addons/base/i18n/mn.po index 0a7fdda48bb..1d69c955339 100644 --- a/openerp/addons/base/i18n/mn.po +++ b/openerp/addons/base/i18n/mn.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:40+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:38+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/nb.po b/openerp/addons/base/i18n/nb.po index fa284c499cc..066dfdb196a 100644 --- a/openerp/addons/base/i18n/nb.po +++ b/openerp/addons/base/i18n/nb.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:40+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:38+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/nl.po b/openerp/addons/base/i18n/nl.po index 437b1573219..9950ea883b2 100644 --- a/openerp/addons/base/i18n/nl.po +++ b/openerp/addons/base/i18n/nl.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:35+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:33+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing @@ -13429,7 +13429,7 @@ msgstr "" #. module: base #: selection:res.currency,position:0 msgid "Before Amount" -msgstr "Voormalig bedrag" +msgstr "Voor bedrag" #. module: base #: field:res.request,act_from:0 diff --git a/openerp/addons/base/i18n/nl_BE.po b/openerp/addons/base/i18n/nl_BE.po index 68516ab59e0..d8e2ab7b023 100644 --- a/openerp/addons/base/i18n/nl_BE.po +++ b/openerp/addons/base/i18n/nl_BE.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:47+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:44+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/pl.po b/openerp/addons/base/i18n/pl.po index 153b4e2d6e4..bdfe4178bb1 100644 --- a/openerp/addons/base/i18n/pl.po +++ b/openerp/addons/base/i18n/pl.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:41+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:39+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/pt.po b/openerp/addons/base/i18n/pt.po index ccc2c104bda..ac1ec23c839 100644 --- a/openerp/addons/base/i18n/pt.po +++ b/openerp/addons/base/i18n/pt.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:41+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:39+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/pt_BR.po b/openerp/addons/base/i18n/pt_BR.po index cb4d741324a..0d5261a6539 100644 --- a/openerp/addons/base/i18n/pt_BR.po +++ b/openerp/addons/base/i18n/pt_BR.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:45+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:43+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/ro.po b/openerp/addons/base/i18n/ro.po index e7e599f738f..f6afb07778e 100644 --- a/openerp/addons/base/i18n/ro.po +++ b/openerp/addons/base/i18n/ro.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:42+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:39+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/ru.po b/openerp/addons/base/i18n/ru.po index 78a9fc009d3..dde0d09a88b 100644 --- a/openerp/addons/base/i18n/ru.po +++ b/openerp/addons/base/i18n/ru.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:42+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:40+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/sk.po b/openerp/addons/base/i18n/sk.po index cf4d85d449d..fa23b9fca86 100644 --- a/openerp/addons/base/i18n/sk.po +++ b/openerp/addons/base/i18n/sk.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:42+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:40+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/sl.po b/openerp/addons/base/i18n/sl.po index a8875055ba1..2ce1102afb6 100644 --- a/openerp/addons/base/i18n/sl.po +++ b/openerp/addons/base/i18n/sl.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:43+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:41+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/sq.po b/openerp/addons/base/i18n/sq.po index 1944d14c787..75418986b60 100644 --- a/openerp/addons/base/i18n/sq.po +++ b/openerp/addons/base/i18n/sq.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:33+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:30+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/sr.po b/openerp/addons/base/i18n/sr.po index 64353f30337..61b34a233f5 100644 --- a/openerp/addons/base/i18n/sr.po +++ b/openerp/addons/base/i18n/sr.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:42+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:40+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/sr@latin.po b/openerp/addons/base/i18n/sr@latin.po index f816a58a2f4..8c9348c3094 100644 --- a/openerp/addons/base/i18n/sr@latin.po +++ b/openerp/addons/base/i18n/sr@latin.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:49+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:47+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/sv.po b/openerp/addons/base/i18n/sv.po index 901f0879025..a8e01fb7a5d 100644 --- a/openerp/addons/base/i18n/sv.po +++ b/openerp/addons/base/i18n/sv.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:43+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:41+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/th.po b/openerp/addons/base/i18n/th.po index 9bfa5e24012..db7e38e2fec 100644 --- a/openerp/addons/base/i18n/th.po +++ b/openerp/addons/base/i18n/th.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:43+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:41+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/tlh.po b/openerp/addons/base/i18n/tlh.po index a5ec6474b4a..afcd43207d9 100644 --- a/openerp/addons/base/i18n/tlh.po +++ b/openerp/addons/base/i18n/tlh.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:43+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:42+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/tr.po b/openerp/addons/base/i18n/tr.po index 49f4b9beda2..2411e610e83 100644 --- a/openerp/addons/base/i18n/tr.po +++ b/openerp/addons/base/i18n/tr.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:44+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:42+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/uk.po b/openerp/addons/base/i18n/uk.po index 9820330aa71..5ec02766e6d 100644 --- a/openerp/addons/base/i18n/uk.po +++ b/openerp/addons/base/i18n/uk.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:44+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:42+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/ur.po b/openerp/addons/base/i18n/ur.po index 56aeeaac51e..d18f9aaea23 100644 --- a/openerp/addons/base/i18n/ur.po +++ b/openerp/addons/base/i18n/ur.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:44+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:42+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/vi.po b/openerp/addons/base/i18n/vi.po index 0d0344feb28..ae1c01cef12 100644 --- a/openerp/addons/base/i18n/vi.po +++ b/openerp/addons/base/i18n/vi.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:44+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:43+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/zh_CN.po b/openerp/addons/base/i18n/zh_CN.po index 8ae359ba3ae..6ea18ad57c9 100644 --- a/openerp/addons/base/i18n/zh_CN.po +++ b/openerp/addons/base/i18n/zh_CN.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:48+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:46+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/zh_HK.po b/openerp/addons/base/i18n/zh_HK.po index 37770edda37..700bf5c84c7 100644 --- a/openerp/addons/base/i18n/zh_HK.po +++ b/openerp/addons/base/i18n/zh_HK.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:44+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:43+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/i18n/zh_TW.po b/openerp/addons/base/i18n/zh_TW.po index 47f285186dc..1b7ff7144f5 100644 --- a/openerp/addons/base/i18n/zh_TW.po +++ b/openerp/addons/base/i18n/zh_TW.po @@ -13,8 +13,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-02-14 06:47+0000\n" -"X-Generator: Launchpad (build 16916)\n" +"X-Launchpad-Export-Date: 2014-03-20 05:45+0000\n" +"X-Generator: Launchpad (build 16967)\n" #. module: base #: model:ir.module.module,description:base.module_account_check_writing diff --git a/openerp/addons/base/ir/__init__.py b/openerp/addons/base/ir/__init__.py index b3df5bcbccb..9aa1e7c42a2 100644 --- a/openerp/addons/base/ir/__init__.py +++ b/openerp/addons/base/ir/__init__.py @@ -39,6 +39,7 @@ import ir_mail_server import ir_fields import ir_qweb import ir_http +import ir_logging # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index 31c170068c8..7b0681fd661 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -135,35 +135,42 @@ class ir_actions_report_xml(osv.osv): _sequence = 'ir_actions_id_seq' _order = 'name' _columns = { - 'name': fields.char('Name', size=64, required=True, translate=True), - 'model': fields.char('Object', size=64, required=True), 'type': fields.char('Action Type', size=32, required=True), - 'report_name': fields.char('Service Name', size=64, required=True), - 'usage': fields.char('Action Usage', size=32), - 'report_type': fields.char('Report Type', size=32, required=True, help="Report Type, e.g. pdf, html, raw, sxw, odt, html2html, mako2html, ..."), + 'name': fields.char('Name', size=64, required=True, translate=True), + + 'model': fields.char('Model', required=True), + 'report_type': fields.selection([('qweb-pdf', 'PDF'), + ('qweb-html', 'HTML'), + ('other', 'Other'), + ('pdf', 'RML pdf (deprecated)'), + ('sxw', 'RML sxw (deprecated)'), + ('webkit', 'Webkit (deprecated)'), + ], 'Report Type', required=True, help="PDF will use wkhtmltopdf to render html to pdf, HTML will directly show html, Other will force download the controller output keeping the MIME type."), + 'report_name': fields.char('Controller Name', required=True, help="URL of the report will be /report//, the default controller also use this field to get the name of the qweb ir.ui.view to render. For RML reports, this is the LocalService name."), 'groups_id': fields.many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', 'Groups'), + + # options 'multi': fields.boolean('On Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view."), - 'attachment': fields.char('Save as Attachment Prefix', size=128, help='This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.'), 'attachment_use': fields.boolean('Reload from Attachment', help='If you check this, then the second time the user prints with same attachment name, it returns the previous report.'), + 'attachment': fields.char('Save as Attachment Prefix', size=128, help='This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.'), + + # Deprecated rml stuff + 'usage': fields.char('Action Usage', size=32), + 'header': fields.boolean('Add RML Header', help="Add or not the corporate RML header"), + 'parser': fields.char('Parser Class'), 'auto': fields.boolean('Custom Python Parser'), - 'header': fields.boolean('Add RML Header', help="Add or not the corporate RML header"), + 'report_xsl': fields.char('XSL Path'), + 'report_xml': fields.char('XML Path'), - 'report_xsl': fields.char('XSL Path', size=256), - 'report_xml': fields.char('XML Path', size=256, help=''), - - # Pending deprecation... to be replaced by report_file as this object will become the default report object (not so specific to RML anymore) - 'report_rml': fields.char('Main Report File Path', size=256, help="The path to the main report file (depending on Report Type) or NULL if the content is in another data field"), - # temporary related field as report_rml is pending deprecation - this field will replace report_rml after v6.0 - 'report_file': fields.related('report_rml', type="char", size=256, required=False, readonly=False, string='Report File', help="The path to the main report file (depending on Report Type) or NULL if the content is in another field", store=True), + 'report_rml': fields.char('Main Report File Path', help="The path to the main report file (depending on Report Type) or NULL if the content is in another data field"), + 'report_file': fields.related('report_rml', type="char", required=False, readonly=False, string='Report File', help="The path to the main report file (depending on Report Type) or NULL if the content is in another field", store=True), 'report_sxw': fields.function(_report_sxw, type='char', string='SXW Path'), 'report_sxw_content_data': fields.binary('SXW Content'), 'report_rml_content_data': fields.binary('RML Content'), 'report_sxw_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='SXW Content',), 'report_rml_content': fields.function(_report_content, fnct_inv=_report_content_inv, type='binary', string='RML Content'), - - 'parser': fields.char('Parser Class'), } _defaults = { 'type': 'ir.actions.report.xml', @@ -552,6 +559,7 @@ class ir_actions_server(osv.osv): # - uid: current user id # - context: current context # - time: Python time module +# - workflow: Workflow engine # If you plan to return an action, assign: action = {...}""", 'use_relational_model': 'base', 'use_create': 'new', @@ -936,6 +944,7 @@ class ir_actions_server(osv.osv): 'uid': uid, 'user': user, 'context': context, + 'workflow': workflow } def run(self, cr, uid, ids, context=None): diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml index 7ac0252da80..77d7eb23e8c 100644 --- a/openerp/addons/base/ir/ir_actions.xml +++ b/openerp/addons/base/ir/ir_actions.xml @@ -57,39 +57,35 @@ - + + - - - + + + - + + + + + + + + - - - - - - - - - - - - @@ -117,7 +113,8 @@ filter_domain="['|', '|', '|', '|', ('name','ilike',self), ('model','ilike',self), ('type','ilike',self), ('report_name','ilike',self), ('report_type','ilike',self)]" string="Report"/> - + + @@ -412,6 +409,7 @@
  • cr: database cursor
  • uid: current user id
  • context: current context
  • +
  • workflow: Workflow engine
  • Example of condition expression using Python

    diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index 69776922362..992ac1b5ff0 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -64,19 +64,37 @@ class ir_attachment(osv.osv): data[attachment.id] = False return data + def _storage(self, cr, uid, context=None): + return self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'ir_attachment.location', 'file') + + @tools.ormcache() + def _filestore(self, cr, uid, context=None): + return os.path.join(tools.config['data_dir'], 'filestore', cr.dbname) + # 'data' field implementation def _full_path(self, cr, uid, location, path): - # location = 'file:filestore' - assert location.startswith('file:'), "Unhandled filestore location %s" % location - location = location[5:] - - # sanitize location name and path - location = re.sub('[.]','',location) - location = location.strip('/\\') - - path = re.sub('[.]','',path) + # sanitize ath + path = re.sub('[.]', '', path) path = path.strip('/\\') - return os.path.join(tools.config['root_path'], location, cr.dbname, path) + return os.path.join(self._filestore(cr, uid), path) + + def _get_path(self, cr, uid, location, bin_data): + sha = hashlib.sha1(bin_data).hexdigest() + + # retro compatibility + fname = sha[:3] + '/' + sha + full_path = self._full_path(cr, uid, location, fname) + if os.path.isfile(full_path): + return fname, full_path # keep existing path + + # scatter files across 256 dirs + # we use '/' in the db (even on windows) + fname = sha[:2] + '/' + sha + full_path = self._full_path(cr, uid, location, fname) + dirname = os.path.dirname(full_path) + if not os.path.isdir(dirname): + os.makedirs(dirname) + return fname, full_path def _file_read(self, cr, uid, location, fname, bin_size=False): full_path = self._full_path(cr, uid, location, fname) @@ -92,18 +110,13 @@ class ir_attachment(osv.osv): def _file_write(self, cr, uid, location, value): bin_value = value.decode('base64') - fname = hashlib.sha1(bin_value).hexdigest() - # scatter files across 1024 dirs - # we use '/' in the db (even on windows) - fname = fname[:3] + '/' + fname - full_path = self._full_path(cr, uid, location, fname) - try: - dirname = os.path.dirname(full_path) - if not os.path.isdir(dirname): - os.makedirs(dirname) - open(full_path,'wb').write(bin_value) - except IOError: - _logger.error("_file_write writing %s",full_path) + fname, full_path = self._get_path(cr, uid, location, bin_value) + if not os.path.exists(full_path): + try: + with open(full_path, 'wb') as fp: + fp.write(bin_value) + except IOError: + _logger.error("_file_write writing %s", full_path) return fname def _file_delete(self, cr, uid, location, fname): @@ -122,10 +135,10 @@ class ir_attachment(osv.osv): if context is None: context = {} result = {} - location = self.pool.get('ir.config_parameter').get_param(cr, uid, 'ir_attachment.location') + location = self._storage(cr, uid, context) bin_size = context.get('bin_size') for attach in self.browse(cr, uid, ids, context=context): - if location and attach.store_fname: + if location != 'db' and attach.store_fname: result[attach.id] = self._file_read(cr, uid, location, attach.store_fname, bin_size) else: result[attach.id] = attach.db_datas @@ -137,9 +150,9 @@ class ir_attachment(osv.osv): return True if context is None: context = {} - location = self.pool.get('ir.config_parameter').get_param(cr, uid, 'ir_attachment.location') + location = self._storage(cr, uid, context) file_size = len(value.decode('base64')) - if location: + if location != 'db': attach = self.browse(cr, uid, id, context=context) if attach.store_fname: self._file_delete(cr, uid, location, attach.store_fname) @@ -285,8 +298,8 @@ class ir_attachment(osv.osv): if isinstance(ids, (int, long)): ids = [ids] self.check(cr, uid, ids, 'unlink', context=context) - location = self.pool.get('ir.config_parameter').get_param(cr, uid, 'ir_attachment.location') - if location: + location = self._storage(cr, uid, context) + if location != 'db': for attach in self.browse(cr, uid, ids, context=context): if attach.store_fname: self._file_delete(cr, uid, location, attach.store_fname) @@ -303,4 +316,3 @@ class ir_attachment(osv.osv): cr, uid, 'base', 'action_attachment', context=context) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 60a69cd6b38..e5a5e5f024b 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -225,12 +225,21 @@ class ir_cron(osv.osv): lock_cr = db.cursor() try: # Try to grab an exclusive lock on the job row from within the task transaction + # Restrict to the same conditions as for the search since the job may have already + # been run by an other thread when cron is running in multi thread lock_cr.execute("""SELECT * FROM ir_cron - WHERE id=%s + WHERE numbercall != 0 + AND active + AND nextcall <= (now() at time zone 'UTC') + AND id=%s FOR UPDATE NOWAIT""", (job['id'],), log_exceptions=False) + locked_job = lock_cr.fetchone() + if not locked_job: + # job was already executed by another parallel process/thread, skipping it. + continue # Got the lock on the job row, run its code _logger.debug('Starting job `%s`.', job['name']) job_cr = db.cursor() diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index 8cc24c88cce..62ecd605223 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -53,8 +53,8 @@ class ir_http(osv.AbstractModel): def _get_converters(self): return {'model': ModelConverter, 'models': ModelsConverter} - def _find_handler(self): - return self.routing_map().bind_to_environ(request.httprequest.environ).match() + def _find_handler(self, return_rule=False): + return self.routing_map().bind_to_environ(request.httprequest.environ).match(return_rule=return_rule) def _auth_method_user(self): request.uid = request.session.uid @@ -77,7 +77,9 @@ class ir_http(osv.AbstractModel): # what if error in security.check() # -> res_users.check() # -> res_users.check_credentials() - except Exception: + except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException): + # All other exceptions mean undetermined status (e.g. connection pool full), + # let them bubble up request.session.logout() getattr(self, "_auth_method_%s" % auth_method)() return auth_method @@ -89,7 +91,8 @@ class ir_http(osv.AbstractModel): def _dispatch(self): # locate the controller method try: - func, arguments = self._find_handler() + rule, arguments = self._find_handler(return_rule=True) + func = rule.endpoint except werkzeug.exceptions.NotFound, e: return self._handle_exception(e) @@ -102,7 +105,7 @@ class ir_http(osv.AbstractModel): convert_exception_to( werkzeug.exceptions.Forbidden)) - processing = self._postprocess_args(arguments) + processing = self._postprocess_args(arguments, rule) if processing: return processing @@ -118,7 +121,7 @@ class ir_http(osv.AbstractModel): return result - def _postprocess_args(self, arguments): + def _postprocess_args(self, arguments, rule): """ post process arg to set uid on browse records """ for arg in arguments.itervalues(): if isinstance(arg, orm.browse_record) and arg._uid is UID_PLACEHOLDER: diff --git a/openerp/addons/base/ir/ir_logging.py b/openerp/addons/base/ir/ir_logging.py new file mode 100644 index 00000000000..dd06a19c0ae --- /dev/null +++ b/openerp/addons/base/ir/ir_logging.py @@ -0,0 +1,46 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014 OpenERP 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 logging + +from openerp.osv import osv, fields +from openerp.tools.translate import _ + +class ir_logging(osv.Model): + _name = 'ir.logging' + _order = 'id DESC' + + EXCEPTIONS_TYPE = [ + ('client', 'Client'), + ('server', 'Server') + ] + + _columns = { + 'create_date': fields.datetime('Create Date', readonly=True), + 'create_uid': fields.integer('Uid', readonly=True), # Integer not m2o is intentionnal + 'name': fields.char('Name', required=True), + 'type': fields.selection(EXCEPTIONS_TYPE, string='Type', required=True, select=True), + 'dbname': fields.char('Database Name'), + 'level': fields.char('Level'), + 'message': fields.text('Message', required=True), + 'path': fields.char('Path', required=True), + 'func': fields.char('Function', required=True), + 'line': fields.char('Line', required=True), + } + diff --git a/openerp/addons/base/ir/ir_logging_view.xml b/openerp/addons/base/ir/ir_logging_view.xml new file mode 100644 index 00000000000..f963fc0d393 --- /dev/null +++ b/openerp/addons/base/ir/ir_logging_view.xml @@ -0,0 +1,66 @@ + + + + + ir.logging + +
    + + + + + + + + + + + +
    +
    +
    + + ir.logging + + + + + + + + + + + + + + + + ir.logging + + + + + + + + + + + + + + + + + + + Logging + ir.logging + form + tree,form + + + +
    +
    diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index d119c0344b4..76166ee92e0 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -33,7 +33,7 @@ from openerp.osv.orm import Model, browse_null from openerp.tools.safe_eval import safe_eval as eval from openerp.tools import config from openerp.tools.translate import _ -from openerp.osv.orm import except_orm, browse_record +from openerp.osv.orm import except_orm, browse_record, MAGIC_COLUMNS _logger = logging.getLogger(__name__) @@ -302,6 +302,8 @@ class ir_model_fields(osv.osv): def _drop_column(self, cr, uid, ids, context=None): for field in self.browse(cr, uid, ids, context): + if field.name in MAGIC_COLUMNS: + continue model = self.pool[field.model] cr.execute('select relkind from pg_class where relname=%s', (model._table,)) result = cr.fetchone() @@ -1117,6 +1119,10 @@ class ir_model_data(osv.osv): # Don't remove the LOG_ACCESS_COLUMNS unless _log_access # has been turned off on the model. field = self.pool[model].browse(cr, uid, [res_id], context=context)[0] + if not field.exists(): + _logger.info('Deleting orphan external_ids %s', external_ids) + self.unlink(cr, uid, external_ids) + continue if field.name in openerp.osv.orm.LOG_ACCESS_COLUMNS and self.pool[field.model]._log_access: continue if field.name == 'id': diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 590ce90188f..0b3bf91dc1f 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -32,7 +32,7 @@ class QWebException(Exception): class QWebTemplateNotFound(QWebException): pass -def convert_to_qweb_exception(etype=None, **kw): +def raise_qweb_exception(etype=None, **kw): if etype is None: etype = QWebException orig_type, original, tb = sys.exc_info() @@ -43,7 +43,7 @@ def convert_to_qweb_exception(etype=None, **kw): e.qweb[k] = v # Will use `raise foo from bar` in python 3 and rename cause to __cause__ e.qweb['cause'] = original - return e + raise class QWebContext(dict): def __init__(self, cr, uid, data, loader=None, templates=None, context=None): @@ -166,7 +166,7 @@ class QWeb(orm.AbstractModel): try: xml_doc = qwebcontext.loader(name) except ValueError: - raise convert_to_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template) + raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template) self.load_document(xml_doc, qwebcontext=qwebcontext) if name in qwebcontext.templates: @@ -179,7 +179,7 @@ class QWeb(orm.AbstractModel): return qwebcontext.safe_eval(expr) except Exception: template = qwebcontext.get('__template__') - raise convert_to_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template) + raise_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template) def eval_object(self, expr, qwebcontext): return self.eval(expr, qwebcontext) @@ -207,7 +207,7 @@ class QWeb(orm.AbstractModel): return str(expr % qwebcontext) except Exception: template = qwebcontext.get('__template__') - raise convert_to_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template) + raise_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template) def eval_bool(self, expr, qwebcontext): return int(bool(self.eval(expr, qwebcontext))) @@ -292,7 +292,7 @@ class QWeb(orm.AbstractModel): raise except Exception: template = qwebcontext.get('__template__') - raise convert_to_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template) + raise_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template) name = str(element.nodeName) inner = "".join(g_inner) trim = template_attributes.get("trim", 0) @@ -488,7 +488,6 @@ class FieldConverter(osv.AbstractModel): A default configuration key is ``widget`` which can override the field's own ``_type``. """ - content = None try: content = self.record_to_html( cr, uid, field_name, record, @@ -503,12 +502,14 @@ class FieldConverter(osv.AbstractModel): field_name, record._model._name, exc_info=True) content = None - g_att += ''.join( - ' %s="%s"' % (name, werkzeug.utils.escape(value)) - for name, value in self.attributes( - cr, uid, field_name, record, options, - source_element, g_att, t_att, qweb_context) - ) + if context and context.get('inherit_branding'): + # add branding attributes + g_att += ''.join( + ' %s="%s"' % (name, werkzeug.utils.escape(value)) + for name, value in self.attributes( + cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context) + ) return self.render_element(cr, uid, source_element, t_att, g_att, qweb_context, content) @@ -611,6 +612,9 @@ class DateTimeConverter(osv.AbstractModel): strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format)) pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale) + if options and options.get('hide_seconds'): + pattern = pattern.replace(":ss", "").replace(":s", "") + return babel.dates.format_datetime(value, format=pattern, locale=locale) class TextConverter(osv.AbstractModel): diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index aafce29b7eb..360c5e37b31 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -316,14 +316,14 @@ class view(osv.osv): return node return None - def inherit_branding(self, specs_tree, view_id, source_id): + def inherit_branding(self, specs_tree, view_id, root_id): for node in specs_tree.iterchildren(tag=etree.Element): xpath = node.getroottree().getpath(node) if node.tag == 'data' or node.tag == 'xpath': - self.inherit_branding(node, view_id, source_id) + self.inherit_branding(node, view_id, root_id) else: node.set('data-oe-id', str(view_id)) - node.set('data-oe-source-id', str(source_id)) + node.set('data-oe-source-id', str(root_id)) node.set('data-oe-xpath', xpath) node.set('data-oe-model', 'ir.ui.view') node.set('data-oe-field', 'arch') @@ -397,7 +397,7 @@ class view(osv.osv): return source - def apply_view_inheritance(self, cr, uid, source, source_id, model, context=None): + def apply_view_inheritance(self, cr, uid, source, source_id, model, root_id=None, context=None): """ Apply all the (directly and indirectly) inheriting views. :param source: a parent architecture to modify (with parent modifications already applied) @@ -408,13 +408,15 @@ class view(osv.osv): :return: a modified source where all the modifying architecture are applied """ if context is None: context = {} + if root_id is None: + root_id = source_id sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context) for (specs, view_id) in sql_inherit: specs_tree = etree.fromstring(specs.encode('utf-8')) if context.get('inherit_branding'): - self.inherit_branding(specs_tree, view_id, source_id) + self.inherit_branding(specs_tree, view_id, root_id) source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context) - source = self.apply_view_inheritance(cr, uid, source, view_id, model, context=context) + source = self.apply_view_inheritance(cr, uid, source, view_id, model, root_id=root_id, context=context) return source def read_combined(self, cr, uid, view_id, fields=None, context=None): @@ -726,9 +728,26 @@ class view(osv.osv): def clear_cache(self): self.read_template.clear_cache(self) + def _contains_branded(self, node): + return node.tag == 't'\ + or 't-raw' in node.attrib\ + or any(self.is_node_branded(child) for child in node.iterdescendants()) + + def _pop_view_branding(self, element): + distributed_branding = dict( + (attribute, element.attrib.pop(attribute)) + for attribute in MOVABLE_BRANDING + if element.get(attribute)) + return distributed_branding + def distribute_branding(self, e, branding=None, parent_xpath='', index_map=misc.ConstantMapping(1)): if e.get('t-ignore') or e.tag == 'head': + # remove any view branding possibly injected by inheritance + attrs = set(MOVABLE_BRANDING) + for descendant in e.iterdescendants(tag=etree.Element): + if not attrs.intersection(descendant.attrib): continue + self._pop_view_branding(descendant) # TODO: find a better name and check if we have a string to boolean helper return @@ -740,15 +759,15 @@ class view(osv.osv): e.set('data-oe-xpath', node_path) if not e.get('data-oe-model'): return - # if a branded element contains branded elements distribute own - # branding to children unless it's t-raw, then just remove branding - # on current element - if e.tag == 't' or 't-raw' in e.attrib or \ - any(self.is_node_branded(child) for child in e.iterdescendants()): - distributed_branding = dict( - (attribute, e.attrib.pop(attribute)) - for attribute in MOVABLE_BRANDING - if e.get(attribute)) + if set(('t-esc', 't-escf', 't-raw', 't-rawf')).intersection(e.attrib): + # nodes which fully generate their content and have no reason to + # be branded because they can not sensibly be edited + self._pop_view_branding(e) + elif self._contains_branded(e): + # if a branded element contains branded elements distribute own + # branding to children unless it's t-raw, then just remove branding + # on current element + distributed_branding = self._pop_view_branding(e) if 't-raw' not in e.attrib: # TODO: collections.Counter if remove p2.6 compat @@ -758,11 +777,12 @@ class view(osv.osv): if child.get('data-oe-xpath'): # injected by view inheritance, skip otherwise # generated xpath is incorrect - continue - indexes[child.tag] += 1 - self.distribute_branding(child, distributed_branding, - parent_xpath=node_path, - index_map=indexes) + self.distribute_branding(child) + else: + indexes[child.tag] += 1 + self.distribute_branding( + child, distributed_branding, + parent_xpath=node_path, index_map=indexes) def is_node_branded(self, node): """ Finds out whether a node is branded or qweb-active (bears a diff --git a/openerp/addons/base/module/wizard/base_module_import.py b/openerp/addons/base/module/wizard/base_module_import.py index 6d6f0e1ef8d..2ba789f3332 100644 --- a/openerp/addons/base/module/wizard/base_module_import.py +++ b/openerp/addons/base/module/wizard/base_module_import.py @@ -28,8 +28,6 @@ from openerp import tools from openerp.osv import osv, fields from openerp.tools.translate import _ -ADDONS_PATH = tools.config['addons_path'].split(",")[-1] - class base_module_import(osv.osv_memory): """ Import Module """ diff --git a/openerp/addons/base/res/res_company.py b/openerp/addons/base/res/res_company.py index ae83aa4f2d1..64ab5676ca2 100644 --- a/openerp/addons/base/res/res_company.py +++ b/openerp/addons/base/res/res_company.py @@ -208,20 +208,19 @@ class res_company(osv.osv): res['value'] = {'currency_id': currency_id} return res - def _search(self, cr, uid, args, offset=0, limit=None, order=None, - context=None, count=False, access_rights_uid=None): + def name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100): if context is None: context = {} - if context.get('user_preference'): + if context.pop('user_preference', None): # We browse as superuser. Otherwise, the user would be able to # select only the currently visible companies (according to rules, # which are probably to allow to see the child companies) even if # she belongs to some other companies. user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context) cmp_ids = list(set([user.company_id.id] + [cmp.id for cmp in user.company_ids])) - return cmp_ids - return super(res_company, self)._search(cr, uid, args, offset=offset, limit=limit, order=order, - context=context, count=count, access_rights_uid=access_rights_uid) + uid = SUPERUSER_ID + args = (args or []) + [('id', 'in', cmp_ids)] + return super(res_company, self).name_search(cr, uid, name=name, args=args, operator=operator, context=context, limit=limit) def _company_default_get(self, cr, uid, object=False, field=False, context=None): """ diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index e9573088680..46f0218e21f 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -350,6 +350,7 @@ class res_partner(osv.osv, format_address): def copy(self, cr, uid, id, default=None, context=None): if default is None: default = {} + default['user_ids'] = False name = self.read(cr, uid, [id], ['name'], context)[0]['name'] default.update({'name': _('%s (copy)') % name}) return super(res_partner, self).copy(cr, uid, id, default, context) diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index b2aba694078..1668cbda3c5 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -3,7 +3,7 @@ # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). -# Copyright (C) 2010-2013 OpenERP s.a. (). +# Copyright (C) 2010-2014 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -169,8 +169,9 @@ class res_users(osv.osv): } def on_change_login(self, cr, uid, ids, login, context=None): - v = {'email': login} if tools.single_email_re.match(login) else {} - return {'value': v} + if login and tools.single_email_re.match(login): + return {'value': {'email': login}} + return {} def onchange_state(self, cr, uid, ids, state_id, context=None): partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)] @@ -899,18 +900,22 @@ class change_password_wizard(osv.TransientModel): })) return {'user_ids': res} - def change_password_button(self, cr, uid, id, context=None): wizard = self.browse(cr, uid, id, context=context)[0] - user_ids = [] - for user in wizard.user_ids: - user_ids.append(user.id) - self.pool.get('change.password.user').change_password_button(cr, uid, user_ids, context=context) + need_reload = any(uid == user.user_id.id for user in wizard.user_ids) + line_ids = [user.id for user in wizard.user_ids] + + self.pool.get('change.password.user').change_password_button(cr, uid, line_ids, context=context) # don't keep temporary password copies in the database longer than necessary - self.pool.get('change.password.user').unlink(cr, uid, user_ids) - return { - 'type': 'ir.actions.act_window_close', - } + self.pool.get('change.password.user').write(cr, uid, line_ids, {'new_passwd': False}, context=context) + + if need_reload: + return { + 'type': 'ir.actions.client', + 'tag': 'reload' + } + + return {'type': 'ir.actions.act_window_close'} class change_password_user(osv.TransientModel): """ diff --git a/openerp/addons/base/res/res_users_view.xml b/openerp/addons/base/res/res_users_view.xml index ba603c7a0f0..8a3f372af7a 100644 --- a/openerp/addons/base/res/res_users_view.xml +++ b/openerp/addons/base/res/res_users_view.xml @@ -282,7 +282,7 @@ - diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index 25df6e117d0..6113f17c5aa 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -113,3 +113,4 @@ "access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1 "access_res_font_all","res_res_font all","model_res_font",,1,0,0,0 "access_res_font_group_user","res_res_font group_user","model_res_font","group_user",1,1,1,1 +"access_ir_logging","ir_logging admin","model_ir_logging","group_erp_manager",1,1,1,1 diff --git a/openerp/addons/base/tests/test_base.py b/openerp/addons/base/tests/test_base.py index 263cb0adf23..bbc5b21d1fb 100644 --- a/openerp/addons/base/tests/test_base.py +++ b/openerp/addons/base/tests/test_base.py @@ -9,6 +9,7 @@ class test_base(common.TransactionCase): super(test_base,self).setUp() self.res_partner = self.registry('res.partner') self.res_users = self.registry('res.users') + self.res_partner_title = self.registry('res.partner.title') # samples use effective TLDs from the Mozilla public suffix # list at http://publicsuffix.org @@ -285,27 +286,99 @@ class test_base(common.TransactionCase): def test_60_read_group(self): cr, uid = self.cr, self.uid - for user_data in [ - {'name': 'Alice', 'login': 'alice', 'color': 1, 'function': 'Friend'}, - {'name': 'Bob', 'login': 'bob', 'color': 2, 'function': 'Friend'}, - {'name': 'Eve', 'login': 'eve', 'color': 3, 'function': 'Eavesdropper'}, - {'name': 'Nab', 'login': 'nab', 'color': 2, 'function': '5$ Wrench'}, - ]: - self.res_users.create(cr, uid, user_data) + title_sir = self.res_partner_title.create(cr, uid, {'name': 'Sir', 'domain': 'contact'}) + title_lady = self.res_partner_title.create(cr, uid, {'name': 'Lady', 'domain': 'contact'}) + test_users = [ + {'name': 'Alice', 'login': 'alice', 'color': 1, 'function': 'Friend', 'date': '2015-03-28', 'title': title_lady}, + {'name': 'Alice', 'login': 'alice2', 'color': 0, 'function': 'Friend', 'date': '2015-01-28', 'title': title_lady}, + {'name': 'Bob', 'login': 'bob', 'color': 2, 'function': 'Friend', 'date': '2015-03-02', 'title': title_sir}, + {'name': 'Eve', 'login': 'eve', 'color': 3, 'function': 'Eavesdropper', 'date': '2015-03-20', 'title': title_lady}, + {'name': 'Nab', 'login': 'nab', 'color': -3, 'function': '5$ Wrench', 'date': '2014-09-10', 'title': title_sir}, + {'name': 'Nab', 'login': 'nab-she', 'color': 6, 'function': '5$ Wrench', 'date': '2014-01-02', 'title': title_lady}, + ] + ids = [self.res_users.create(cr, uid, u) for u in test_users] + domain = [('id', 'in', ids)] - groups_data = self.res_users.read_group(cr, uid, domain=[('login', 'in', ('alice', 'bob', 'eve'))], fields=['name', 'color', 'function'], groupby='function') - self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field") + # group on local char field without domain and without active_test (-> empty WHERE clause) + groups_data = self.res_users.read_group(cr, uid, [], fields=['login'], groupby=['login'], orderby='login DESC', context={'active_test': False}) + self.assertGreater(len(groups_data), 6, "Incorrect number of results when grouping on a field") + + # group on local char field with limit + groups_data = self.res_users.read_group(cr, uid, domain, fields=['login'], groupby=['login'], orderby='login DESC', limit=3, offset=3) + self.assertEqual(len(groups_data), 3, "Incorrect number of results when grouping on a field with limit") + self.assertEqual(['bob', 'alice2', 'alice'], [g['login'] for g in groups_data], 'Result mismatch') + + # group on inherited char field, aggregate on int field (second groupby ignored on purpose) + groups_data = self.res_users.read_group(cr, uid, domain, fields=['name', 'color', 'function'], groupby=['function', 'login']) + self.assertEqual(len(groups_data), 3, "Incorrect number of results when grouping on a field") + self.assertEqual(['5$ Wrench', 'Eavesdropper', 'Friend'], [g['function'] for g in groups_data], 'incorrect read_group order') for group_data in groups_data: - self.assertIn('color', group_data, "Aggregated data for the column 'color' is not present in read_group return values") - self.assertEqual(group_data['color'], 3, "Incorrect sum for aggregated data for the column 'color'") + self.assertIn('color', group_data, "Aggregated data for the column 'color' is not present in read_group return values") + self.assertEqual(group_data['color'], 3, "Incorrect sum for aggregated data for the column 'color'") - groups_data = self.res_users.read_group(cr, uid, domain=[('login', 'in', ('alice', 'bob', 'eve'))], fields=['name', 'color'], groupby='name', orderby='name DESC, color asc') - self.assertEqual(len(groups_data), 3, "Incorrect number of results when grouping on a field") - self.assertEqual([user['name'] for user in groups_data], ['Eve', 'Bob', 'Alice'], 'Incorrect ordering of the list') + # group on inherited char field, reverse order + groups_data = self.res_users.read_group(cr, uid, domain, fields=['name', 'color'], groupby='name', orderby='name DESC') + self.assertEqual(['Nab', 'Eve', 'Bob', 'Alice'], [g['name'] for g in groups_data], 'Incorrect ordering of the list') + + # group on int field, default ordering + groups_data = self.res_users.read_group(cr, uid, domain, fields=['color'], groupby='color') + self.assertEqual([-3, 0, 1, 2, 3, 6], [g['color'] for g in groups_data], 'Incorrect ordering of the list') + + # multi group, second level is int field, should still be summed in first level grouping + groups_data = self.res_users.read_group(cr, uid, domain, fields=['name', 'color'], groupby=['name', 'color'], orderby='name DESC') + self.assertEqual(['Nab', 'Eve', 'Bob', 'Alice'], [g['name'] for g in groups_data], 'Incorrect ordering of the list') + self.assertEqual([3, 3, 2, 1], [g['color'] for g in groups_data], 'Incorrect ordering of the list') + + # group on inherited char field, multiple orders with directions + groups_data = self.res_users.read_group(cr, uid, domain, fields=['name', 'color'], groupby='name', orderby='color DESC, name') + self.assertEqual(len(groups_data), 4, "Incorrect number of results when grouping on a field") + self.assertEqual(['Eve', 'Nab', 'Bob', 'Alice'], [g['name'] for g in groups_data], 'Incorrect ordering of the list') + self.assertEqual([1, 2, 1, 2], [g['name_count'] for g in groups_data], 'Incorrect number of results') + + # group on inherited date column (res_partner.date) -> Year-Month, default ordering + groups_data = self.res_users.read_group(cr, uid, domain, fields=['function', 'color', 'date'], groupby=['date']) + self.assertEqual(len(groups_data), 4, "Incorrect number of results when grouping on a field") + self.assertEqual(['January 2014', 'September 2014', 'January 2015', 'March 2015'], [g['date'] for g in groups_data], 'Incorrect ordering of the list') + self.assertEqual([1, 1, 1, 3], [g['date_count'] for g in groups_data], 'Incorrect number of results') + + # group on inherited date column (res_partner.date) -> Year-Month, custom order + groups_data = self.res_users.read_group(cr, uid, domain, fields=['function', 'color', 'date'], groupby=['date'], orderby='date DESC') + self.assertEqual(len(groups_data), 4, "Incorrect number of results when grouping on a field") + self.assertEqual(['March 2015', 'January 2015', 'September 2014', 'January 2014'], [g['date'] for g in groups_data], 'Incorrect ordering of the list') + self.assertEqual([3, 1, 1, 1], [g['date_count'] for g in groups_data], 'Incorrect number of results') + + # group on inherited many2one (res_partner.title), default order + groups_data = self.res_users.read_group(cr, uid, domain, fields=['function', 'color', 'title'], groupby=['title']) + self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field") + # m2o is returned as a (id, label) pair + self.assertEqual([(title_lady, 'Lady'), (title_sir, 'Sir')], [g['title'] for g in groups_data], 'Incorrect ordering of the list') + self.assertEqual([4, 2], [g['title_count'] for g in groups_data], 'Incorrect number of results') + self.assertEqual([10, -1], [g['color'] for g in groups_data], 'Incorrect aggregation of int column') + + # group on inherited many2one (res_partner.title), reversed natural order + groups_data = self.res_users.read_group(cr, uid, domain, fields=['function', 'color', 'title'], groupby=['title'], orderby="title desc") + self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field") + # m2o is returned as a (id, label) pair + self.assertEqual([(title_sir, 'Sir'), (title_lady, 'Lady')], [g['title'] for g in groups_data], 'Incorrect ordering of the list') + self.assertEqual([2, 4], [g['title_count'] for g in groups_data], 'Incorrect number of results') + self.assertEqual([-1, 10], [g['color'] for g in groups_data], 'Incorrect aggregation of int column') + + # group on inherited many2one (res_partner.title), multiple orders with m2o in second position + groups_data = self.res_users.read_group(cr, uid, domain, fields=['function', 'color', 'title'], groupby=['title'], orderby="color desc, title desc") + self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field") + # m2o is returned as a (id, label) pair + self.assertEqual([(title_lady, 'Lady'), (title_sir, 'Sir')], [g['title'] for g in groups_data], 'Incorrect ordering of the result') + self.assertEqual([4, 2], [g['title_count'] for g in groups_data], 'Incorrect number of results') + self.assertEqual([10, -1], [g['color'] for g in groups_data], 'Incorrect aggregation of int column') + + # group on inherited many2one (res_partner.title), ordered by other inherited field (color) + groups_data = self.res_users.read_group(cr, uid, domain, fields=['function', 'color', 'title'], groupby=['title'], orderby='color') + self.assertEqual(len(groups_data), 2, "Incorrect number of results when grouping on a field") + # m2o is returned as a (id, label) pair + self.assertEqual([(title_sir, 'Sir'), (title_lady, 'Lady')], [g['title'] for g in groups_data], 'Incorrect ordering of the list') + self.assertEqual([2, 4], [g['title_count'] for g in groups_data], 'Incorrect number of results') + self.assertEqual([-1, 10], [g['color'] for g in groups_data], 'Incorrect aggregation of int column') - groups_data = self.res_users.read_group(cr, uid, domain=[('login', 'in', ('alice', 'bob', 'eve', 'nab'))], fields=['function', 'color'], groupby='function', orderby='color ASC') - self.assertEqual(len(groups_data), 3, "Incorrect number of results when grouping on a field") - self.assertEqual(groups_data, sorted(groups_data, key=lambda x: x['color']), 'Incorrect ordering of the list') class test_partner_recursion(common.TransactionCase): diff --git a/openerp/addons/base/tests/test_fields.py b/openerp/addons/base/tests/test_fields.py index 47ffcd2f5b1..ce34bceb014 100644 --- a/openerp/addons/base/tests/test_fields.py +++ b/openerp/addons/base/tests/test_fields.py @@ -179,3 +179,51 @@ class TestPropertyField(common.TransactionCase): self.partner.write(cr, alice, [partner_id], {'property_country': country_be}) self.assertEqual(self.partner.browse(cr, alice, partner_id).property_country.id, country_be, "Alice does not see the value he has set on the property field") self.assertEqual(self.partner.browse(cr, bob, partner_id).property_country.id, country_fr, "Changes made by Alice have overwritten Bob's value") + + +class TestHtmlField(common.TransactionCase): + + def setUp(self): + super(TestHtmlField, self).setUp() + self.partner = self.registry('res.partner') + + def test_00_sanitize(self): + cr, uid, context = self.cr, self.uid, {} + old_columns = self.partner._columns + self.partner._columns = dict(old_columns) + self.partner._columns.update({ + 'comment': fields.html('Secure Html', sanitize=False), + }) + some_ugly_html = """

    Oops this should maybe be sanitized +% if object.some_field and not object.oriented: + + % if object.other_field: + + ${object.mako_thing} + + % endif + +%if object.dummy_field: +

    Youpie

    +%endif""" + + pid = self.partner.create(cr, uid, { + 'name': 'Raoul Poilvache', + 'comment': some_ugly_html, + }, context=context) + partner = self.partner.browse(cr, uid, pid, context=context) + self.assertEqual(partner.comment, some_ugly_html, 'Error in HTML field: content was sanitized but field has sanitize=False') + + self.partner._columns.update({ + 'comment': fields.html('Unsecure Html', sanitize=True), + }) + self.partner.write(cr, uid, [pid], { + 'comment': some_ugly_html, + }, context=context) + partner = self.partner.browse(cr, uid, pid, context=context) + # sanitize should have closed tags left open in the original html + self.assertIn('
    +
    ', partner.comment, 'Error in HTML field: content does not seem to have been sanitized despise sanitize=True') + self.assertIn('', partner.comment, 'Error in HTML field: content does not seem to have been sanitized despise sanitize=True') + + self.partner._columns = old_columns diff --git a/openerp/addons/base/tests/test_ir_actions.py b/openerp/addons/base/tests/test_ir_actions.py index 04f67617ad6..baea8299f9e 100644 --- a/openerp/addons/base/tests/test_ir_actions.py +++ b/openerp/addons/base/tests/test_ir_actions.py @@ -97,7 +97,8 @@ class TestServerActions(TestServerActionsBase): self.ir_actions_server.write(cr, uid, self.act_id, { 'state': 'code', 'code': """partner_name = obj.name + '_code' -self.pool["res.partner"].create(cr, uid, {"name": partner_name}, context=context)""" +self.pool["res.partner"].create(cr, uid, {"name": partner_name}, context=context) +workflow""" }) run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context) self.assertFalse(run_res, 'ir_actions_server: code server action correctly finished should return False') diff --git a/openerp/addons/base/tests/test_ir_attachment.py b/openerp/addons/base/tests/test_ir_attachment.py index 4d96ec42b28..891b38b7dc3 100644 --- a/openerp/addons/base/tests/test_ir_attachment.py +++ b/openerp/addons/base/tests/test_ir_attachment.py @@ -1,89 +1,92 @@ import hashlib import os -import unittest2 - import openerp import openerp.tests.common -class test_ir_attachment(openerp.tests.common.TransactionCase): +HASH_SPLIT = 2 # FIXME: testing implementations detail is not a good idea - def test_00_attachment_flow(self): +class test_ir_attachment(openerp.tests.common.TransactionCase): + def setUp(self): + super(test_ir_attachment, self).setUp() registry, cr, uid = self.registry, self.cr, self.uid - root_path = openerp.tools.config['root_path'] - ira = registry('ir.attachment') + self.ira = registry('ir.attachment') + self.filestore = self.ira._filestore(cr, uid) # Blob1 - blob1 = 'blob1' - blob1_b64 = blob1.encode('base64') - blob1_hash = hashlib.sha1(blob1).hexdigest() - blob1_fname = blob1_hash[:3] + '/' + blob1_hash + self.blob1 = 'blob1' + self.blob1_b64 = self.blob1.encode('base64') + blob1_hash = hashlib.sha1(self.blob1).hexdigest() + self.blob1_fname = blob1_hash[:HASH_SPLIT] + '/' + blob1_hash # Blob2 blob2 = 'blob2' - blob2_b64 = blob2.encode('base64') - blob2_hash = hashlib.sha1(blob2).hexdigest() - blob2_fname = blob2_hash[:3] + '/' + blob2_hash + self.blob2_b64 = blob2.encode('base64') + + def test_01_store_in_db(self): + registry, cr, uid = self.registry, self.cr, self.uid + + # force storing in database + registry('ir.config_parameter').set_param(cr, uid, 'ir_attachment.location', 'db') # 'ir_attachment.location' is undefined test database storage - a1 = ira.create(cr, uid, {'name': 'a1', 'datas': blob1_b64}) - a1_read = ira.read(cr, uid, [a1], ['datas']) - self.assertEqual(a1_read[0]['datas'], blob1_b64) + a1 = self.ira.create(cr, uid, {'name': 'a1', 'datas': self.blob1_b64}) + a1_read = self.ira.read(cr, uid, [a1], ['datas']) + self.assertEqual(a1_read[0]['datas'], self.blob1_b64) - cr.execute("select id,db_datas from ir_attachment where id = %s", (a1,) ) - a1_db_datas = str(cr.fetchall()[0][1]) - self.assertEqual(a1_db_datas, blob1_b64) + a1_db_datas = self.ira.browse(cr, uid, a1).db_datas + self.assertEqual(a1_db_datas, self.blob1_b64) - # define a location for filestore - registry('ir.config_parameter').set_param(cr, uid, 'ir_attachment.location', 'file:///filestore') + def test_02_store_on_disk(self): + registry, cr, uid = self.registry, self.cr, self.uid - # Test file storage - a2 = ira.create(cr, uid, {'name': 'a2', 'datas': blob1_b64}) - a2_read = ira.read(cr, uid, [a2], ['datas']) - self.assertEqual(a2_read[0]['datas'], blob1_b64) + a2 = self.ira.create(cr, uid, {'name': 'a2', 'datas': self.blob1_b64}) + a2_store_fname = self.ira.browse(cr, uid, a2).store_fname - cr.execute("select id,store_fname from ir_attachment where id = %s", (a2,) ) - a2_store_fname = cr.fetchall()[0][1] - self.assertEqual(a2_store_fname, blob1_fname) + self.assertEqual(a2_store_fname, self.blob1_fname) + self.assertTrue(os.path.isfile(os.path.join(self.filestore, a2_store_fname))) - a2_fn = os.path.join(root_path, 'filestore', cr.dbname, blob1_hash[:3], blob1_hash) - fc = file(a2_fn).read() - self.assertEqual(fc, blob1) + def test_03_no_duplication(self): + registry, cr, uid = self.registry, self.cr, self.uid - # create a3 with same blob - a3 = ira.create(cr, uid, {'name': 'a3', 'datas': blob1_b64}) - a3_read = ira.read(cr, uid, [a3], ['datas']) - self.assertEqual(a3_read[0]['datas'], blob1_b64) + a2 = self.ira.create(cr, uid, {'name': 'a2', 'datas': self.blob1_b64}) + a2_store_fname = self.ira.browse(cr, uid, a2).store_fname + + a3 = self.ira.create(cr, uid, {'name': 'a3', 'datas': self.blob1_b64}) + a3_store_fname = self.ira.browse(cr, uid, a3).store_fname - cr.execute("select id,store_fname from ir_attachment where id = %s", (a3,) ) - a3_store_fname = cr.fetchall()[0][1] self.assertEqual(a3_store_fname, a2_store_fname) - # create a4 blob2 - a4 = ira.create(cr, uid, {'name': 'a4', 'datas': blob2_b64}) - a4_read = ira.read(cr, uid, [a4], ['datas']) - self.assertEqual(a4_read[0]['datas'], blob2_b64) + def test_04_keep_file(self): + registry, cr, uid = self.registry, self.cr, self.uid - a4_fn = os.path.join(root_path, 'filestore', cr.dbname, blob2_hash[:3], blob2_hash) - self.assertTrue(os.path.isfile(a4_fn)) + a2 = self.ira.create(cr, uid, {'name': 'a2', 'datas': self.blob1_b64}) + a3 = self.ira.create(cr, uid, {'name': 'a3', 'datas': self.blob1_b64}) - # delete a3 but file stays - ira.unlink(cr, uid, [a3]) + a2_store_fname = self.ira.browse(cr, uid, a2).store_fname + a2_fn = os.path.join(self.filestore, a2_store_fname) + + self.ira.unlink(cr, uid, [a3]) self.assertTrue(os.path.isfile(a2_fn)) # delete a2 it is unlinked - ira.unlink(cr, uid, [a2]) + self.ira.unlink(cr, uid, [a2]) self.assertFalse(os.path.isfile(a2_fn)) - # update a4 blob2 by blob1 - ira.write(cr, uid, [a4], {'datas': blob1_b64}) - a4_read = ira.read(cr, uid, [a4], ['datas']) - self.assertEqual(a4_read[0]['datas'], blob1_b64) + def test_05_change_data_change_file(self): + registry, cr, uid = self.registry, self.cr, self.uid + + a2 = self.ira.create(cr, uid, {'name': 'a2', 'datas': self.blob1_b64}) + a2_store_fname = self.ira.browse(cr, uid, a2).store_fname + a2_fn = os.path.join(self.filestore, a2_store_fname) - # file of a4 disapear and a2 reappear - self.assertFalse(os.path.isfile(a4_fn)) self.assertTrue(os.path.isfile(a2_fn)) - # everybody applause + self.ira.write(cr, uid, [a2], {'datas': self.blob2_b64}) + self.assertFalse(os.path.isfile(a2_fn)) + new_a2_store_fname = self.ira.browse(cr, uid, a2).store_fname + self.assertNotEqual(a2_store_fname, new_a2_store_fname) + new_a2_fn = os.path.join(self.filestore, new_a2_store_fname) + self.assertTrue(os.path.isfile(new_a2_fn)) diff --git a/openerp/addons/base/tests/test_ir_sequence.py b/openerp/addons/base/tests/test_ir_sequence.py index bc428d7f647..e925fffa3b1 100644 --- a/openerp/addons/base/tests/test_ir_sequence.py +++ b/openerp/addons/base/tests/test_ir_sequence.py @@ -8,6 +8,7 @@ # OPENERP_DATABASE=yy PYTHONPATH=../:. unit2 test_ir_sequence # This assume an existing database. import psycopg2 +import psycopg2.errorcodes import unittest2 import openerp @@ -111,11 +112,11 @@ class test_ir_sequence_no_gap(unittest2.TestCase): cr0 = cursor() cr1 = cursor() cr1._default_log_exceptions = False # Prevent logging a traceback - msg_re = '^could not obtain lock on row in relation "ir_sequence"$' - with self.assertRaisesRegexp(psycopg2.OperationalError, msg_re): + with self.assertRaises(psycopg2.OperationalError) as e: n0 = registry('ir.sequence').next_by_code(cr0, ADMIN_USER_ID, 'test_sequence_type_2', {}) assert n0 n1 = registry('ir.sequence').next_by_code(cr1, ADMIN_USER_ID, 'test_sequence_type_2', {}) + self.assertEqual(e.exception.pgcode, psycopg2.errorcodes.LOCK_NOT_AVAILABLE, msg="postgresql returned an incorrect errcode") cr0.close() cr1.close() diff --git a/openerp/addons/base/tests/test_qweb.py b/openerp/addons/base/tests/test_qweb.py index 71beaab605a..ca1503a0c24 100644 --- a/openerp/addons/base/tests/test_qweb.py +++ b/openerp/addons/base/tests/test_qweb.py @@ -14,7 +14,8 @@ class TestQWebTField(common.TransactionCase): self.engine = self.registry('ir.qweb') def context(self, values): - return ir_qweb.QWebContext(self.cr, self.uid, values) + return ir_qweb.QWebContext( + self.cr, self.uid, values, context={'inherit_branding': True}) def test_trivial(self): field = document.createElement('span') diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 6780996ed6e..06030a84792 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -1,6 +1,8 @@ # -*- encoding: utf-8 -*- from functools import partial +import unittest2 + from lxml import etree as ET from lxml.builder import E @@ -8,6 +10,24 @@ from openerp.tests import common Field = E.field +class ViewCase(common.TransactionCase): + def setUp(self): + super(ViewCase, self).setUp() + self.addTypeEqualityFunc(ET._Element, self.assertTreesEqual) + + def assertTreesEqual(self, n1, n2, msg=None): + self.assertEqual(n1.tag, n2.tag) + self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg) + self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg) + + # Because lxml uses ordereddicts in which order is important to + # equality (!?!?!?!) + self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg) + + for c1, c2 in zip(n1, n2): + self.assertTreesEqual(c1, c2, msg) + + class TestNodeLocator(common.BaseCase): """ The node locator returns None when it can not find a node, and the first @@ -98,7 +118,7 @@ class TestNodeLocator(common.BaseCase): E.foo(attr='1', version='3')) self.assertIsNone(node) -class TestViewInheritance(common.TransactionCase): +class TestViewInheritance(ViewCase): def arch_for(self, name, view_type='form', parent=None): """ Generates a trivial view of the specified ``view_type``. @@ -206,7 +226,7 @@ class TestViewInheritance(common.TransactionCase): self.View.default_view( self.cr, self.uid, model=self.model, view_type='graph')) -class TestApplyInheritanceSpecs(common.TransactionCase): +class TestApplyInheritanceSpecs(ViewCase): """ Applies a sequence of inheritance specification nodes to a base architecture. IO state parameters (cr, uid, model, context) are used for error reporting @@ -230,8 +250,8 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form(Field(name="replacement"), string="Title"))) + self.base_arch, + E.form(Field(name="replacement"), string="Title")) def test_delete(self): spec = Field(name="target", position="replace") @@ -241,8 +261,8 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form(string="Title"))) + self.base_arch, + E.form(string="Title")) def test_insert_after(self): spec = Field( @@ -254,12 +274,12 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form( + self.base_arch, + E.form( Field(name="target"), Field(name="inserted"), string="Title" - ))) + )) def test_insert_before(self): spec = Field( @@ -271,11 +291,11 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form( + self.base_arch, + E.form( Field(name="inserted"), Field(name="target"), - string="Title"))) + string="Title")) def test_insert_inside(self): default = Field(Field(name="inserted"), name="target") @@ -289,13 +309,13 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form( + self.base_arch, + E.form( Field( Field(name="inserted"), Field(name="inserted 2"), name="target"), - string="Title"))) + string="Title")) def test_unpack_data(self): spec = E.data( @@ -310,15 +330,15 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form( + self.base_arch, + E.form( Field( Field(name="inserted 0"), Field(name="inserted 1"), Field(name="inserted 2"), Field(name="inserted 3"), name="target"), - string="Title"))) + string="Title")) def test_invalid_position(self): spec = Field( @@ -350,18 +370,18 @@ class TestApplyInheritanceSpecs(common.TransactionCase): self.base_arch, spec, None) -class TestApplyInheritedArchs(common.TransactionCase): +class TestApplyInheritedArchs(ViewCase): """ Applies a sequence of modificator archs to a base view """ -class TestViewCombined(common.TransactionCase): +class TestViewCombined(ViewCase): """ Test fallback operations of View.read_combined: * defaults mapping * ? """ -class TestNoModel(common.TransactionCase): +class TestNoModel(ViewCase): def test_create_view_nomodel(self): View = self.registry('ir.ui.view') view_id = View.create(self.cr, self.uid, { @@ -411,13 +431,11 @@ class TestNoModel(common.TransactionCase): 'value': translated_text, }) sarch = View.translate_qweb(self.cr, self.uid, None, self.arch, 'fr_FR') - - self.text_para.text = translated_text - self.assertEqual( - ET.tostring(sarch, encoding='utf-8'), - ET.tostring(self.arch, encoding='utf-8')) -class TestTemplating(common.TransactionCase): + self.text_para.text = translated_text + self.assertEqual(sarch, self.arch) + +class TestTemplating(ViewCase): def setUp(self): import openerp.modules super(TestTemplating, self).setUp() @@ -473,7 +491,126 @@ class TestTemplating(common.TransactionCase): second.get('data-oe-id'), "second should come from the extension view") -class test_views(common.TransactionCase): + def test_branding_distribute_inner(self): + """ Checks that the branding is correctly distributed within a view + extension + """ + Views = self.registry('ir.ui.view') + id = Views.create(self.cr, self.uid, { + 'name': "Base view", + 'type': 'qweb', + 'arch': """ + + """ + }) + id2 = Views.create(self.cr, self.uid, { + 'name': "Extension", + 'type': 'qweb', + 'inherit_id': id, + 'arch': """ + + bar + + """ + }) + + arch_string = Views.read_combined( + self.cr, self.uid, id, fields=['arch'], + context={'inherit_branding': True})['arch'] + + arch = ET.fromstring(arch_string) + Views.distribute_branding(arch) + + self.assertEqual( + arch, + E.root( + E.item( + E.content("bar", { + 't-att-href': "foo", + 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(id2), + 'data-oe-field': 'arch', + 'data-oe-xpath': '/xpath/item/content[1]', + }), { + 'order': '2', + 'data-oe-source-id': str(id) + }), + E.item({ + 'order': '1', + 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(id), + 'data-oe-field': 'arch', + 'data-oe-xpath': '/root[1]/item[1]' + }) + ) + ) + + def test_esc_no_branding(self): + Views = self.registry('ir.ui.view') + id = Views.create(self.cr, self.uid, { + 'name': "Base View", + 'type': 'qweb', + 'arch': """ + + """, + }) + + arch_string = Views.read_combined( + self.cr, self.uid, id, fields=['arch'], + context={'inherit_branding': True})['arch'] + arch = ET.fromstring(arch_string) + Views.distribute_branding(arch) + + self.assertEqual(arch, E.root(E.item(E.span({'t-esc': "foo"})))) + + def test_ignore_unbrand(self): + Views = self.registry('ir.ui.view') + id = Views.create(self.cr, self.uid, { + 'name': "Base view", + 'type': 'qweb', + 'arch': """ + + + + """ + }) + id2 = Views.create(self.cr, self.uid, { + 'name': "Extension", + 'type': 'qweb', + 'inherit_id': id, + 'arch': """ + + bar + + """ + }) + + arch_string = Views.read_combined( + self.cr, self.uid, id, fields=['arch'], + context={'inherit_branding': True})['arch'] + + arch = ET.fromstring(arch_string) + Views.distribute_branding(arch) + + self.assertEqual( + arch, + E.root( + E.item( + {'t-ignore': 'true', 'order': '1'}, + E.t({'t-esc': 'foo'}), + E.item( + {'order': '2', 'data-oe-source-id': str(id)}, + E.content( + {'t-att-href': 'foo'}, + "bar") + ) + ) + ), + "t-ignore should apply to injected sub-view branding, not just to" + " the main view's" + ) + +class test_views(ViewCase): def test_nonexistent_attribute_removal(self): Views = self.registry('ir.ui.view') @@ -589,16 +726,17 @@ class test_views(common.TransactionCase): }) self.assertEqual(view['type'], 'form') self.assertEqual( - ET.tostring(ET.fromstring( + ET.fromstring( view['arch'], parser=ET.XMLParser(remove_blank_text=True) - )), - '

    ' - '

    Replacement data

    ' - '
    ' - '
    ' - '
    ') + ), + E.form( + E.p("Replacement data"), + E.footer( + E.button(name="action_next", type="object", string="New button"), + thing="bob" + ), + string="Replacement title", version="7.0")) def test_view_inheritance_divergent_models(self): Views = self.registry('ir.ui.view') @@ -656,14 +794,13 @@ class test_views(common.TransactionCase): }) self.assertEqual(view['type'], 'form') self.assertEqual( - ET.tostring(ET.fromstring( + ET.fromstring( view['arch'], parser=ET.XMLParser(remove_blank_text=True) - )), - '
    ' - '

    Replacement data

    ' - '
    ' - '
    ' - '
    ') - + ), + E.form( + E.p("Replacement data"), + E.footer( + E.button(name="action_next", type="object", string="New button")), + string="Replacement title", version="7.0" + )) diff --git a/openerp/addons/test_converter/tests/test_html.py b/openerp/addons/test_converter/tests/test_html.py index 2f0896990ea..3ef6566e829 100644 --- a/openerp/addons/test_converter/tests/test_html.py +++ b/openerp/addons/test_converter/tests/test_html.py @@ -112,11 +112,14 @@ class TestCurrencyExport(TestExport): 'widget': 'monetary', 'display_currency': 'c2' } + context = dict(inherit_branding=True) converted = converter.to_html( self.cr, self.uid, 'value', obj, options, doc.createElement('span'), {'field': 'obj.value', 'field-options': json.dumps(options)}, - '', ir_qweb.QWebContext(self.cr, self.uid, {'obj': obj, 'c2': dest, })) + '', ir_qweb.QWebContext(self.cr, self.uid, {'obj': obj, 'c2': dest, }), + context=context, + ) return converted def test_currency_post(self): diff --git a/openerp/addons/test_impex/tests/test_export.py b/openerp/addons/test_impex/tests/test_export.py index 26219b36d3b..995d69471d6 100644 --- a/openerp/addons/test_impex/tests/test_export.py +++ b/openerp/addons/test_impex/tests/test_export.py @@ -309,8 +309,9 @@ class test_m2o(CreatorCase): def test_external_id(self): integer_id = self.registry('export.integer').create( self.cr, openerp.SUPERUSER_ID, {'value': 42}) - # __export__.$class.$id - external_id = u'__export__.export_many2one_%d' % integer_id + # Expecting the m2o target model name in the external id, + # not this model's name + external_id = u'__export__.export_integer_%d' % integer_id self.assertEqual( self.export(integer_id, fields=['value/id']), [[external_id]]) diff --git a/openerp/cli/server.py b/openerp/cli/server.py index 004cb1ab8bb..42d920da9aa 100644 --- a/openerp/cli/server.py +++ b/openerp/cli/server.py @@ -72,7 +72,7 @@ def report_configuration(): """ config = openerp.tools.config _logger.info("OpenERP version %s", __version__) - for name, value in [('addons paths', config['addons_path']), + for name, value in [('addons paths', openerp.modules.module.ad_paths), ('database hostname', config['db_host'] or 'localhost'), ('database port', config['db_port'] or '5432'), ('database user', config['db_user'])]: diff --git a/openerp/http.py b/openerp/http.py index 74d833e4730..5876cdc6a9a 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -23,6 +23,7 @@ import urlparse import warnings import babel.core +import psutil import psycopg2 import simplejson import werkzeug.contrib.sessions @@ -35,7 +36,6 @@ import werkzeug.wsgi import openerp from openerp.service import security, model as service_model -import openerp.tools _logger = logging.getLogger(__name__) @@ -50,6 +50,70 @@ request = _request_stack() A global proxy that always redirect to the current request object. """ +def replace_request_password(args): + # password is always 3rd argument in a request, we replace it in RPC logs + # so it's easier to forward logs for diagnostics/debugging purposes... + if len(args) > 2: + args = list(args) + args[2] = '*' + return tuple(args) + +def dispatch_rpc(service_name, method, params): + """ Handle a RPC call. + + This is pure Python code, the actual marshalling (from/to XML-RPC) is done + in a upper layer. + """ + try: + rpc_request = logging.getLogger(__name__ + '.rpc.request') + rpc_response = logging.getLogger(__name__ + '.rpc.response') + rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG) + rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG) + if rpc_request_flag or rpc_response_flag: + start_time = time.time() + start_rss, start_vms = 0, 0 + start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info() + if rpc_request and rpc_response_flag: + openerp.netsvc.log(rpc_request, logging.DEBUG, '%s.%s' % (service_name, method), replace_request_password(params)) + + threading.current_thread().uid = None + threading.current_thread().dbname = None + if service_name == 'common': + dispatch = openerp.service.common.dispatch + elif service_name == 'db': + dispatch = openerp.service.db.dispatch + elif service_name == 'object': + dispatch = openerp.service.model.dispatch + elif service_name == 'report': + dispatch = openerp.service.report.dispatch + else: + dispatch = openerp.service.wsgi_server.rpc_handlers.get(service_name) + result = dispatch(method, params) + + if rpc_request_flag or rpc_response_flag: + end_time = time.time() + end_rss, end_vms = 0, 0 + end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info() + logline = '%s.%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (service_name, method, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024) + if rpc_response_flag: + openerp.netsvc.log(rpc_response, logging.DEBUG, logline, result) + else: + openerp.netsvc.log(rpc_request, logging.DEBUG, logline, replace_request_password(params), depth=1) + + return result + except (openerp.osv.orm.except_orm, openerp.exceptions.AccessError, \ + openerp.exceptions.AccessDenied, openerp.exceptions.Warning, \ + openerp.exceptions.RedirectWarning): + raise + except openerp.exceptions.DeferredException, e: + _logger.exception(openerp.tools.exception_to_unicode(e)) + openerp.tools.debugger.post_mortem(openerp.tools.config, e.traceback) + raise + except Exception, e: + _logger.exception(openerp.tools.exception_to_unicode(e)) + openerp.tools.debugger.post_mortem(openerp.tools.config, sys.exc_info()) + raise + def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303): url = path if not query: @@ -129,12 +193,14 @@ class WebRequest(object): self.session_id = httprequest.session.sid self.disable_db = False self.uid = None - self.func = None - self.func_arguments = {} + self.endpoint = None self.auth_method = None self._cr_cm = None self._cr = None - self.func_request_type = None + + # prevents transaction commit, use when you catch an exception during handling + self._failed = None + # set db/uid trackers - they're cleaned up at the WSGI # dispatching phase in openerp.service.wsgi_server.application if self.db: @@ -182,50 +248,51 @@ class WebRequest(object): _request_stack.pop() if self._cr: - # Dont commit test cursors - if not openerp.tests.common.release_test_cursor(self.session_id): - if exc_type is None: + # Dont close test cursors + if not openerp.tests.common.release_test_cursor(self._cr): + if exc_type is None and not self._failed: self._cr.commit() + else: + # just to be explicit - happens at close() anyway + self._cr.rollback() self._cr.close() # just to be sure no one tries to re-use the request self.disable_db = True self.uid = None - def set_handler(self, func, arguments, auth): + def set_handler(self, endpoint, arguments, auth): # is this needed ? arguments = dict((k, v) for k, v in arguments.iteritems() if not k.startswith("_ignored_")) - self.func = func - self.func_request_type = func.routing['type'] - self.func_arguments = arguments + endpoint.arguments = arguments + self.endpoint = endpoint self.auth_method = auth def _call_function(self, *args, **kwargs): request = self - if self.func_request_type != self._request_type: + if self.endpoint.routing['type'] != self._request_type: raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \ - % (self.func, self.httprequest.path, self.func_request_type, self._request_type)) + % (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type)) - kwargs.update(self.func_arguments) + kwargs.update(self.endpoint.arguments) # Backward for 7.0 - if getattr(self.func.method, '_first_arg_is_req', False): + if self.endpoint.first_arg_is_req: args = (request,) + args + # Correct exception handling and concurency retry @service_model.check def checked_call(___dbname, *a, **kw): - return self.func(*a, **kw) - - # FIXME: code and rollback management could be cleaned - try: - if self.db: - return checked_call(self.db, *args, **kwargs) - return self.func(*args, **kwargs) - except Exception: + # The decorator can call us more than once if there is an database error. In this + # case, the request cursor is unusable. Rollback transaction to create a new one. if self._cr: self._cr.rollback() - raise + return self.endpoint(*a, **kw) + + if self.db: + return checked_call(self.db, *args, **kwargs) + return self.endpoint(*args, **kwargs) @property def debug(self): @@ -265,8 +332,23 @@ def route(route=None, **kw): else: routes = [route] routing['routes'] = routes - f.routing = routing - return f + @functools.wraps(f) + def response_wrap(*args, **kw): + response = f(*args, **kw) + if isinstance(response, Response) or f.routing_type == 'json': + return response + elif isinstance(response, werkzeug.wrappers.BaseResponse): + response = Response.force_type(response) + response.set_default() + return response + elif isinstance(response, basestring): + return Response(response) + else: + _logger.warn(" returns an invalid response type for an http request" % (f.__module__, f.__name__)) + return response + response_wrap.routing = routing + response_wrap.original_func = f + return response_wrap return decorator class JsonRequest(WebRequest): @@ -350,21 +432,25 @@ class JsonRequest(WebRequest): response['id'] = self.jsonrequest.get('id') response["result"] = self._call_function(**self.params) except AuthenticationError, e: - _logger.exception("Exception during JSON request handling.") + _logger.exception("JSON-RPC AuthenticationError in %s.", self.httprequest.path) se = serialize_exception(e) error = { 'code': 100, 'message': "OpenERP Session Invalid", 'data': se } + self._failed = e # prevent tx commit except Exception, e: - _logger.exception("Exception during JSON request handling.") + # Mute test cursor error for runbot + if not (openerp.tools.config['test_enable'] and isinstance(e, psycopg2.OperationalError)): + _logger.exception("JSON-RPC Exception in %s.", self.httprequest.path) se = serialize_exception(e) error = { 'code': 200, 'message': "OpenERP Server Error", 'data': se } + self._failed = e # prevent tx commit if error: response["error"] = error @@ -379,7 +465,7 @@ class JsonRequest(WebRequest): mime = 'application/json' body = simplejson.dumps(response) - r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))]) + r = Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))]) return r def serialize_exception(e): @@ -437,23 +523,16 @@ class HttpRequest(WebRequest): self.params = params def dispatch(self): - # TODO: refactor this correctly. This is a quick fix for pos demo. - if request.httprequest.method == 'OPTIONS' and request.func and request.func.routing.get('cors'): - response = werkzeug.wrappers.Response(status=200) - response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors']) - methods = 'GET, POST' - if request.func_request_type == 'json': - methods = 'POST' - elif request.func.routing.get('methods'): - methods = ', '.join(request.func.routing['methods']) - response.headers.set('Access-Control-Allow-Methods', methods) - response.headers.set('Access-Control-Max-Age',60*60*24) - response.headers.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') - return response + if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'): + headers = { + 'Access-Control-Max-Age': 60 * 60 * 24, + 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept' + } + return Response(status=200, headers=headers) r = self._call_function(**self.params) if not r: - r = werkzeug.wrappers.Response(status=204) # no content + r = Response(status=204) # no content return r def make_response(self, data, headers=None, cookies=None): @@ -470,12 +549,24 @@ class HttpRequest(WebRequest): :type headers: ``[(name, value)]`` :param collections.Mapping cookies: cookies to set on the client """ - response = werkzeug.wrappers.Response(data, headers=headers) + response = Response(data, headers=headers) if cookies: for k, v in cookies.iteritems(): response.set_cookie(k, v) return response + def render(self, template, qcontext=None, **kw): + """ Lazy render of QWeb template. + + The actual rendering of the given template will occur at then end of + the dispatching. Meanwhile, the template and/or qcontext can be + altered or even replaced by a static response. + + :param basestring template: template to render + :param dict qcontext: Rendering context to use + """ + return Response(template=template, qcontext=qcontext, **kw) + def not_found(self, description=None): """ Helper for 404 response, return its result from the method """ @@ -530,7 +621,15 @@ class Controller(object): class EndPoint(object): def __init__(self, method, routing): self.method = method + self.original = getattr(method, 'original_func', method) self.routing = routing + self.arguments = {} + + @property + def first_arg_is_req(self): + # Backward for 7.0 + return getattr(self.method, '_first_arg_is_req', False) + def __call__(self, *args, **kw): return self.method(*args, **kw) @@ -553,9 +652,19 @@ def routing_map(modules, nodb_only, converters=None): if inspect.ismethod(mv) and hasattr(mv, 'routing'): routing = dict(type='http', auth='user', methods=None, routes=None) methods_done = list() + routing_type = None for claz in reversed(mv.im_class.mro()): fn = getattr(claz, mv.func_name, None) if fn and hasattr(fn, 'routing') and fn not in methods_done: + fn_type = fn.routing.get('type') + if not routing_type: + routing_type = fn_type + else: + if fn_type and routing_type != fn_type: + _logger.warn("Subclass re-defines with different type than original." + " Will use original type: %r", fn.__module__, fn.__name__, routing_type) + fn.routing['type'] = routing_type + fn.original_func.routing_type = routing_type methods_done.append(fn) routing.update(fn.routing) if not nodb_only or nodb_only == (routing['auth'] == "none"): @@ -583,7 +692,7 @@ class SessionExpiredException(Exception): class Service(object): """ .. deprecated:: 8.0 - Use ``openerp.netsvc.dispatch_rpc()`` instead. + Use ``dispatch_rpc()`` instead. """ def __init__(self, session, service_name): self.session = session @@ -591,7 +700,7 @@ class Service(object): def __getattr__(self, method): def proxy_method(*args): - result = openerp.netsvc.dispatch_rpc(self.service_name, method, args) + result = dispatch_rpc(self.service_name, method, args) return result return proxy_method @@ -664,7 +773,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): HTTP_HOST=wsgienv['HTTP_HOST'], REMOTE_ADDR=wsgienv['REMOTE_ADDR'], ) - uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env]) + uid = dispatch_rpc('common', 'authenticate', [db, login, password, env]) else: security.check(db, uid, password) self.db = db @@ -698,7 +807,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.setdefault("uid", None) self.setdefault("login", None) self.setdefault("password", None) - self.setdefault("context", {'tz': "UTC", "uid": None}) + self.setdefault("context", {}) def get_context(self): """ @@ -768,14 +877,14 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): def send(self, service_name, method, *args): """ .. deprecated:: 8.0 - Use ``openerp.netsvc.dispatch_rpc()`` instead. + Use ``dispatch_rpc()`` instead. """ - return openerp.netsvc.dispatch_rpc(service_name, method, args) + return dispatch_rpc(service_name, method, args) def proxy(self, service): """ .. deprecated:: 8.0 - Use ``openerp.netsvc.dispatch_rpc()`` instead. + Use ``dispatch_rpc()`` instead. """ return Service(self, service) @@ -892,19 +1001,51 @@ mimetypes.add_type('application/font-woff', '.woff') mimetypes.add_type('application/vnd.ms-fontobject', '.eot') mimetypes.add_type('application/x-font-ttf', '.ttf') -class LazyResponse(werkzeug.wrappers.Response): - """ Lazy werkzeug response. - API not yet frozen""" +class Response(werkzeug.wrappers.Response): + """ Response object passed through controller route chain. - def __init__(self, callback, status_code=None, **kwargs): - super(LazyResponse, self).__init__(mimetype='text/html') - if status_code: - self.status_code = status_code - self.callback = callback - self.params = kwargs - def process(self): - response = self.callback(**self.params) - self.response.append(response) + In addition to the werkzeug.wrappers.Response parameters, this + classe's constructor can take the following additional parameters + for QWeb Lazy Rendering. + + :param basestring template: template to render + :param dict qcontext: Rendering context to use + :param int uid: User id to use for the ir.ui.view render call + """ + default_mimetype = 'text/html' + def __init__(self, *args, **kw): + template = kw.pop('template', None) + qcontext = kw.pop('qcontext', None) + uid = kw.pop('uid', None) + super(Response, self).__init__(*args, **kw) + self.set_default(template, qcontext, uid) + + def set_default(self, template=None, qcontext=None, uid=None): + self.template = template + self.qcontext = qcontext or dict() + self.uid = uid + # Support for Cross-Origin Resource Sharing + if request.endpoint and 'cors' in request.endpoint.routing: + self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors']) + methods = 'GET, POST' + if request.endpoint.routing['type'] == 'json': + methods = 'POST' + elif request.endpoint.routing.get('methods'): + methods = ', '.join(request.endpoint.routing['methods']) + self.headers.set('Access-Control-Allow-Methods', methods) + + @property + def is_qweb(self): + return self.template is not None + + def render(self): + view_obj = request.registry["ir.ui.view"] + uid = self.uid or request.uid or openerp.SUPERUSER_ID + return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context) + + def flatten(self): + self.response.append(self.render()) + self.template = None class DisableCacheMiddleware(object): def __init__(self, app): @@ -928,33 +1069,12 @@ class DisableCacheMiddleware(object): start_response(status, new_headers) return self.app(environ, start_wrapped) -def session_path(): - try: - import pwd - username = pwd.getpwuid(os.geteuid()).pw_name - except ImportError: - try: - username = getpass.getuser() - except Exception: - username = "unknown" - path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username) - try: - os.mkdir(path, 0700) - except OSError as exc: - if exc.errno == errno.EEXIST: - # directory exists: ensure it has the correct permissions - # this will fail if the directory is not owned by the current user - os.chmod(path, 0700) - else: - raise - return path - class Root(object): """Root WSGI application for the OpenERP Web Client. """ def __init__(self): # Setup http sessions - path = session_path() + path = openerp.tools.config.session_dir _logger.debug('HTTP sessions stored in: %s', path) self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession) @@ -1039,9 +1159,9 @@ class Root(object): return HttpRequest(httprequest) def get_response(self, httprequest, result, explicit_session): - if isinstance(result, LazyResponse): + if isinstance(result, Response) and result.is_qweb: try: - result.process() + result.flatten() except(Exception), e: if request.db: result = request.registry['ir.http']._handle_exception(e) @@ -1049,7 +1169,7 @@ class Root(object): raise if isinstance(result, basestring): - response = werkzeug.wrappers.Response(result, mimetype='text/html') + response = Response(result, mimetype='text/html') else: response = result @@ -1064,16 +1184,6 @@ class Root(object): if not explicit_session and hasattr(response, 'set_cookie'): response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60) - # Support for Cross-Origin Resource Sharing - if request.func and 'cors' in request.func.routing: - response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors']) - methods = 'GET, POST' - if request.func_request_type == 'json': - methods = 'POST' - elif request.func.routing.get('methods'): - methods = ', '.join(request.func.routing['methods']) - response.headers.set('Access-Control-Allow-Methods', methods) - return response def dispatch(self, environ, start_response): @@ -1127,12 +1237,12 @@ class Root(object): return request.registry['ir.http'].routing_map() def db_list(force=False, httprequest=None): - dbs = openerp.netsvc.dispatch_rpc("db", "list", [force]) + dbs = dispatch_rpc("db", "list", [force]) return db_filter(dbs, httprequest=httprequest) def db_filter(dbs, httprequest=None): httprequest = httprequest or request.httprequest - h = httprequest.environ['HTTP_HOST'].split(':')[0] + h = httprequest.environ.get('HTTP_HOST', '').split(':')[0] d = h.split('.')[0] r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d) dbs = [i for i in dbs if re.match(r, i)] @@ -1172,12 +1282,7 @@ class CommonController(Controller): @route('/jsonrpc', type='json', auth="none") def jsonrpc(self, service, method, args): """ Method used by client APIs to contact OpenERP. """ - return openerp.netsvc.dispatch_rpc(service, method, args) - - @route('/gen_session_id', type='json', auth="none") - def gen_session_id(self): - nsession = root.session_store.new() - return nsession.sid + return dispatch_rpc(service, method, args) root = None diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 7ca0a6785d6..681d8534c05 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -187,6 +187,10 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= # Yamel test report.record_result(load_test(module_name, idref, mode)) # Python tests + ir_http = registry['ir.http'] + if hasattr(ir_http, '_routing_map'): + # Force routing map to be rebuilt between each module test suite + del(ir_http._routing_map) report.record_result(openerp.modules.module.run_unit_tests(module_name, cr.dbname)) processed_modules.append(package.name) diff --git a/openerp/modules/module.py b/openerp/modules/module.py index a443a2576ab..8958813baa2 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -3,7 +3,7 @@ # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). -# Copyright (C) 2010-2012 OpenERP s.a. (). +# Copyright (C) 2010-2014 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -38,10 +38,6 @@ import openerp.release as release from openerp.tools.safe_eval import safe_eval as eval _logger = logging.getLogger(__name__) -_test_logger = logging.getLogger('openerp.tests') - -# addons path ','.joined -_ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base) # addons path as a list ad_paths = [] @@ -90,8 +86,13 @@ def initialize_sys_path(): if ad_paths: return - ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(',')) - ad_paths.append(os.path.abspath(_ad)) # for get_module_path + ad_paths = [tools.config.addons_data_dir] + ad_paths += map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(',')) + + # add base module path + base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons')) + ad_paths += [base_path] + sys.meta_path.append(AddonsImportHook()) def get_module_path(module, downloaded=False, display_warning=True): @@ -108,7 +109,7 @@ def get_module_path(module, downloaded=False, display_warning=True): return opj(adp, module) if downloaded: - return opj(_ad, module) + return opj(tools.config.addons_data_dir, module) if display_warning: _logger.warning('module %s: module not found', module) return False @@ -338,7 +339,8 @@ def get_test_modules(module): # Use a custom stream object to log the test executions. class TestStream(object): - def __init__(self): + def __init__(self, logger_name='openerp.tests'): + self.logger = logging.getLogger(logger_name) self.r = re.compile(r'^-*$|^ *... *$|^ok$') def flush(self): pass @@ -350,7 +352,7 @@ class TestStream(object): if not first: c = '` ' + c first = False - _test_logger.info(c) + self.logger.info(c) current_test = None @@ -367,9 +369,9 @@ def run_unit_tests(module_name, dbname): for m in mods: tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m)) suite = unittest2.TestSuite(itertools.ifilter(runs_at_install, tests)) - _logger.info('module %s: running test %s.', module_name, m.__name__) + _logger.info('running %s tests.', m.__name__) - result = unittest2.TextTestRunner(verbosity=2, stream=TestStream()).run(suite) + result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite) if not result.wasSuccessful(): r = False diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 755f6513b25..d762eb5006f 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2012 OpenERP SA () +# Copyright (C) 2004-2014 OpenERP 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 @@ -19,23 +19,29 @@ # ############################################################################## - import logging import logging.handlers import os +import pprint import release import sys import threading -import time -import types -from pprint import pformat -import psutil -import tools +import psycopg2 + import openerp +import sql_db +import tools _logger = logging.getLogger(__name__) +def log(logger, level, prefix, msg, depth=None): + indent='' + indent_after=' '*len(prefix) + for line in (prefix + pprint.pformat(msg, depth=depth)).split('\n'): + logger.log(level, indent+line) + indent=indent_after + def LocalService(name): """ The openerp.netsvc.LocalService() function is deprecated. It still works @@ -62,6 +68,36 @@ def LocalService(name): with registry.cursor() as cr: return registry['ir.actions.report.xml']._lookup_report(cr, name[len('report.'):]) +class PostgreSQLHandler(logging.Handler): + """ PostgreSQL Loggin Handler will store logs in the database, by default + the current database, can be set using --log-db=DBNAME + """ + def emit(self, record): + ct = threading.current_thread() + ct_db = getattr(ct, 'dbname', None) + ct_uid = getattr(ct, 'uid', None) + dbname = tools.config['log_db'] or ct_db + if dbname: + cr = None + try: + cr = sql_db.db_connect(dbname).cursor() + msg = unicode(record.msg) + traceback = getattr(record, 'exc_text', '') + if traceback: + msg = "%s\n%s" % (msg, traceback) + level = logging.getLevelName(record.levelno) + val = (ct_uid, ct_uid, 'server', dbname, record.name, level, msg, record.pathname, record.lineno, record.funcName) + cr.execute(""" + INSERT INTO ir_logging(create_date, write_date, create_uid, write_uid, type, dbname, name, level, message, path, line, func) + VALUES (NOW() at time zone 'UTC', NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, val ) + cr.commit() + except Exception, e: + pass + finally: + if cr: + cr.close() + BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10) #The background is set with 40 plus the number of the color, and the foreground with 30 #These are the sequences need to get colored ouput @@ -89,7 +125,13 @@ class ColoredFormatter(DBFormatter): record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname) return DBFormatter.format(self, record) +_logger_init = False def init_logger(): + global _logger_init + if _logger_init: + return + _logger_init = True + from tools.translate import resetlocale resetlocale() @@ -101,7 +143,7 @@ def init_logger(): if os.name == 'nt': handler = logging.handlers.NTEventLogHandler("%s %s" % (release.description, release.version)) else: - handler = logging.handlers.SysLogHandler('/dev/log') + handler = logging.handlers.SysLogHandler() format = '%s %s' % (release.description, release.version) \ + ':%(dbname)s:%(levelname)s:%(name)s:%(message)s' @@ -109,11 +151,12 @@ def init_logger(): # LogFile Handler logf = tools.config['logfile'] try: + # We check we have the right location for the log files dirname = os.path.dirname(logf) if dirname and not os.path.isdir(dirname): os.makedirs(dirname) if tools.config['logrotate'] is not False: - handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30) + handler = logging.handlers.TimedRotatingFileHandler(filename=logf, when='D', interval=1, backupCount=30) elif os.name == 'posix': handler = logging.handlers.WatchedFileHandler(logf) else: @@ -129,15 +172,23 @@ def init_logger(): # behind Apache with mod_wsgi, handler.stream will have type mod_wsgi.Log, # which has no fileno() method. (mod_wsgi.Log is what is being bound to # sys.stderr when the logging.StreamHandler is being constructed above.) - if isinstance(handler, logging.StreamHandler) \ - and hasattr(handler.stream, 'fileno') \ - and os.isatty(handler.stream.fileno()): + def is_a_tty(stream): + return hasattr(stream, 'fileno') and os.isatty(stream.fileno()) + + if isinstance(handler, logging.StreamHandler) and is_a_tty(handler.stream): formatter = ColoredFormatter(format) else: formatter = DBFormatter(format) handler.setFormatter(formatter) - # Configure handlers + logging.getLogger().addHandler(handler) + + if tools.config['log_db']: + postgresqlHandler = PostgreSQLHandler() + postgresqlHandler.setLevel(logging.WARNING) + logging.getLogger().addHandler(postgresqlHandler) + + # Configure loggers levels pseudo_config = PSEUDOCONFIG_MAPPER.get(tools.config['log_level'], []) logconfig = tools.config['log_handler'] @@ -147,26 +198,22 @@ def init_logger(): loggername, level = logconfig_item.split(':') level = getattr(logging, level, logging.INFO) logger = logging.getLogger(loggername) - logger.handlers = [] logger.setLevel(level) - logger.addHandler(handler) - if loggername != '': - logger.propagate = False for logconfig_item in logging_configurations: _logger.debug('logger level set: "%s"', logconfig_item) DEFAULT_LOG_CONFIGURATION = [ 'openerp.workflow.workitem:WARNING', - 'openerp.netsvc.rpc.request:INFO', - 'openerp.netsvc.rpc.response:INFO', + 'openerp.http.rpc.request:INFO', + 'openerp.http.rpc.response:INFO', 'openerp.addons.web.http:INFO', 'openerp.sql_db:INFO', ':INFO', ] PSEUDOCONFIG_MAPPER = { - 'debug_rpc_answer': ['openerp:DEBUG','openerp.netsvc.rpc.request:DEBUG', 'openerp.netsvc.rpc.response:DEBUG'], - 'debug_rpc': ['openerp:DEBUG','openerp.netsvc.rpc.request:DEBUG'], + 'debug_rpc_answer': ['openerp:DEBUG','openerp.http.rpc.request:DEBUG', 'openerp.http.rpc.response:DEBUG'], + 'debug_rpc': ['openerp:DEBUG','openerp.http.rpc.request:DEBUG'], 'debug': ['openerp:DEBUG'], 'debug_sql': ['openerp.sql_db:DEBUG'], 'info': [], @@ -175,99 +222,4 @@ PSEUDOCONFIG_MAPPER = { 'critical': ['openerp:CRITICAL'], } -# A alternative logging scheme for automated runs of the -# server intended to test it. -def init_alternative_logger(): - class H(logging.Handler): - def emit(self, record): - if record.levelno > 20: - print record.levelno, record.pathname, record.msg - handler = H() - # Add the handler to the 'openerp' logger. - logger = logging.getLogger('openerp') - logger.addHandler(handler) - logger.setLevel(logging.ERROR) - -def replace_request_password(args): - # password is always 3rd argument in a request, we replace it in RPC logs - # so it's easier to forward logs for diagnostics/debugging purposes... - if len(args) > 2: - args = list(args) - args[2] = '*' - return tuple(args) - -def log(logger, level, prefix, msg, depth=None): - indent='' - indent_after=' '*len(prefix) - for line in (prefix+pformat(msg, depth=depth)).split('\n'): - logger.log(level, indent+line) - indent=indent_after - -def dispatch_rpc(service_name, method, params): - """ Handle a RPC call. - - This is pure Python code, the actual marshalling (from/to XML-RPC) is done - in a upper layer. - """ - try: - rpc_request = logging.getLogger(__name__ + '.rpc.request') - rpc_response = logging.getLogger(__name__ + '.rpc.response') - rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG) - rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG) - if rpc_request_flag or rpc_response_flag: - start_time = time.time() - start_rss, start_vms = 0, 0 - start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info() - if rpc_request and rpc_response_flag: - log(rpc_request,logging.DEBUG,'%s.%s'%(service_name,method), replace_request_password(params)) - - threading.current_thread().uid = None - threading.current_thread().dbname = None - if service_name == 'common': - dispatch = openerp.service.common.dispatch - elif service_name == 'db': - dispatch = openerp.service.db.dispatch - elif service_name == 'object': - dispatch = openerp.service.model.dispatch - elif service_name == 'report': - dispatch = openerp.service.report.dispatch - else: - dispatch = openerp.service.wsgi_server.rpc_handlers.get(service_name) - result = dispatch(method, params) - - if rpc_request_flag or rpc_response_flag: - end_time = time.time() - end_rss, end_vms = 0, 0 - end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info() - logline = '%s.%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (service_name, method, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024) - if rpc_response_flag: - log(rpc_response,logging.DEBUG, logline, result) - else: - log(rpc_request,logging.DEBUG, logline, replace_request_password(params), depth=1) - - return result - except openerp.osv.orm.except_orm: - raise - except openerp.exceptions.AccessError: - raise - except openerp.exceptions.AccessDenied: - raise - except openerp.exceptions.Warning: - raise - except openerp.exceptions.RedirectWarning: - raise - except openerp.exceptions.DeferredException, e: - _logger.exception(tools.exception_to_unicode(e)) - post_mortem(e.traceback) - raise - except Exception, e: - _logger.exception(tools.exception_to_unicode(e)) - post_mortem(sys.exc_info()) - raise - -def post_mortem(info): - if tools.config['debug_mode'] and isinstance(info[2], types.TracebackType): - import pdb - pdb.post_mortem(info[2]) - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 478f982cd69..d5d0ee0fa30 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -236,15 +236,24 @@ class char(_column): class text(_column): _type = 'text' + class html(text): _type = 'html' _symbol_c = '%s' - def _symbol_f(x): - if x is None or x == False: + + def _symbol_set_html(self, value): + if value is None or value is False: return None - return html_sanitize(x) - - _symbol_set = (_symbol_c, _symbol_f) + if not self._sanitize: + return value + return html_sanitize(value) + + def __init__(self, string='unknown', sanitize=True, **args): + super(html, self).__init__(string=string, **args) + self._sanitize = sanitize + # symbol_set redefinition because of sanitize specific behavior + self._symbol_f = self._symbol_set_html + self._symbol_set = (self._symbol_c, self._symbol_f) import __builtin__ diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 3126507608f..64fa06783ab 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -75,7 +75,7 @@ _schema = logging.getLogger(__name__ + '.schema') # List of etree._Element subclasses that we choose to ignore when parsing XML. from openerp.tools import SKIPPED_ELEMENT_TYPES -regex_order = re.compile('^(([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I) +regex_order = re.compile('^( *([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I) regex_object_name = re.compile(r'^[a-z0-9_.]+$') AUTOINIT_RECALCULATE_STORED_FIELDS = 1000 @@ -1124,7 +1124,7 @@ class BaseModel(object): def _get_xml_id(self, cr, uid, r): model_data = self.pool.get('ir.model.data') - data_ids = model_data.search(cr, uid, [('model', '=', r._table_name), ('res_id', '=', r['id'])]) + data_ids = model_data.search(cr, uid, [('model', '=', r._model._name), ('res_id', '=', r['id'])]) if len(data_ids): d = model_data.read(cr, uid, data_ids, ['name', 'module'])[0] if d['module']: @@ -1134,13 +1134,13 @@ class BaseModel(object): else: postfix = 0 while True: - n = self._table+'_'+str(r['id']) + (postfix and ('_'+str(postfix)) or '' ) + n = r._model._table+'_'+str(r['id']) + (postfix and ('_'+str(postfix)) or '' ) if not model_data.search(cr, uid, [('name', '=', n)]): break postfix += 1 model_data.create(cr, SUPERUSER_ID, { 'name': n, - 'model': self._name, + 'model': r._model._name, 'res_id': r['id'], 'module': '__export__', }) @@ -2185,36 +2185,42 @@ class BaseModel(object): r['__fold'] = folded.get(r[groupby] and r[groupby][0], False) return result - def _read_group_generate_order_by(self, orderby, aggregated_fields, groupby, query): + def _read_group_prepare(self, orderby, aggregated_fields, groupby, qualified_groupby_field, query, groupby_type=None): """ - Generates the ORDER BY sql clause for the read group method. Adds the missing JOIN clause + Prepares the GROUP BY and ORDER BY terms for the read_group method. Adds the missing JOIN clause to the query if order should be computed against m2o field. :param orderby: the orderby definition in the form "%(field)s %(order)s" :param aggregated_fields: list of aggregated fields in the query :param groupby: the current groupby field name - :param query: the query object used to construct the query afterwards + :param qualified_groupby_field: the fully qualified SQL name for the grouped field + :param osv.Query query: the query under construction + :param groupby_type: the type of the grouped field + :return: (groupby_terms, orderby_terms) """ - orderby_list = [] - ob = [] - for order_splits in orderby.split(','): - order_split = order_splits.split() - orderby_field = order_split[0] - fields = openerp.osv.fields - if isinstance(self._all_columns[orderby_field].column, (fields.date, fields.datetime)): - continue - orderby_dir = len(order_split) == 2 and order_split[1].upper() == 'ASC' and 'ASC' or 'DESC' - if orderby_field == groupby: - orderby_item = self._generate_order_by(order_splits, query).replace('ORDER BY ', '') - if orderby_item: - orderby_list.append(orderby_item) - ob += [obi.split()[0] for obi in orderby_item.split(',')] - elif orderby_field in aggregated_fields: - orderby_list.append('%s %s' % (orderby_field,orderby_dir)) + orderby_terms = [] + groupby_terms = [qualified_groupby_field] if groupby else [] + if not orderby: + return groupby_terms, orderby_terms - if orderby_list: - return ' ORDER BY %s' % (','.join(orderby_list)), ob and ','.join(ob) or '' - else: - return '', '' + self._check_qorder(orderby) + for order_part in orderby.split(','): + order_split = order_part.split() + order_field = order_split[0] + if order_field == groupby: + if groupby_type == 'many2one': + order_clause = self._generate_order_by(order_part, query).replace('ORDER BY ', '') + if order_clause: + orderby_terms.append(order_clause) + groupby_terms += [order_term.split()[0] for order_term in order_clause.split(',')] + else: + orderby_terms.append(order_part) + elif order_field in aggregated_fields: + orderby_terms.append(order_part) + else: + # Cannot order by a field that will not appear in the results (needs to be grouped or aggregated) + _logger.warn('%s: read_group order by `%s` ignored, cannot sort on empty columns (not grouped/aggregated)', + self._name, order_part) + return groupby_terms, orderby_terms def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): """ @@ -2275,9 +2281,9 @@ class BaseModel(object): # TODO it seems fields_get can be replaced by _all_columns (no need for translation) fget = self.fields_get(cr, uid, fields) - flist = '' - group_count = group_by = groupby group_by_params = {} + select_terms = [] + groupby_type = None if groupby: if fget.get(groupby): groupby_type = fget[groupby]['type'] @@ -2305,12 +2311,9 @@ class BaseModel(object): timezone = context.get('tz', 'UTC') qualified_groupby_field = "timezone('%s', timezone('UTC',%s))" % (timezone, qualified_groupby_field) qualified_groupby_field = "date_trunc('%s', %s)" % (interval, qualified_groupby_field) - flist = "%s as %s " % (qualified_groupby_field, groupby) elif groupby_type == 'boolean': qualified_groupby_field = "coalesce(%s,false)" % qualified_groupby_field - flist = "%s as %s " % (qualified_groupby_field, groupby) - else: - flist = qualified_groupby_field + select_terms.append("%s as %s " % (qualified_groupby_field, groupby)) else: # Don't allow arbitrary values, as this would be a SQL injection vector! raise except_orm(_('Invalid group_by'), @@ -2318,34 +2321,48 @@ class BaseModel(object): aggregated_fields = [ f for f in fields - if f not in ('id', 'sequence') + if f not in ('id', 'sequence', groupby) if fget[f]['type'] in ('integer', 'float') if (f in self._all_columns and getattr(self._all_columns[f].column, '_classic_write'))] for f in aggregated_fields: group_operator = fget[f].get('group_operator', 'sum') - if flist: - flist += ', ' qualified_field = self._inherits_join_calc(f, query) - flist += "%s(%s) AS %s" % (group_operator, qualified_field, f) + select_terms.append("%s(%s) AS %s" % (group_operator, qualified_field, f)) - order = orderby or groupby - orderby_clause = '' - ob = '' - if order: - orderby_clause, ob = self._read_group_generate_order_by(order, aggregated_fields, groupby, query) - - gb = groupby and (' GROUP BY ' + qualified_groupby_field) or '' + order = orderby or groupby or '' + groupby_terms, orderby_terms = self._read_group_prepare(order, aggregated_fields, groupby, qualified_groupby_field, query, groupby_type) from_clause, where_clause, where_clause_params = query.get_sql() - where_clause = where_clause and ' WHERE ' + where_clause - limit_str = limit and ' limit %d' % limit or '' - offset_str = offset and ' offset %d' % offset or '' if len(groupby_list) < 2 and context.get('group_by_no_leaf'): - group_count = '_' - cr.execute('SELECT min(%s.id) AS id, count(%s.id) AS %s_count' % (self._table, self._table, group_count) + (flist and ',') + flist + ' FROM ' + from_clause + where_clause + gb + (ob and ',') + ob + orderby_clause + limit_str + offset_str, where_clause_params) - alldata = {} - groupby = group_by + count_field = '_' + else: + count_field = groupby + prefix_terms = lambda prefix, terms: (prefix + " " + ",".join(terms)) if terms else '' + prefix_term = lambda prefix, term: ('%s %s' % (prefix, term)) if term else '' + + query = """ + SELECT min(%(table)s.id) AS id, count(%(table)s.id) AS %(count_field)s_count + %(extra_fields)s + FROM %(from)s + %(where)s + %(groupby)s + %(orderby)s + %(limit)s + %(offset)s + """ % { + 'table': self._table, + 'count_field': count_field, + 'extra_fields': prefix_terms(',', select_terms), + 'from': from_clause, + 'where': prefix_term('WHERE', where_clause), + 'groupby': prefix_terms('GROUP BY', groupby_terms), + 'orderby': prefix_terms('ORDER BY', orderby_terms), + 'limit': prefix_term('LIMIT', int(limit) if limit else None), + 'offset': prefix_term('OFFSET', int(offset) if limit else None), + } + cr.execute(query, where_clause_params) + alldata = {} fetched_data = cr.dictfetchall() data_ids = [] @@ -2356,8 +2373,6 @@ class BaseModel(object): data_ids.append(r['id']) del r['id'] - - if groupby: data = self.read(cr, uid, data_ids, [groupby], context=context) # restore order of the search as read() uses the default _order (this is only for groups, so the footprint of data should be small): @@ -2847,7 +2862,7 @@ class BaseModel(object): msg = "Table '%s': dropping index for column '%s' of type '%s' as it is not required anymore" _schema.debug(msg, self._table, k, f._type) - if isinstance(f, fields.many2one): + if isinstance(f, fields.many2one) or (isinstance(f, fields.function) and f._type == 'many2one' and f.store): dest_model = self.pool[f._obj] if dest_model._table != 'ir_actions': self._m2o_fix_foreign_key(cr, self._table, k, dest_model, f.ondelete) @@ -2882,7 +2897,7 @@ class BaseModel(object): todo_end.append((order, self._update_store, (f, k))) # and add constraints if needed - if isinstance(f, fields.many2one): + if isinstance(f, fields.many2one) or (isinstance(f, fields.function) and f._type == 'many2one' and f.store): if f._obj not in self.pool: raise except_orm('Programming Error', 'There is no reference available for %s' % (f._obj,)) dest_model = self.pool[f._obj] diff --git a/openerp/release.py b/openerp/release.py index 1ad35340496..1a9c863660c 100644 --- a/openerp/release.py +++ b/openerp/release.py @@ -32,7 +32,7 @@ RELEASE_LEVELS_DISPLAY = {ALPHA: ALPHA, # (6,1,0,'candidate',2) < (6,1,0,'final',0) < (6,1,2,'final',0) version_info = (8, 0, 0, ALPHA, 1) version = '.'.join(map(str, version_info[:2])) + RELEASE_LEVELS_DISPLAY[version_info[3]] + str(version_info[4] or '') -serie = major_version = '.'.join(map(str, version_info[:2])) +series = serie = major_version = '.'.join(map(str, version_info[:2])) description = 'OpenERP Server' long_desc = '''OpenERP is a complete ERP and CRM. The main features are accounting (analytic @@ -50,6 +50,6 @@ author = 'OpenERP S.A.' author_email = 'info@openerp.com' license = 'AGPL-3' -nt_service_name = "openerp-server-" + serie +nt_service_name = "openerp-server-" + series # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/db.py b/openerp/service/db.py index adbd058755d..f10f39aab9b 100644 --- a/openerp/service/db.py +++ b/openerp/service/db.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- - -import base64 -import contextlib +from contextlib import closing +from functools import wraps import logging import os +import shutil import threading import traceback -from contextlib import contextmanager, closing +import tempfile +import zipfile + +import psycopg2 import openerp from openerp import SUPERUSER_ID @@ -28,7 +31,8 @@ def _initialize_db(id, db_name, demo, lang, user_password): self_actions[id]['progress'] = 0 db = openerp.sql_db.db_connect(db_name) with closing(db.cursor()) as cr: - openerp.modules.db.initialize(cr) # TODO this should be removed as it is done by RegistryManager.new(). + # TODO this should be removed as it is done by RegistryManager.new(). + openerp.modules.db.initialize(cr) openerp.tools.config['lang'] = lang cr.commit() @@ -55,14 +59,13 @@ def _initialize_db(id, db_name, demo, lang, user_password): self_actions[id]['traceback'] = traceback.format_exc() def dispatch(method, params): - if method in [ 'create', 'get_progress', 'drop', 'dump', - 'restore', 'rename', - 'change_admin_password', 'migrate_databases', - 'create_database', 'duplicate_database' ]: + if method in ['create', 'get_progress', 'drop', 'dump', 'restore', 'rename', + 'change_admin_password', 'migrate_databases', + 'create_database', 'duplicate_database']: passwd = params[0] params = params[1:] security.check_super(passwd) - elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]: + elif method in ['db_exist', 'list', 'list_lang', 'server_version']: # params = params # No security check for these methods pass @@ -78,9 +81,9 @@ def _create_empty_database(name): cr.execute("SELECT datname FROM pg_database WHERE datname = %s", (name,)) if cr.fetchall(): - raise openerp.exceptions.Warning(" %s database already exists!" % name ) + raise openerp.exceptions.Warning("database %r already exists!" % (name,)) else: - cr.autocommit(True) # avoid transaction block + cr.autocommit(True) # avoid transaction block cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "%s" """ % (name, chosen_template)) def exp_create(db_name, demo, lang, user_password='admin'): @@ -96,7 +99,7 @@ def exp_create(db_name, demo, lang, user_password='admin'): _logger.info('CREATE DATABASE %s', db_name.lower()) create_thread = threading.Thread(target=_initialize_db, - args=(id, db_name, demo, lang, user_password)) + args=(id, db_name, demo, lang, user_password)) create_thread.start() self_actions[id]['thread'] = create_thread return id @@ -121,14 +124,14 @@ def exp_duplicate_database(db_original_name, db_name): openerp.sql_db.close_db(db_original_name) db = openerp.sql_db.db_connect('postgres') with closing(db.cursor()) as cr: - cr.autocommit(True) # avoid transaction block + cr.autocommit(True) # avoid transaction block cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "%s" """ % (db_name, db_original_name)) return True def exp_get_progress(id): if self_actions[id]['thread'].isAlive(): # return openerp.modules.init_progress[db_name] - return min(self_actions[id].get('progress', 0),0.95), [] + return min(self_actions[id].get('progress', 0), 0.95), [] else: clean = self_actions[id]['clean'] if clean: @@ -140,9 +143,9 @@ def exp_get_progress(id): self_actions.pop(id) return 1.0, users else: - e = self_actions[id]['exception'] # TODO this seems wrong: actions[id]['traceback'] is set, but not 'exception'. - self_actions.pop(id) - raise Exception, e + a = self_actions.pop(id) + exc, tb = a['exception'], a['traceback'] + raise Exception, exc, tb def exp_drop(db_name): if db_name not in exp_list(True): @@ -152,18 +155,17 @@ def exp_drop(db_name): db = openerp.sql_db.db_connect('postgres') with closing(db.cursor()) as cr: - cr.autocommit(True) # avoid transaction block + cr.autocommit(True) # avoid transaction block # Try to terminate all other connections that might prevent # dropping the database try: - # PostgreSQL 9.2 renamed pg_stat_activity.procpid to pid: # http://www.postgresql.org/docs/9.2/static/release-9-2.html#AEN110389 pid_col = 'pid' if cr._cnx.server_version >= 90200 else 'procpid' cr.execute("""SELECT pg_terminate_backend(%(pid_col)s) FROM pg_stat_activity - WHERE datname = %%s AND + WHERE datname = %%s AND %(pid_col)s != pg_backend_pid()""" % {'pid_col': pid_col}, (db_name,)) except Exception: @@ -178,94 +180,140 @@ def exp_drop(db_name): _logger.info('DROP DB: %s', db_name) return True -@contextlib.contextmanager -def _set_pg_password_in_environment(): +def _set_pg_password_in_environment(func): """ On systems where pg_restore/pg_dump require an explicit password (i.e. when not connecting via unix sockets, and most importantly on Windows), it is necessary to pass the PG user password in the environment or in a special .pgpass file. - This context management method handles setting + This decorator handles setting :envvar:`PGPASSWORD` if it is not already set, and removing it afterwards. See also http://www.postgresql.org/docs/8.4/static/libpq-envars.html - + .. note:: This is not thread-safe, and should never be enabled for SaaS (giving SaaS users the super-admin password is not a good idea anyway) """ - if os.environ.get('PGPASSWORD') or not openerp.tools.config['db_password']: - yield - else: - os.environ['PGPASSWORD'] = openerp.tools.config['db_password'] - try: - yield - finally: - del os.environ['PGPASSWORD'] - + @wraps(func) + def wrapper(*args, **kwargs): + if os.environ.get('PGPASSWORD') or not openerp.tools.config['db_password']: + return func(*args, **kwargs) + else: + os.environ['PGPASSWORD'] = openerp.tools.config['db_password'] + try: + return func(*args, **kwargs) + finally: + del os.environ['PGPASSWORD'] + return wrapper def exp_dump(db_name): - with _set_pg_password_in_environment(): - cmd = ['pg_dump', '--format=c', '--no-owner'] + with tempfile.TemporaryFile() as t: + dump_db(db_name, t) + t.seek(0) + return t.read().encode('base64') + +@_set_pg_password_in_environment +def dump_db(db, stream): + """Dump database `db` into file-like object `stream`""" + with openerp.tools.osutil.tempdir() as dump_dir: + registry = openerp.modules.registry.RegistryManager.get(db) + with registry.cursor() as cr: + filestore = registry['ir.attachment']._filestore(cr, SUPERUSER_ID) + if os.path.exists(filestore): + shutil.copytree(filestore, os.path.join(dump_dir, 'filestore')) + + dump_file = os.path.join(dump_dir, 'dump.sql') + cmd = ['pg_dump', '--format=p', '--no-owner', '--file=' + dump_file] if openerp.tools.config['db_user']: cmd.append('--username=' + openerp.tools.config['db_user']) if openerp.tools.config['db_host']: cmd.append('--host=' + openerp.tools.config['db_host']) if openerp.tools.config['db_port']: cmd.append('--port=' + str(openerp.tools.config['db_port'])) - cmd.append(db_name) + cmd.append(db) - stdin, stdout = openerp.tools.exec_pg_command_pipe(*tuple(cmd)) - stdin.close() - data = stdout.read() - res = stdout.close() + if openerp.tools.exec_pg_command(*cmd): + _logger.error('DUMP DB: %s failed! Please verify the configuration of the database ' + 'password on the server. You may need to create a .pgpass file for ' + 'authentication, or specify `db_password` in the server configuration ' + 'file.', db) + raise Exception("Couldn't dump database") - if not data or res: - _logger.error( - 'DUMP DB: %s failed! Please verify the configuration of the database password on the server. ' - 'You may need to create a .pgpass file for authentication, or specify `db_password` in the ' - 'server configuration file.\n %s', db_name, data) - raise Exception, "Couldn't dump database" - _logger.info('DUMP DB successful: %s', db_name) + openerp.tools.osutil.zip_dir(dump_dir, stream, include_dir=False) - return base64.encodestring(data) + _logger.info('DUMP DB successful: %s', db) -def exp_restore(db_name, data): - with _set_pg_password_in_environment(): - if exp_db_exist(db_name): - _logger.warning('RESTORE DB: %s already exists', db_name) - raise Exception, "Database already exists" +def exp_restore(db_name, data, copy=False): + data_file = tempfile.NamedTemporaryFile(delete=False) + try: + data_file.write(data.decode('base64')) + data_file.close() + restore_db(db_name, data_file.name, copy=copy) + finally: + os.unlink(data_file.name) + return True - _create_empty_database(db_name) +@_set_pg_password_in_environment +def restore_db(db, dump_file, copy=False): + assert isinstance(db, basestring) + if exp_db_exist(db): + _logger.warning('RESTORE DB: %s already exists', db) + raise Exception("Database already exists") - cmd = ['pg_restore', '--no-owner'] + _create_empty_database(db) + + filestore_path = None + with openerp.tools.osutil.tempdir() as dump_dir: + if zipfile.is_zipfile(dump_file): + # v8 format + with zipfile.ZipFile(dump_file, 'r') as z: + # only extract known members! + filestore = [m for m in z.namelist() if m.startswith('filestore/')] + z.extractall(dump_dir, ['dump.sql'] + filestore) + + if filestore: + filestore_path = os.path.join(dump_dir, 'filestore') + + pg_cmd = 'psql' + pg_args = ['-q', '-f', os.path.join(dump_dir, 'dump.sql')] + + else: + # <= 7.0 format (raw pg_dump output) + pg_cmd = 'pg_restore' + pg_args = ['--no-owner', dump_file] + + args = [] if openerp.tools.config['db_user']: - cmd.append('--username=' + openerp.tools.config['db_user']) + args.append('--username=' + openerp.tools.config['db_user']) if openerp.tools.config['db_host']: - cmd.append('--host=' + openerp.tools.config['db_host']) + args.append('--host=' + openerp.tools.config['db_host']) if openerp.tools.config['db_port']: - cmd.append('--port=' + str(openerp.tools.config['db_port'])) - cmd.append('--dbname=' + db_name) - args2 = tuple(cmd) + args.append('--port=' + str(openerp.tools.config['db_port'])) + args.append('--dbname=' + db) + pg_args = args + pg_args - buf=base64.decodestring(data) - if os.name == "nt": - tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam() - file(tmpfile, 'wb').write(buf) - args2=list(args2) - args2.append(tmpfile) - args2=tuple(args2) - stdin, stdout = openerp.tools.exec_pg_command_pipe(*args2) - if not os.name == "nt": - stdin.write(base64.decodestring(data)) - stdin.close() - res = stdout.close() - if res: - raise Exception, "Couldn't restore database" - _logger.info('RESTORE DB: %s', db_name) + if openerp.tools.exec_pg_command(pg_cmd, *pg_args): + raise Exception("Couldn't restore database") - return True + registry = openerp.modules.registry.RegistryManager.new(db) + with registry.cursor() as cr: + if copy: + # if it's a copy of a database, force generation of a new dbuuid + registry['ir.config_parameter'].init(cr, force=True) + if filestore_path: + filestore_dest = registry['ir.attachment']._filestore(cr, SUPERUSER_ID) + shutil.move(filestore_path, filestore_dest) + + if openerp.tools.config['unaccent']: + try: + with cr.savepoint(): + cr.execute("CREATE EXTENSION unaccent") + except psycopg2.Error: + pass + + _logger.info('RESTORE DB: %s', db) def exp_rename(old_name, new_name): openerp.modules.registry.RegistryManager.delete(old_name) @@ -273,20 +321,16 @@ def exp_rename(old_name, new_name): db = openerp.sql_db.db_connect('postgres') with closing(db.cursor()) as cr: - cr.autocommit(True) # avoid transaction block + cr.autocommit(True) # avoid transaction block try: cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name)) + _logger.info('RENAME DB: %s -> %s', old_name, new_name) except Exception, e: _logger.error('RENAME DB: %s -> %s failed:\n%s', old_name, new_name, e) raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e)) - else: - fs = os.path.join(openerp.tools.config['root_path'], 'filestore') - if os.path.exists(os.path.join(fs, old_name)): - os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name)) - - _logger.info('RENAME DB: %s -> %s', old_name, new_name) return True +@openerp.tools.mute_logger('openerp.sql_db') def exp_db_exist(db_name): ## Not True: in fact, check if connection to database is possible. The database may exists return bool(openerp.sql_db.db_connect(db_name)) diff --git a/openerp/service/server.py b/openerp/service/server.py index 5372459cd41..f9db66a28ef 100644 --- a/openerp/service/server.py +++ b/openerp/service/server.py @@ -9,8 +9,10 @@ import os.path import platform import psutil import random -import re -import resource +if os.name == 'posix': + import resource +else: + resource = None import select import signal import socket @@ -32,15 +34,14 @@ except ImportError: setproctitle = lambda x: None import openerp -import openerp.tools.config as config +from openerp.modules.registry import RegistryManager from openerp.release import nt_service_name +import openerp.tools.config as config from openerp.tools.misc import stripped_sys_argv, dumpstacks -import wsgi_server - _logger = logging.getLogger(__name__) -SLEEP_INTERVAL = 60 # 1 min +SLEEP_INTERVAL = 60 # 1 min #---------------------------------------------------------- # Werkzeug WSGI servers patched @@ -110,15 +111,14 @@ class AutoReload(object): self.handler = EventHandler(self) self.notifier = pyinotify.Notifier(self.wm, self.handler, timeout=0) mask = pyinotify.IN_MODIFY | pyinotify.IN_CREATE # IN_MOVED_FROM, IN_MOVED_TO ? - for path in openerp.tools.config.options["addons_path"].split(','): + for path in openerp.modules.modules.ad_paths: _logger.info('Watching addons folder %s', path) self.wm.add_watch(path, mask, rec=True) def process_data(self, files): xml_files = [i for i in files if i.endswith('.xml')] - addons_path = openerp.tools.config.options["addons_path"].split(',') for i in xml_files: - for path in addons_path: + for path in openerp.modules.modules.ad_paths: if i.startswith(path): # find out wich addons path the file belongs to # and extract it's module name @@ -126,7 +126,7 @@ class AutoReload(object): if len(right) < 2: continue module = right[0] - self.modules[module]=1 + self.modules[module] = 1 if self.modules: _logger.info('autoreload: xml change detected, autoreload activated') restart() @@ -146,7 +146,7 @@ class AutoReload(object): if py_errors: _logger.info('autoreload: python code change detected, errors found') for i in py_errors: - _logger.info('autoreload: SyntaxError %s',i) + _logger.info('autoreload: SyntaxError %s', i) else: _logger.info('autoreload: python code updated, autoreload activated') restart() @@ -213,7 +213,7 @@ class ThreadedServer(CommonServer): self.httpd = None def signal_handler(self, sig, frame): - if sig in [signal.SIGINT,signal.SIGTERM]: + if sig in [signal.SIGINT, signal.SIGTERM]: # shutdown on kill -INT or -TERM self.quit_signals_received += 1 if self.quit_signals_received > 1: @@ -227,7 +227,7 @@ class ThreadedServer(CommonServer): def cron_thread(self, number): while True: - time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style + time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style registries = openerp.modules.registry.RegistryManager.registries _logger.debug('cron%d polling for jobs', number) for db_name, registry in registries.items(): @@ -257,13 +257,15 @@ class ThreadedServer(CommonServer): _logger.debug("cron%d started!" % i) def http_thread(self): - def app(e,s): - return self.app(e,s) + def app(e, s): + return self.app(e, s) self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, app) self.httpd.serve_forever() def http_spawn(self): - threading.Thread(target=self.http_thread).start() + t = threading.Thread(target=self.http_thread, name="openerp.service.httpd") + t.setDaemon(True) + t.start() _logger.info('HTTP service (werkzeug) running on %s:%s', self.interface, self.port) def start(self): @@ -276,7 +278,7 @@ class ThreadedServer(CommonServer): signal.signal(signal.SIGQUIT, dumpstacks) elif os.name == 'nt': import win32api - win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1) + win32api.SetConsoleCtrlHandler(lambda sig: self.signal_handler(sig, None), 1) self.cron_spawn() self.http_spawn() @@ -286,8 +288,9 @@ class ThreadedServer(CommonServer): _logger.info("Initiating shutdown") _logger.info("Hit CTRL-C again or send a second signal to force the shutdown.") - self.httpd.shutdown() - self.close_socket(self.httpd.socket) + if self.httpd: + self.httpd.shutdown() + self.close_socket(self.httpd.socket) # Manually join() all threads before calling sys.exit() to allow a second signal # to trigger _force_quit() in case some non-daemon threads won't exit cleanly. @@ -316,12 +319,11 @@ class ThreadedServer(CommonServer): """ self.start() - preload_registries(preload) + rc = preload_registries(preload) if stop: self.stop() - return - + return rc # Wait for a first signal to be handled. (time.sleep will be interrupted # by the signal handler.) The try/except is for the win32 case. @@ -438,7 +440,7 @@ class PreforkServer(CommonServer): sys.exit(0) def long_polling_spawn(self): - nargs = stripped_sys_argv('--pidfile','--workers') + nargs = stripped_sys_argv('--pidfile', '--workers') cmd = nargs[0] cmd = os.path.join(os.path.dirname(cmd), "openerp-gevent") nargs[0] = cmd @@ -447,10 +449,10 @@ class PreforkServer(CommonServer): def worker_pop(self, pid): if pid in self.workers: - _logger.debug("Worker (%s) unregistered",pid) + _logger.debug("Worker (%s) unregistered", pid) try: - self.workers_http.pop(pid,None) - self.workers_cron.pop(pid,None) + self.workers_http.pop(pid, None) + self.workers_cron.pop(pid, None) u = self.workers.pop(pid) u.close() except OSError: @@ -466,7 +468,7 @@ class PreforkServer(CommonServer): def process_signals(self): while len(self.queue): sig = self.queue.pop(0) - if sig in [signal.SIGINT,signal.SIGTERM]: + if sig in [signal.SIGINT, signal.SIGTERM]: raise KeyboardInterrupt elif sig == signal.SIGHUP: # restart on kill -HUP @@ -502,8 +504,8 @@ class PreforkServer(CommonServer): def process_timeout(self): now = time.time() for (pid, worker) in self.workers.items(): - if (worker.watchdog_timeout is not None) and \ - (now - worker.watchdog_time >= worker.watchdog_timeout): + if worker.watchdog_timeout is not None and \ + (now - worker.watchdog_time) >= worker.watchdog_timeout: _logger.error("Worker (%s) timeout", pid) self.worker_kill(pid, signal.SIGKILL) @@ -518,7 +520,7 @@ class PreforkServer(CommonServer): def sleep(self): try: # map of fd -> worker - fds = dict([(w.watchdog_pipe[0],w) for k,w in self.workers.items()]) + fds = dict([(w.watchdog_pipe[0], w) for k, w in self.workers.items()]) fd_in = fds.keys() + [self.pipe[0]] # check for ping or internal wakeups ready = select.select(fd_in, [], [], self.beat) @@ -558,11 +560,12 @@ class PreforkServer(CommonServer): self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setblocking(0) self.socket.bind(self.address) - self.socket.listen(8*self.population) + self.socket.listen(8 * self.population) def stop(self, graceful=True): if self.long_polling_pid is not None: - self.worker_kill(self.long_polling_pid, signal.SIGKILL) # FIXME make longpolling process handle SIGTERM correctly + # FIXME make longpolling process handle SIGTERM correctly + self.worker_kill(self.long_polling_pid, signal.SIGKILL) self.long_polling_pid = None if graceful: _logger.info("Stopping gracefully") @@ -581,11 +584,11 @@ class PreforkServer(CommonServer): def run(self, preload, stop): self.start() - preload_registries(preload) + rc = preload_registries(preload) if stop: self.stop() - return + return rc _logger.debug("Multiprocess starting") while 1: @@ -600,7 +603,7 @@ class PreforkServer(CommonServer): _logger.debug("Multiprocess clean stop") self.stop() break - except Exception,e: + except Exception, e: _logger.exception(e) self.stop(False) return -1 @@ -632,12 +635,14 @@ class Worker(object): def sleep(self): try: - ret = select.select([self.multi.socket], [], [], self.multi.beat) + select.select([self.multi.socket], [], [], self.multi.beat) except select.error, e: if e[0] not in [errno.EINTR]: raise def process_limit(self): + if resource is None: + return # If our parent changed sucide if self.ppid != os.getppid(): _logger.info("Worker (%s) Parent changed", self.pid) @@ -650,7 +655,7 @@ class Worker(object): rss, vms = psutil.Process(os.getpid()).get_memory_info() if vms > config['limit_memory_soft']: _logger.info('Worker (%d) virtual memory limit (%s) reached.', self.pid, vms) - self.alive = False # Commit suicide after the request. + self.alive = False # Commit suicide after the request. # VMS and RLIMIT_AS are the same thing: virtual memory, a.k.a. address space soft, hard = resource.getrlimit(resource.RLIMIT_AS) @@ -698,7 +703,7 @@ class Worker(object): self.process_work() _logger.info("Worker (%s) exiting. request_count: %s.", self.pid, self.request_count) self.stop() - except Exception,e: + except Exception: _logger.exception("Worker (%s) Exception occured, exiting..." % self.pid) # should we use 3 to abort everything ? sys.exit(1) @@ -716,7 +721,7 @@ class WorkerHTTP(Worker): # tolerate broken pipe when the http client closes the socket before # receiving the full reply try: - self.server.process_request(client,addr) + self.server.process_request(client, addr) except IOError, e: if e.errno != errno.EPIPE: raise @@ -747,7 +752,7 @@ class WorkerCron(Worker): def sleep(self): # Really sleep once all the databases have been processed. if self.db_index == 0: - interval = SLEEP_INTERVAL + self.pid % 10 # chorus effect + interval = SLEEP_INTERVAL + self.pid % 10 # chorus effect time.sleep(interval) def _db_list(self): @@ -769,7 +774,7 @@ class WorkerCron(Worker): if rpc_request_flag: start_time = time.time() start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info() - + import openerp.addons.base as base base.ir.ir_cron.ir_cron._acquire_job(db_name) openerp.modules.registry.RegistryManager.delete(db_name) @@ -778,16 +783,18 @@ class WorkerCron(Worker): if len(db_names) > 1: openerp.sql_db.close_db(db_name) if rpc_request_flag: - end_time = time.time() + run_time = time.time() - start_time end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info() - logline = '%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (db_name, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024) + vms_diff = (end_vms - start_vms) / 1024 + logline = '%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % \ + (db_name, run_time, start_vms / 1024, end_vms / 1024, vms_diff) _logger.debug("WorkerCron (%s) %s", self.pid, logline) self.request_count += 1 if self.request_count >= self.request_max and self.request_max < len(db_names): _logger.error("There are more dabatases to process than allowed " - "by the `limit_request` configuration variable: %s more.", - len(db_names) - self.request_max) + "by the `limit_request` configuration variable: %s more.", + len(db_names) - self.request_max) else: self.db_index = 0 @@ -820,18 +827,17 @@ def _reexec(updated_modules=None): subprocess.call('net stop {0} && net start {0}'.format(nt_service_name), shell=True) exe = os.path.basename(sys.executable) args = stripped_sys_argv() - args += ["-u", ','.join(updated_modules)] + args += ["-u", ','.join(updated_modules)] if not args or args[0] != exe: args.insert(0, exe) os.execv(sys.executable, args) -def load_test_file_yml(test_file): - cr = registry.db.cursor() - openerp.tools.convert_yaml_import(cr, 'base', file(test_file), 'test', {}, 'test', True) - cr.rollback() - cr.close() +def load_test_file_yml(registry, test_file): + with registry.cursor() as cr: + openerp.tools.convert_yaml_import(cr, 'base', file(test_file), 'test', {}, 'test', True) + cr.rollback() -def load_test_file_py(test_file): +def load_test_file_py(registry, test_file): # Locate python module based on its filename and run the tests test_path, _ = os.path.splitext(os.path.abspath(test_file)) for mod_name, mod_mod in sys.modules.items(): @@ -842,9 +848,12 @@ def load_test_file_py(test_file): for t in unittest2.TestLoader().loadTestsFromModule(mod_mod): suite.addTest(t) _logger.log(logging.INFO, 'running tests %s.', mod_mod.__name__) - result = unittest2.TextTestRunner(verbosity=2, stream=openerp.modules.module.TestStream()).run(suite) - if not result.wasSuccessful(): - r = False + stream = openerp.modules.module.TestStream() + result = unittest2.TextTestRunner(verbosity=2, stream=stream).run(suite) + success = result.wasSuccessful() + if hasattr(registry._assertion_report,'report_result'): + registry._assertion_report.report_result(success) + if not success: _logger.error('%s: at least one error occurred in a test', test_file) def preload_registries(dbnames): @@ -853,20 +862,25 @@ def preload_registries(dbnames): config = openerp.tools.config test_file = config['test_file'] dbnames = dbnames or [] + rc = 0 for dbname in dbnames: try: update_module = config['init'] or config['update'] - registry = openerp.modules.registry.RegistryManager.new(dbname, update_module=update_module) + registry = RegistryManager.new(dbname, update_module=update_module) # run test_file if provided if test_file: _logger.info('loading test file %s', test_file) if test_file.endswith('yml'): - load_test_file_yml(test_file) + load_test_file_yml(registry, test_file) elif test_file.endswith('py'): - load_test_file_py(test_file) + load_test_file_py(registry, test_file) + + if registry._assertion_report.failures: + rc += 1 except Exception: - _logger.exception('Failed to initialize database `%s`.', dbname) - return + _logger.critical('Failed to initialize database `%s`.', dbname, exc_info=True) + return -1 + return rc def start(preload=None, stop=False): """ Start the openerp http server and cron processor. diff --git a/openerp/service/wsgi_server.py b/openerp/service/wsgi_server.py index 506a07d0295..b31736ba998 100644 --- a/openerp/service/wsgi_server.py +++ b/openerp/service/wsgi_server.py @@ -72,7 +72,7 @@ def xmlrpc_return(start_response, service, method, params, string_faultcode=Fals # This also mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for # exception handling. try: - result = openerp.netsvc.dispatch_rpc(service, method, params) + result = openerp.http.dispatch_rpc(service, method, params) response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) except Exception, e: if string_faultcode: diff --git a/openerp/sql_db.py b/openerp/sql_db.py index 694ae02b3d9..59841656d8c 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -148,7 +148,7 @@ class Cursor(object): def check(f): @wraps(f) def wrapper(self, *args, **kwargs): - if self.__closed: + if self._closed: msg = 'Unable to use a closed cursor.' if self.__closer: msg += ' It was closed at %s, line %s' % self.__closer @@ -165,7 +165,7 @@ class Cursor(object): self.sql_log = _logger.isEnabledFor(logging.DEBUG) self.sql_log_count = 0 - self.__closed = True # avoid the call of close() (by __del__) if an exception + self._closed = True # avoid the call of close() (by __del__) if an exception # is raised by any of the following initialisations self._pool = pool self.dbname = dbname @@ -180,7 +180,7 @@ class Cursor(object): self.__caller = frame_codeinfo(currentframe(),2) else: self.__caller = False - self.__closed = False # real initialisation value + self._closed = False # real initialisation value self.autocommit(False) self.__closer = False @@ -189,7 +189,7 @@ class Cursor(object): self.cache = {} def __del__(self): - if not self.__closed and not self._cnx.closed: + if not self._closed and not self._cnx.closed: # Oops. 'self' has not been closed explicitly. # The cursor will be deleted by the garbage collector, # but the database connection is not put back into the connection @@ -302,7 +302,7 @@ class Cursor(object): # collected as fast as they should). The problem is probably due in # part because browse records keep a reference to the cursor. del self._obj - self.__closed = True + self._closed = True # Clean the underlying connection. self._cnx.rollback() diff --git a/openerp/tests/common.py b/openerp/tests/common.py index 2f10371a8f0..a53a8d82240 100644 --- a/openerp/tests/common.py +++ b/openerp/tests/common.py @@ -13,7 +13,11 @@ import subprocess import threading import time import unittest2 +import urllib2 import xmlrpclib +from datetime import datetime, timedelta + +import werkzeug import openerp @@ -45,10 +49,9 @@ def acquire_test_cursor(session_id): cr._test_lock.acquire() return cr -def release_test_cursor(session_id): +def release_test_cursor(cr): if openerp.tools.config['test_enable']: - cr = HTTP_SESSION.get(session_id) - if cr: + if hasattr(cr, '_test_lock'): cr._test_lock.release() return True return False @@ -150,7 +153,7 @@ class SingleTransactionCase(BaseCase): class HttpCase(TransactionCase): - """ Transactionnal HTTP TestCase with a phantomjs helper. + """ Transactionnal HTTP TestCase with url_open and phantomjs helpers. """ def __init__(self, methodName='runTest'): @@ -171,11 +174,17 @@ class HttpCase(TransactionCase): self.cr._test_lock = threading.RLock() HTTP_SESSION[self.session_id] = self.cr - def tearDown(self): del HTTP_SESSION[self.session_id] super(HttpCase, self).tearDown() + def url_open(self, url, data=None, timeout=10): + opener = urllib2.build_opener() + opener.addheaders.append(('Cookie', 'session_id=%s' % self.session_id)) + if url.startswith('/'): + url = "http://localhost:%s%s" % (PORT, url) + return opener.open(url, data, timeout) + def phantom_poll(self, phantom, timeout): """ Phantomjs Test protocol. @@ -187,11 +196,12 @@ class HttpCase(TransactionCase): Other lines are relayed to the test log. """ - t0 = time.time() + t0 = datetime.now() + td = timedelta(seconds=timeout) buf = bytearray() while True: # timeout - self.assertLess(time.time(), t0 + timeout, + self.assertLess(datetime.now() - t0, td, "PhantomJS tests should take less than %s seconds" % timeout) # read a byte @@ -202,7 +212,8 @@ class HttpCase(TransactionCase): # OSError, and no errno/strerror/filename, only a pair of # unnamed arguments (matching errno and strerror) err, _ = e.args - if err == errno.EINTR: continue + if err == errno.EINTR: + continue raise if ready: @@ -214,22 +225,24 @@ class HttpCase(TransactionCase): # process lines if '\n' in buf: line, buf = buf.split('\n', 1) - line = str(line) + + # relay everything from console.log, even 'ok' or 'error...' lines + _logger.info("phantomjs: %s", line) + if line == "ok": break if line.startswith("error"): line_ = line[6:] - try: line_ = json.loads(line_) - except ValueError: pass + # when error occurs the execution stack may be sent as as JSON + try: + line_ = json.loads(line_) + except ValueError: + pass self.fail(line_ or "phantomjs test failed") - try: line = json.loads(line) - except ValueError: pass - _logger.info("phantomjs: %s", line) - def phantom_run(self, cmd, timeout): - _logger.debug('executing `%s`', ' '.join(cmd)) + _logger.info('phantom_run executing %s', ' '.join(cmd)) try: phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) except OSError: @@ -240,6 +253,7 @@ class HttpCase(TransactionCase): # kill phantomjs if phantom.exit() wasn't called in the test if phantom.poll() is None: phantom.terminate() + _logger.info("phantom_run execution finished") def phantom_jsfile(self, jsfile, timeout=30, **kw): options = { diff --git a/openerp/tests/phantomtest.js b/openerp/tests/phantomtest.js index 1cdfeabce76..63a3b2e33ff 100644 --- a/openerp/tests/phantomtest.js +++ b/openerp/tests/phantomtest.js @@ -2,22 +2,17 @@ function waitFor (ready, callback, timeout, timeoutMessageCallback) { timeout = timeout || 10000; - var start = new Date().getTime(); - var condition = ready(); - var interval = setInterval(function() { - if ((new Date().getTime() - start < timeout) && !condition ) { - condition = ready(); + var start = new Date; + + (function waitLoop() { + if(new Date - start > timeout) { + error(timeoutMessageCallback ? timeoutMessageCallback() : "Timeout after "+timeout+" ms"); + } else if (ready()) { + callback(); } else { - if(!condition) { - var message = timeoutMessageCallback ? timeoutMessageCallback() : "Timeout after "+timeout+" ms"; - console.log("Waiting for " + ready); - error(message); - } else { - clearInterval(interval); - callback(); - } + setTimeout(waitLoop, 250); } - }, 250); + }()); } function error(message) { @@ -103,12 +98,12 @@ function PhantomTest() { // ---------------------------------------------------- this.run = function(url_path, code, ready) { if(self.options.login) { - qp = []; + var qp = []; qp.push('db=' + self.options.db); qp.push('login=' + self.options.login); qp.push('key=' + self.options.password); qp.push('redirect=' + encodeURIComponent(url_path)); - var url_path = "/login?" + qp.join('&'); + url_path = "/login?" + qp.join('&'); } var url = self.origin + url_path; self.page.open(url, function(status) { @@ -118,18 +113,22 @@ function PhantomTest() { console.log('loaded', url, status); // process ready waitFor(function() { - return self.page.evaluate(function (ready) { + console.log("PhantomTest.run: wait for condition: " + ready); + return self.page.evaluate(function (ready) { var r = false; try { + console.log("page.evaluate eval expr:", ready); r = !!eval(ready); - } catch(ex) { - console.log("waiting for " + ready); + } catch(ex) { } + console.log("page.evaluate eval result:", r); return r; }, ready); // run test }, function() { + console.log("PhantomTest.run: condition statified, executing: " + code); self.page.evaluate(function (code) { return eval(code); }, code); + console.log("PhantomTest.run: execution launched, waiting for console.log('ok')..."); }); } }); diff --git a/openerp/tools/__init__.py b/openerp/tools/__init__.py index 18dab088440..c927b2fc87a 100644 --- a/openerp/tools/__init__.py +++ b/openerp/tools/__init__.py @@ -21,6 +21,7 @@ import copy import win32 +import appdirs from config import config from misc import * from convert import * @@ -34,7 +35,7 @@ from yaml_import import * from sql import * from float_utils import * from mail import * - +from debugger import * # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tools/appdirs.py b/openerp/tools/appdirs.py new file mode 100644 index 00000000000..1d24dd8586a --- /dev/null +++ b/openerp/tools/appdirs.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2010 ActiveState Software Inc. +# Copyright (c) 2013 Eddy Petrișor + +"""Utilities for determining application-specific dirs. + +See for details and usage. +""" +# Dev Notes: +# - MSDN on where to store app data files: +# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120 +# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html +# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + +__version_info__ = (1, 3, 0) +__version__ = '.'.join(map(str, __version_info__)) + + +import sys +import os + +PY3 = sys.version_info[0] == 3 + +if PY3: + unicode = str + + + +def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user data directories are: + Mac OS X: ~/Library/Application Support/ + Unix: ~/.local/share/ # or in $XDG_DATA_HOME, if defined + Win XP (not roaming): C:\Documents and Settings\\Application Data\\ + Win XP (roaming): C:\Documents and Settings\\Local Settings\Application Data\\ + Win 7 (not roaming): C:\Users\\AppData\Local\\ + Win 7 (roaming): C:\Users\\AppData\Roaming\\ + + For Unix, we follow the XDG spec and support $XDG_DATA_HOME. + That means, by deafult "~/.local/share/". + """ + if sys.platform == "win32": + if appauthor is None: + appauthor = appname + const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA" + path = os.path.normpath(_get_win_folder(const)) + if appname: + path = os.path.join(path, appauthor, appname) + elif sys.platform == 'darwin': + path = os.path.expanduser('~/Library/Application Support/') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): + """Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of data dirs should be + returned. By default, the first item from XDG_DATA_DIRS is + returned, or '/usr/local/share/', + if XDG_DATA_DIRS is not set + + Typical user data directories are: + Mac OS X: /Library/Application Support/ + Unix: /usr/local/share/ or /usr/share/ + Win XP: C:\Documents and Settings\All Users\Application Data\\ + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + Win 7: C:\ProgramData\\ # Hidden, but writeable on Win 7. + + For Unix, this is using the $XDG_DATA_DIRS[0] default. + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if sys.platform == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA")) + if appname: + path = os.path.join(path, appauthor, appname) + elif sys.platform == 'darwin': + path = os.path.expanduser('/Library/Application Support') + if appname: + path = os.path.join(path, appname) + else: + # XDG default for $XDG_DATA_DIRS + # only first, if multipath is False + path = os.getenv('XDG_DATA_DIRS', + os.pathsep.join(['/usr/local/share', '/usr/share'])) + pathlist = [ os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) ] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [ os.sep.join([x, appname]) for x in pathlist ] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + if appname and version: + path = os.path.join(path, version) + return path + + +def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific config dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user data directories are: + Mac OS X: same as user_data_dir + Unix: ~/.config/ # or in $XDG_CONFIG_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow the XDG spec and support $XDG_DATA_HOME. + That means, by deafult "~/.local/share/". + """ + if sys.platform in [ "win32", "darwin" ]: + path = user_data_dir(appname, appauthor, None, roaming) + else: + path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): + """Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of config dirs should be + returned. By default, the first item from XDG_CONFIG_DIRS is + returned, or '/etc/xdg/', if XDG_CONFIG_DIRS is not set + + Typical user data directories are: + Mac OS X: same as site_data_dir + Unix: /etc/xdg/ or $XDG_CONFIG_DIRS[i]/ for each value in + $XDG_CONFIG_DIRS + Win *: same as site_data_dir + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + + For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if sys.platform in [ "win32", "darwin" ]: + path = site_data_dir(appname, appauthor) + if appname and version: + path = os.path.join(path, version) + else: + # XDG default for $XDG_CONFIG_DIRS + # only first, if multipath is False + path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') + pathlist = [ os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) ] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [ os.sep.join([x, appname]) for x in pathlist ] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + +def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific cache dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Cache" to the base app data dir for Windows. See + discussion below. + + Typical user cache directories are: + Mac OS X: ~/Library/Caches/ + Unix: ~/.cache/ (XDG default) + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Cache + Vista: C:\Users\\AppData\Local\\\Cache + + On Windows the only suggestion in the MSDN docs is that local settings go in + the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming + app data dir (the default returned by `user_data_dir` above). Apps typically + put cache data somewhere *under* the given dir here. Some examples: + ...\Mozilla\Firefox\Profiles\\Cache + ...\Acme\SuperApp\Cache\1.0 + OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value. + This can be disabled with the `opinion=False` option. + """ + if sys.platform == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) + if appname: + path = os.path.join(path, appauthor, appname) + if opinion: + path = os.path.join(path, "Cache") + elif sys.platform == 'darwin': + path = os.path.expanduser('~/Library/Caches') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + +def user_log_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific log dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Logs" to the base app data dir for Windows, and "log" to the + base cache dir for Unix. See discussion below. + + Typical user cache directories are: + Mac OS X: ~/Library/Logs/ + Unix: ~/.cache//log # or under $XDG_CACHE_HOME if defined + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Logs + Vista: C:\Users\\AppData\Local\\\Logs + + On Windows the only suggestion in the MSDN docs is that local settings + go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in + examples of what some windows apps use for a logs dir.) + + OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA` + value for Windows and appends "log" to the user cache dir for Unix. + This can be disabled with the `opinion=False` option. + """ + if sys.platform == "darwin": + path = os.path.join( + os.path.expanduser('~/Library/Logs'), + appname) + elif sys.platform == "win32": + path = user_data_dir(appname, appauthor, version); version=False + if opinion: + path = os.path.join(path, "Logs") + else: + path = user_cache_dir(appname, appauthor, version); version=False + if opinion: + path = os.path.join(path, "log") + if appname and version: + path = os.path.join(path, version) + return path + + +class AppDirs(object): + """Convenience wrapper for getting application dirs.""" + def __init__(self, appname, appauthor=None, version=None, + roaming=False, multipath=False): + self.appname = appname + self.appauthor = appauthor + self.version = version + self.roaming = roaming + self.multipath = multipath + @property + def user_data_dir(self): + return user_data_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + @property + def site_data_dir(self): + return site_data_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + @property + def user_config_dir(self): + return user_config_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + @property + def site_config_dir(self): + return site_data_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + @property + def user_cache_dir(self): + return user_cache_dir(self.appname, self.appauthor, + version=self.version) + @property + def user_log_dir(self): + return user_log_dir(self.appname, self.appauthor, + version=self.version) + + + + +#---- internal support stuff + +def _get_win_folder_from_registry(csidl_name): + """This is a fallback technique at best. I'm not sure if using the + registry for this guarantees us the correct answer for all CSIDL_* + names. + """ + import _winreg + + shell_folder_name = { + "CSIDL_APPDATA": "AppData", + "CSIDL_COMMON_APPDATA": "Common AppData", + "CSIDL_LOCAL_APPDATA": "Local AppData", + }[csidl_name] + + key = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") + dir, type = _winreg.QueryValueEx(key, shell_folder_name) + return dir + +def _get_win_folder_with_pywin32(csidl_name): + from win32com.shell import shellcon, shell + dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0) + # Try to make this a unicode path because SHGetFolderPath does + # not return unicode strings when there is unicode data in the + # path. + try: + dir = unicode(dir) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in dir: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + try: + import win32api + dir = win32api.GetShortPathName(dir) + except ImportError: + pass + except UnicodeError: + pass + return dir + +def _get_win_folder_with_ctypes(csidl_name): + import ctypes + + csidl_const = { + "CSIDL_APPDATA": 26, + "CSIDL_COMMON_APPDATA": 35, + "CSIDL_LOCAL_APPDATA": 28, + }[csidl_name] + + buf = ctypes.create_unicode_buffer(1024) + ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in buf: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf2 = ctypes.create_unicode_buffer(1024) + if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): + buf = buf2 + + return buf.value + +if sys.platform == "win32": + try: + import win32com.shell + _get_win_folder = _get_win_folder_with_pywin32 + except ImportError: + try: + import ctypes + _get_win_folder = _get_win_folder_with_ctypes + except ImportError: + _get_win_folder = _get_win_folder_from_registry + + + +#---- self test code + +if __name__ == "__main__": + appname = "MyApp" + appauthor = "MyCompany" + + props = ("user_data_dir", "site_data_dir", + "user_config_dir", "site_config_dir", + "user_cache_dir", "user_log_dir") + + print("-- app dirs (with optional 'version')") + dirs = AppDirs(appname, appauthor, version="1.0") + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'version')") + dirs = AppDirs(appname, appauthor) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'appauthor')") + dirs = AppDirs(appname) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 07435075f8d..8313f4d786e 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -1,9 +1,9 @@ -# -*- coding: utf-8 -*- +#openerp.loggers.handlers. -*- coding: utf-8 -*- ############################################################################## # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). -# Copyright (C) 2010-2012 OpenERP s.a. (). +# Copyright (C) 2010-2014 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -29,6 +29,7 @@ import openerp.conf import openerp.loglevels as loglevels import logging import openerp.release as release +import appdirs class MyOption (optparse.Option, object): """ optparse Option with two additional attributes. @@ -59,6 +60,9 @@ def check_ssl(): DEFAULT_LOG_HANDLER = [':INFO'] +def _get_default_datadir(): + return appdirs.user_data_dir(appname='OpenERP', appauthor=release.author) + class configmanager(object): def __init__(self, fname=None): # Options not exposed on the command line. Command line options will be added @@ -72,9 +76,10 @@ class configmanager(object): } # Not exposed in the configuration file. - self.blacklist_for_save = set( - ['publisher_warranty_url', 'load_language', 'root_path', - 'init', 'save', 'config', 'update', 'stop_after_init']) + self.blacklist_for_save = set([ + 'publisher_warranty_url', 'load_language', 'root_path', + 'init', 'save', 'config', 'update', 'stop_after_init' + ]) # dictionary mapping option destination (keys in self.options) to MyOptions. self.casts = {} @@ -83,7 +88,10 @@ class configmanager(object): self.config_file = fname self.has_ssl = check_ssl() - self._LOGLEVELS = dict([(getattr(loglevels, 'LOG_%s' % x), getattr(logging, x)) for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET')]) + self._LOGLEVELS = dict([ + (getattr(loglevels, 'LOG_%s' % x), getattr(logging, x)) + for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET') + ]) version = "%s %s" % (release.description, release.version) self.parser = parser = optparse.OptionParser(version=version, option_class=MyOption) @@ -106,6 +114,9 @@ class configmanager(object): help="specify additional addons paths (separated by commas).", action="callback", callback=self._check_addons_path, nargs=1, type="string") group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules default=web") + + group.add_option("-D", "--data-dir", dest="data_dir", my_default=_get_default_datadir(), + help="Directory where to store OpenERP data") parser.add_option_group(group) # XML-RPC / HTTP @@ -172,16 +183,20 @@ class configmanager(object): group.add_option("--logrotate", dest="logrotate", action="store_true", my_default=False, help="enable logfile rotation") group.add_option("--syslog", action="store_true", dest="syslog", my_default=False, help="Send the log to the syslog server") group.add_option('--log-handler', action="append", default=DEFAULT_LOG_HANDLER, my_default=DEFAULT_LOG_HANDLER, metavar="PREFIX:LEVEL", help='setup a handler at LEVEL for a given PREFIX. An empty PREFIX indicates the root logger. This option can be repeated. Example: "openerp.orm:DEBUG" or "werkzeug:CRITICAL" (default: ":INFO")') - group.add_option('--log-request', action="append_const", dest="log_handler", const="openerp.netsvc.rpc.request:DEBUG", help='shortcut for --log-handler=openerp.netsvc.rpc.request:DEBUG') - group.add_option('--log-response', action="append_const", dest="log_handler", const="openerp.netsvc.rpc.response:DEBUG", help='shortcut for --log-handler=openerp.netsvc.rpc.response:DEBUG') - group.add_option('--log-web', action="append_const", dest="log_handler", const="openerp.addons.web.http:DEBUG", help='shortcut for --log-handler=openerp.addons.web.http:DEBUG') + group.add_option('--log-request', action="append_const", dest="log_handler", const="openerp.http.rpc.request:DEBUG", help='shortcut for --log-handler=openerp.http.rpc.request:DEBUG') + group.add_option('--log-response', action="append_const", dest="log_handler", const="openerp.http.rpc.response:DEBUG", help='shortcut for --log-handler=openerp.http.rpc.response:DEBUG') + group.add_option('--log-web', action="append_const", dest="log_handler", const="openerp.http:DEBUG", help='shortcut for --log-handler=openerp.http:DEBUG') group.add_option('--log-sql', action="append_const", dest="log_handler", const="openerp.sql_db:DEBUG", help='shortcut for --log-handler=openerp.sql_db:DEBUG') + group.add_option('--log-db', dest='log_db', help="Logging database", my_default=False) # For backward-compatibility, map the old log levels to something # quite close. - levels = ['info', 'debug_rpc', 'warn', 'test', 'critical', - 'debug_sql', 'error', 'debug', 'debug_rpc_answer', 'notset'] - group.add_option('--log-level', dest='log_level', type='choice', choices=levels, - my_default='info', help='specify the level of the logging. Accepted values: ' + str(levels) + ' (deprecated option).') + levels = [ + 'info', 'debug_rpc', 'warn', 'test', 'critical', + 'debug_sql', 'error', 'debug', 'debug_rpc_answer', 'notset' + ] + group.add_option('--log-level', dest='log_level', type='choice', + choices=levels, my_default='info', + help='specify the level of the logging. Accepted values: %s (deprecated option).' % (levels,)) parser.add_option_group(group) @@ -245,7 +260,8 @@ class configmanager(object): # Advanced options group = optparse.OptionGroup(parser, "Advanced options") - group.add_option('--auto-reload', dest='auto_reload', action='store_true', my_default=False, help='enable auto reload') + if os.name == 'posix': + group.add_option('--auto-reload', dest='auto_reload', action='store_true', my_default=False, help='enable auto reload') group.add_option('--debug', dest='debug_mode', action='store_true', my_default=False, help='enable debug mode') group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False, help="stop the server after its initialization") @@ -267,27 +283,28 @@ class configmanager(object): help="Use the unaccent function provided by the database when available.") parser.add_option_group(group) - group = optparse.OptionGroup(parser, "Multiprocessing options") - # TODO sensible default for the three following limits. - group.add_option("--workers", dest="workers", my_default=0, - help="Specify the number of workers, 0 disable prefork mode.", - type="int") - group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=2048 * 1024 * 1024, - help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request (default 671088640 aka 640MB).", - type="int") - group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default=2560 * 1024 * 1024, - help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail (default 805306368 aka 768MB).", - type="int") - group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60, - help="Maximum allowed CPU time per request (default 60).", - type="int") - group.add_option("--limit-time-real", dest="limit_time_real", my_default=120, - help="Maximum allowed Real time per request (default 120).", - type="int") - group.add_option("--limit-request", dest="limit_request", my_default=8192, - help="Maximum number of request to be processed per worker (default 8192).", - type="int") - parser.add_option_group(group) + if os.name == 'posix': + group = optparse.OptionGroup(parser, "Multiprocessing options") + # TODO sensible default for the three following limits. + group.add_option("--workers", dest="workers", my_default=0, + help="Specify the number of workers, 0 disable prefork mode.", + type="int") + group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=2048 * 1024 * 1024, + help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request (default 671088640 aka 640MB).", + type="int") + group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default=2560 * 1024 * 1024, + help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail (default 805306368 aka 768MB).", + type="int") + group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60, + help="Maximum allowed CPU time per request (default 60).", + type="int") + group.add_option("--limit-time-real", dest="limit_time_real", my_default=120, + help="Maximum allowed Real time per request (default 120).", + type="int") + group.add_option("--limit-request", dest="limit_request", my_default=8192, + help="Maximum number of request to be processed per worker (default 8192).", + type="int") + parser.add_option_group(group) # Copy all optparse options (i.e. MyOption) into self.options. for group in parser.option_groups: @@ -348,6 +365,7 @@ class configmanager(object): # (../etc from the server) # if the server is run by an unprivileged user, he has to specify location of a config file where he has the rights to write, # else he won't be able to save the configurations, or even to start the server... + # TODO use appdirs if os.name == 'nt': rcfilepath = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'openerp-server.conf') else: @@ -358,7 +376,6 @@ class configmanager(object): or os.environ.get('OPENERP_SERVER') or rcfilepath) self.load() - # Verify that we want to log or not, if not the output will go to stdout if self.options['logfile'] in ('None', 'False'): self.options['logfile'] = False @@ -375,7 +392,7 @@ class configmanager(object): 'xmlrpc', 'syslog', 'without_demo', 'timezone', 'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs', 'static_http_enable', 'static_http_document_root', 'static_http_url_prefix', - 'secure_cert_file', 'secure_pkey_file', 'dbfilter', 'log_handler', 'log_level' + 'secure_cert_file', 'secure_pkey_file', 'dbfilter', 'log_handler', 'log_level', 'log_db' ] for arg in keys: @@ -387,7 +404,6 @@ class configmanager(object): elif isinstance(self.options[arg], basestring) and self.casts[arg].type in optparse.Option.TYPE_CHECKER: self.options[arg] = optparse.Option.TYPE_CHECKER[self.casts[arg].type](self.casts[arg], arg, self.options[arg]) - if isinstance(self.options['log_handler'], basestring): self.options['log_handler'] = self.options['log_handler'].split(',') @@ -399,11 +415,22 @@ class configmanager(object): 'list_db', 'xmlrpcs', 'proxy_mode', 'test_file', 'test_enable', 'test_commit', 'test_report_directory', 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', - 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request', 'auto_reload' + 'data_dir', ] + posix_keys = [ + 'auto_reload', 'workers', + 'limit_memory_hard', 'limit_memory_soft', + 'limit_time_cpu', 'limit_time_real', 'limit_request', + ] + + if os.name == 'posix': + keys += posix_keys + else: + self.options.update(dict.fromkeys(posix_keys, None)) + + # Copy the command-line arguments... for arg in keys: - # Copy the command-line argument... if getattr(opt, arg) is not None: self.options[arg] = getattr(opt, arg) # ... or keep, but cast, the config file value. @@ -617,6 +644,24 @@ class configmanager(object): def __getitem__(self, key): return self.options[key] + @property + def addons_data_dir(self): + d = os.path.join(self['data_dir'], 'addons', release.series) + if not os.path.exists(d): + os.makedirs(d, 0700) + else: + os.chmod(d, 0700) + return d + + @property + def session_dir(self): + d = os.path.join(self['data_dir'], 'sessions', release.series) + if not os.path.exists(d): + os.makedirs(d, 0700) + else: + os.chmod(d, 0700) + return d + config = configmanager() diff --git a/openerp/tools/debugger.py b/openerp/tools/debugger.py new file mode 100644 index 00000000000..293d4052513 --- /dev/null +++ b/openerp/tools/debugger.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright: 2014 - OpenERP S.A. +import types + +def post_mortem(config, info): + if config['debug_mode'] and isinstance(info[2], types.TracebackType): + import pdb + pdb.post_mortem(info[2]) diff --git a/openerp/tools/image.py b/openerp/tools/image.py index 196ace2da60..08c2fd91f21 100644 --- a/openerp/tools/image.py +++ b/openerp/tools/image.py @@ -32,7 +32,7 @@ from random import randint # Image resizing # ---------------------------------------- -def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype='PNG', avoid_if_small=False): +def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype=None, avoid_if_small=False): """ Function to resize an image. The image will be resized to the given size, while keeping the aspect ratios, and holes in the image will be filled with transparent background. The image will not be stretched if @@ -58,7 +58,8 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file height mean an automatically computed value based respectivelly on height or width of the source image. :param encoding: the output encoding - :param filetype: the output filetype + :param filetype: the output filetype, by default the source image's + :type filetype: str, any PIL image format (supported for creation) :param avoid_if_small: do not resize if image height and width are smaller than the expected size. """ @@ -68,6 +69,8 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file return base64_source image_stream = StringIO.StringIO(base64_source.decode(encoding)) image = Image.open(image_stream) + # store filetype here, as Image.new below will lose image.format + filetype = filetype or image.format asked_width, asked_height = size if asked_width is None: @@ -95,21 +98,21 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file image.save(background_stream, filetype) return background_stream.getvalue().encode(encoding) -def image_resize_image_big(base64_source, size=(1204, 1204), encoding='base64', filetype='PNG', avoid_if_small=True): +def image_resize_image_big(base64_source, size=(1204, 1024), encoding='base64', filetype=None, avoid_if_small=True): """ Wrapper on image_resize_image, to resize images larger than the standard 'big' image size: 1024x1024px. :param size, encoding, filetype, avoid_if_small: refer to image_resize_image """ return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small) -def image_resize_image_medium(base64_source, size=(128, 128), encoding='base64', filetype='PNG', avoid_if_small=False): +def image_resize_image_medium(base64_source, size=(128, 128), encoding='base64', filetype=None, avoid_if_small=False): """ Wrapper on image_resize_image, to resize to the standard 'medium' image size: 180x180. :param size, encoding, filetype, avoid_if_small: refer to image_resize_image """ return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small) -def image_resize_image_small(base64_source, size=(64, 64), encoding='base64', filetype='PNG', avoid_if_small=False): +def image_resize_image_small(base64_source, size=(64, 64), encoding='base64', filetype=None, avoid_if_small=False): """ Wrapper on image_resize_image, to resize to the standard 'small' image size: 50x50. :param size, encoding, filetype, avoid_if_small: refer to image_resize_image diff --git a/openerp/tools/osutil.py b/openerp/tools/osutil.py index b5cff6b2646..94684d34673 100644 --- a/openerp/tools/osutil.py +++ b/openerp/tools/osutil.py @@ -23,8 +23,12 @@ Some functions related to the os and os.path module """ +from contextlib import contextmanager import os from os.path import join as opj +import shutil +import tempfile +import zipfile if os.name == 'nt': import ctypes @@ -61,6 +65,30 @@ def walksymlinks(top, topdown=True, onerror=None): if not topdown: yield dirpath, dirnames, filenames +@contextmanager +def tempdir(): + tmpdir = tempfile.mkdtemp() + try: + yield tmpdir + finally: + shutil.rmtree(tmpdir) + +def zip_dir(path, stream, include_dir=True): # TODO add ignore list + path = os.path.normpath(path) + len_prefix = len(os.path.dirname(path)) if include_dir else len(path) + if len_prefix: + len_prefix += 1 + + with zipfile.ZipFile(stream, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True) as zipf: + for dirpath, dirnames, filenames in os.walk(path): + for fname in filenames: + bname, ext = os.path.splitext(fname) + ext = ext or bname + if ext not in ['.pyc', '.pyo', '.swp', '.DS_Store']: + path = os.path.normpath(os.path.join(dirpath, fname)) + if os.path.isfile(path): + zipf.write(path, path[len_prefix:]) + if os.name != 'nt': getppid = os.getppid diff --git a/openerp/tools/safe_eval.py b/openerp/tools/safe_eval.py index b64b401a71c..508a072a3b2 100644 --- a/openerp/tools/safe_eval.py +++ b/openerp/tools/safe_eval.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# Copyright (C) 2004-2012 OpenERP s.a. (). +# Copyright (C) 2004-2014 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -225,9 +225,14 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal 'False': False, 'None': None, 'str': str, + 'unicode': unicode, 'globals': locals, 'locals': locals, 'bool': bool, + 'int': int, + 'float': float, + 'long': long, + 'enumerate': enumerate, 'dict': dict, 'list': list, 'tuple': tuple, @@ -235,15 +240,23 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal 'abs': abs, 'min': min, 'max': max, + 'sum': sum, 'reduce': reduce, 'filter': filter, 'round': round, 'len': len, - 'set': set, 'repr': repr, - 'int': int, - 'float': float, + 'set': set, + 'all': all, + 'any': any, + 'ord': ord, + 'chr': chr, + 'cmp': cmp, + 'divmod': divmod, + 'isinstance': isinstance, 'range': range, + 'xrange': xrange, + 'zip': zip, } ) if locals_builtins: diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 58eefc92df4..b7954054604 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -458,7 +458,7 @@ def trans_export(lang, modules, buffer, format, cr): row.setdefault('tnrs', []).append((type, name, res_id)) row.setdefault('comments', set()).update(comments) - for src, row in grouped_rows.items(): + for src, row in sorted(grouped_rows.items()): if not lang: # translation template, so no translation value row['translation'] = '' @@ -778,49 +778,32 @@ def trans_generate(lang, modules, cr): if model_obj._sql_constraints: push_local_constraints(module, model_obj, 'sql_constraints') - def get_module_from_path(path, mod_paths=None): - if not mod_paths: - # First, construct a list of possible paths - def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base) - ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(',')) - mod_paths=[def_path] - for adp in ad_paths: - mod_paths.append(adp) - if not os.path.isabs(adp): - mod_paths.append(adp) - elif adp.startswith(def_path): - mod_paths.append(adp[len(def_path)+1:]) - for mp in mod_paths: - if path.startswith(mp) and (os.path.dirname(path) != mp): - path = path[len(mp)+1:] - return path.split(os.path.sep)[0] - return 'base' # files that are not in a module are considered as being in 'base' module modobj = registry['ir.module.module'] installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')]) installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name'])) - root_path = os.path.join(config.config['root_path'], 'addons') - - apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(','))) - if root_path in apaths: - path_list = apaths - else : - path_list = [root_path,] + apaths - + path_list = list(openerp.modules.module.ad_paths) # Also scan these non-addon paths for bin_path in ['osv', 'report' ]: path_list.append(os.path.join(config.config['root_path'], bin_path)) _logger.debug("Scanning modules at paths: ", path_list) - mod_paths = [] + mod_paths = list(path_list) + + def get_module_from_path(path): + for mp in mod_paths: + if path.startswith(mp) and (os.path.dirname(path) != mp): + path = path[len(mp)+1:] + return path.split(os.path.sep)[0] + return 'base' # files that are not in a module are considered as being in 'base' module def verified_module_filepaths(fname, path, root): fabsolutepath = join(root, fname) frelativepath = fabsolutepath[len(path):] display_path = "addons%s" % frelativepath - module = get_module_from_path(fabsolutepath, mod_paths=mod_paths) + module = get_module_from_path(fabsolutepath) if ('all' in modules or module in modules) and module in installed_modules: return module, fabsolutepath, frelativepath, display_path return None, None, None, None diff --git a/setup.py b/setup.py index 05178fb12ac..a44e26fd36f 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ def py2exe_options(): "skip_archive": 1, "optimize": 0, # keep the assert running, because the integrated tests rely on them. "dist_dir": 'dist', - "packages": [ "DAV", "HTMLParser", "PIL", "asynchat", "asyncore", "commands", "dateutil", "decimal", "docutils", "email", "encodings", "imaplib", "jinja2", "lxml", "lxml._elementpath", "lxml.builder", "lxml.etree", "lxml.objectify", "mako", "openerp", "poplib", "pychart", "pydot", "pyparsing", "pytz", "reportlab", "select", "simplejson", "smtplib", "uuid", "vatnumber", "vobject", "xml", "xml.dom", "yaml", ], + "packages": ["HTMLParser", "PIL", "asynchat", "asyncore", "commands", "dateutil", "decimal", "docutils", "email", "encodings", "imaplib", "jinja2", "lxml", "lxml._elementpath", "lxml.builder", "lxml.etree", "lxml.objectify", "mako", "openerp", "poplib", "pychart", "pydot", "pyparsing", "pytz", "reportlab", "select", "simplejson", "smtplib", "uuid", "vatnumber", "vobject", "xml", "xml.dom", "yaml", ], "excludes" : ["Tkconstants","Tkinter","tcl"], } }