diff --git a/debian/control b/debian/control
index 74cd027427a..7f7786ee98d 100644
--- a/debian/control
+++ b/debian/control
@@ -16,6 +16,7 @@ Depends:
python,
postgresql-client,
python-dateutil,
+ python-decorator,
python-docutils,
python-feedparser,
python-gdata,
@@ -34,6 +35,7 @@ Depends:
python-pydot,
python-pyparsing,
python-reportlab,
+ python-requests,
python-simplejson,
python-tz,
python-unittest2,
diff --git a/oe b/oe
index 59827731409..2da32cbe31d 100755
--- a/oe
+++ b/oe
@@ -1,5 +1,8 @@
#! /usr/bin/env python2
if __name__ == '__main__':
+ import sys
+ if sys.argv[1] == 'run-tests':
+ sys.exit(0)
import openerpcommand.main
openerpcommand.main.run()
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..3d0d3e4dda4 100644
--- a/openerp/addons/base/__openerp__.py
+++ b/openerp/addons/base/__openerp__.py
@@ -57,6 +57,8 @@ 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',
+ 'ir/ir_qweb.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 af76114eaa5..b607b97927a 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-04 05:36+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:37+0000\n"
+"X-Generator: Launchpad (build 16985)\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 95d7c4cddb3..18368ca9625 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-04 05:36+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:38+0000\n"
+"X-Generator: Launchpad (build 16985)\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 2e1580840e1..e01800e903e 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-04 05:37+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:38+0000\n"
+"X-Generator: Launchpad (build 16985)\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 72cc888612d..94d519ede9f 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-04 05:37+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:38+0000\n"
+"X-Generator: Launchpad (build 16985)\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 a1343e9b5b2..22143793621 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-04 05:38+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:39+0000\n"
+"X-Generator: Launchpad (build 16985)\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 4cae60c0fd9..0b90f4004aa 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-04 05:37+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:39+0000\n"
+"X-Generator: Launchpad (build 16985)\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 32692af2985..babf20c960f 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-04 05:38+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:39+0000\n"
+"X-Generator: Launchpad (build 16985)\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 5e11069300e..ffc9fde2411 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-04 05:38+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:39+0000\n"
+"X-Generator: Launchpad (build 16985)\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 cc5f1bf1586..a887afb6485 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-04 05:38+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:40+0000\n"
+"X-Generator: Launchpad (build 16985)\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 980899edd9f..4de0dfda4aa 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-04 05:39+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:40+0000\n"
+"X-Generator: Launchpad (build 16985)\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 346c5f7439e..77d38b8c065 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-04 05:40+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:42+0000\n"
+"X-Generator: Launchpad (build 16985)\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 0b1c0c4a5d7..83bc68b4931 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-04 05:40+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:42+0000\n"
+"X-Generator: Launchpad (build 16985)\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 5aaf09880de..d51e067b3af 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-04 05:48+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:52+0000\n"
+"X-Generator: Launchpad (build 16985)\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 5290f4cb519..4df65aba682 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-04 05:46+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:49+0000\n"
+"X-Generator: Launchpad (build 16985)\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 a6047c27dc2..a74699fb374 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-04 05:48+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:52+0000\n"
+"X-Generator: Launchpad (build 16985)\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 ff376f2a0ae..9d4ae622b5c 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-04 05:49+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:53+0000\n"
+"X-Generator: Launchpad (build 16985)\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 e85936de771..a113e36a82e 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-04 05:48+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:52+0000\n"
+"X-Generator: Launchpad (build 16985)\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 969bbe3e25c..e5a617216b6 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-04 05:49+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:53+0000\n"
+"X-Generator: Launchpad (build 16985)\n"
"Language: \n"
#. module: base
diff --git a/openerp/addons/base/i18n/es_DO.po b/openerp/addons/base/i18n/es_DO.po
index 5e22c757364..140f6cfa803 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-04 05:48+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:52+0000\n"
+"X-Generator: Launchpad (build 16985)\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 0035789b6da..5804c8e377d 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-04 05:50+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:54+0000\n"
+"X-Generator: Launchpad (build 16985)\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 6268a950bbc..cc4ea1ea67a 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-04 05:49+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:54+0000\n"
+"X-Generator: Launchpad (build 16985)\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 f992ba35e92..46733d5b1c6 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-04 05:50+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:54+0000\n"
+"X-Generator: Launchpad (build 16985)\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 606226222ea..039baabe46c 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-04 05:47+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:51+0000\n"
+"X-Generator: Launchpad (build 16985)\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 a5d7c6d6c95..aa32b7585ab 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-04 05:39+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:40+0000\n"
+"X-Generator: Launchpad (build 16985)\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 5fff780629c..517499d3839 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-04 05:37+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:39+0000\n"
+"X-Generator: Launchpad (build 16985)\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 b09b365c526..55137dce3ab 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-04 05:44+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:46+0000\n"
+"X-Generator: Launchpad (build 16985)\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 c973339c63e..2673b2cc498 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-04 05:50+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:55+0000\n"
+"X-Generator: Launchpad (build 16985)\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 07d98a2c908..93f646c46e7 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-04 05:39+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:41+0000\n"
+"X-Generator: Launchpad (build 16985)\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
@@ -13158,7 +13184,7 @@ msgstr ""
#. module: base
#: field:res.company,company_registry:0
msgid "Company Registry"
-msgstr "Yritysrekisteri"
+msgstr "Y-tunnus"
#. module: base
#: view:ir.actions.report.xml:0
@@ -13471,7 +13497,7 @@ msgstr "Riippuvuudet:"
#. module: base
#: field:res.company,vat:0
msgid "Tax ID"
-msgstr "Vero ID"
+msgstr "VAT-numero"
#. module: base
#: model:ir.module.module,shortdesc:base.module_account_bank_statement_extensions
@@ -13821,7 +13847,7 @@ msgstr "Marokko - Kirjanpito"
#: field:res.bank,bic:0
#: field:res.partner.bank,bank_bic:0
msgid "Bank Identifier Code"
-msgstr "BIC koodi"
+msgstr "BIC-koodi"
#. module: base
#: view:base.language.export:0
@@ -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 e77035c9574..faaa7ded49a 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-04 05:40+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:41+0000\n"
+"X-Generator: Launchpad (build 16985)\n"
#. module: base
#: model:ir.module.module,description:base.module_account_check_writing
@@ -9951,7 +9951,7 @@ msgid ""
msgstr ""
"\n"
"Le noyau d'OpenERP, nécessaire pour toute installation.\n"
-"===================================================\n"
+"=======================================================\n"
#. module: base
#: model:ir.model,name:base.model_ir_server_object_lines
diff --git a/openerp/addons/base/i18n/fr_CA.po b/openerp/addons/base/i18n/fr_CA.po
index 63fa854f839..96217a0ed29 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-04 05:48+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:53+0000\n"
+"X-Generator: Launchpad (build 16985)\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 cff4bafb75a..33e0ca7f8e0 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-04 05:40+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:42+0000\n"
+"X-Generator: Launchpad (build 16985)\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 5d6e2d50420..bb053ba30fe 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-04 05:41+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:42+0000\n"
+"X-Generator: Launchpad (build 16985)\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 2f31ae2c3c7..370dc41acf9 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-04 05:41+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:43+0000\n"
+"X-Generator: Launchpad (build 16985)\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 8c43c8f933d..0df8f74590c 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-04 05:41+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:43+0000\n"
+"X-Generator: Launchpad (build 16985)\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 4c94c2fa7df..cd0597f7d39 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-04 05:45+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:48+0000\n"
+"X-Generator: Launchpad (build 16985)\n"
"Language: hr\n"
#. module: base
diff --git a/openerp/addons/base/i18n/hu.po b/openerp/addons/base/i18n/hu.po
index 361444eb848..c97b13187a0 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-04 05:41+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:43+0000\n"
+"X-Generator: Launchpad (build 16985)\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 1d3b92c118b..e416c0e4a62 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-04 05:37+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:38+0000\n"
+"X-Generator: Launchpad (build 16985)\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 1e67e4d07da..25414f99fb5 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-04 05:42+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:43+0000\n"
+"X-Generator: Launchpad (build 16985)\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 4bcccb5530e..125515b191f 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-04 05:41+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:43+0000\n"
+"X-Generator: Launchpad (build 16985)\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 c9631c7f581..28d29b79aaf 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-04 05:42+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:44+0000\n"
+"X-Generator: Launchpad (build 16985)\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 383540b1d4f..3af2a469283 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-04 05:42+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:44+0000\n"
+"X-Generator: Launchpad (build 16985)\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 e8af7a5fc47..5dcd445e2ff 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-04 05:40+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:41+0000\n"
+"X-Generator: Launchpad (build 16985)\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 f3aeceb85af..f1fa010812b 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-04 05:42+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:44+0000\n"
+"X-Generator: Launchpad (build 16985)\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 4b6da274dc9..2404dae9661 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-04 05:42+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:44+0000\n"
+"X-Generator: Launchpad (build 16985)\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 4e080f9d7fc..25d6e31e73d 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-04 05:43+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:45+0000\n"
+"X-Generator: Launchpad (build 16985)\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 de8c9289f47..af3d0828dc7 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-04 05:43+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:45+0000\n"
+"X-Generator: Launchpad (build 16985)\n"
#. module: base
#: model:ir.module.module,description:base.module_account_check_writing
@@ -16519,3 +16519,10 @@ msgstr ""
#~ msgstr ""
#~ "Nevar izveidot dokumentu (%s) ! Pārbaudiet, vai lietotājs pieder kādai no "
#~ "šīm grupām: %s."
+
+#~ msgid ""
+#~ "\n"
+#~ " "
+#~ msgstr ""
+#~ "\n"
+#~ " "
diff --git a/openerp/addons/base/i18n/mk.po b/openerp/addons/base/i18n/mk.po
index 5ca2aa2d844..f725d265332 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-04 05:43+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:45+0000\n"
+"X-Generator: Launchpad (build 16985)\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 58838e67f53..78ce6ac5c69 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-04 05:43+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:46+0000\n"
+"X-Generator: Launchpad (build 16985)\n"
#. module: base
#: model:ir.module.module,description:base.module_account_check_writing
@@ -3094,7 +3094,7 @@ msgstr "Моделийн Тойм"
#. module: base
#: model:ir.module.module,shortdesc:base.module_product_margin
msgid "Margins by Products"
-msgstr "Захалбар Бараагаар"
+msgstr "Бохир ашиг Бараагаар"
#. module: base
#: model:ir.ui.menu,name:base.menu_invoiced
@@ -5214,7 +5214,7 @@ msgstr ""
" * Дагавар\n"
" * Дараагийн Дугаар\n"
" * Нэмэгдэх Тоо\n"
-" * Гүйцээлтийн тоо\n"
+" * Дугаарлалтын гүйцээлт\n"
" "
#. module: base
@@ -7400,7 +7400,7 @@ msgid ""
" "
msgstr ""
"\n"
-"Энэ модуль нь борлуулалтын захиалга дээр 'Тодотгол' нэмдэг.\n"
+"Энэ модуль нь борлуулалтын захиалга дээр 'Бохир ашиг' нэмдэг.\n"
"=============================================\n"
"\n"
"Нэгж үнэ, Өртөг үнийг харьцуулан бохир ашгийг харуулдаг.\n"
@@ -11017,7 +11017,7 @@ msgstr "Өөриймшүүлсэн"
#. module: base
#: model:ir.module.module,shortdesc:base.module_sale_margin
msgid "Margins in Sales Orders"
-msgstr "Борлуулалтын захиалгын зөрүү"
+msgstr "Борлуулалтын захиалгын бохир ашиг"
#. module: base
#: model:ir.module.module,shortdesc:base.module_purchase
@@ -12353,7 +12353,7 @@ msgid ""
"you need.\n"
msgstr ""
"\n"
-"Бараанд борлуулалт, худалдан авалт, тохируулалт зэрэг олон сонирхолтой "
+"Бараанд борлуулалт, худалдан авалт, бохир ашиг зэрэг олон сонирхолтой "
"үзүүлэлтийг тооцолох тайлангийн менюг нэмдэг.\n"
"============================================================================="
"================================================\n"
@@ -13178,7 +13178,7 @@ msgstr "Гүйцэтгэх дараагийн огноо"
#. module: base
#: field:ir.sequence,padding:0
msgid "Number Padding"
-msgstr "Тоон дугаарын гүйцээлт"
+msgstr "Дугаарын гүйцээлт"
#. module: base
#: help:multi_company.default,field_id:0
@@ -18268,9 +18268,6 @@ msgstr "Бүртгүүлэх"
#~ msgid "View Ordering"
#~ msgstr "Дэлгэц эрэмблэлт"
-#~ msgid "Number padding"
-#~ msgstr "Тоон дугаарын урт"
-
#~ msgid "Object Identifiers"
#~ msgstr "Обьект бүрдүүлбэр"
@@ -24131,3 +24128,6 @@ msgstr "Бүртгүүлэх"
#~ msgid "Retro-Planning on Events"
#~ msgstr "Үйл явдлын хойноос урагшаа төлөвлөлт"
+
+#~ msgid "Number padding"
+#~ msgstr "Тоон дугаарын гүйцээлт"
diff --git a/openerp/addons/base/i18n/nb.po b/openerp/addons/base/i18n/nb.po
index 803b851b55c..65d8cf62e7d 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-04 05:43+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:46+0000\n"
+"X-Generator: Launchpad (build 16985)\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 75b988f395f..39d1bddcd75 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-04 05:39+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:40+0000\n"
+"X-Generator: Launchpad (build 16985)\n"
#. module: base
#: model:ir.module.module,description:base.module_account_check_writing
@@ -1625,7 +1625,7 @@ msgstr ""
#: model:ir.module.category,name:base.module_category_purchase_management
#: model:ir.ui.menu,name:base.menu_purchase_root
msgid "Purchases"
-msgstr "Inkoop"
+msgstr "Inkopen"
#. module: base
#: model:res.country,name:base.md
@@ -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 ae1e9323694..a07fb33fb4b 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-04 05:48+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:53+0000\n"
+"X-Generator: Launchpad (build 16985)\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 6766222fb5a..ca3e805d7f1 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-04 05:44+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:47+0000\n"
+"X-Generator: Launchpad (build 16985)\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 fc92e7f4efc..0576d40462c 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-04 05:44+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:47+0000\n"
+"X-Generator: Launchpad (build 16985)\n"
#. module: base
#: model:ir.module.module,description:base.module_account_check_writing
@@ -5633,7 +5633,7 @@ msgstr "Falta o valor do campo '%s'."
#. module: base
#: view:ir.rule:0
msgid "Write Access Right"
-msgstr ""
+msgstr "Permissões de Escrita"
#. module: base
#: model:ir.actions.act_window,help:base.action_res_groups
diff --git a/openerp/addons/base/i18n/pt_BR.po b/openerp/addons/base/i18n/pt_BR.po
index 4eaf7f824dd..2b19ec2c486 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-04 05:47+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:51+0000\n"
+"X-Generator: Launchpad (build 16985)\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 9d53ec1a629..6d29c529e44 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-04 05:44+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:47+0000\n"
+"X-Generator: Launchpad (build 16985)\n"
#. module: base
#: model:ir.module.module,description:base.module_account_check_writing
@@ -431,7 +431,8 @@ msgstr ""
"\n"
"Va permite sa adaugati metode de livrare la comenzile de vanzare si "
"ridicare.\n"
-"==============================================================\n"
+"============================================================================="
+"\n"
"\n"
"Va puteti defini propriile grile de preturi pentru transport si livrare. "
"Atunci cand creati \n"
@@ -617,7 +618,7 @@ msgid ""
msgstr ""
"\n"
"Modul pentru definirea obiectului contabilitatii analitice.\n"
-"=========================================================\n"
+"===========================================================\n"
"\n"
"In OpenERP, conturile analitice sunt legate de conturile generale, dar sunt "
"tratate\n"
@@ -855,7 +856,7 @@ msgid ""
msgstr ""
"\n"
"Contabilitate si Management Financiar.\n"
-"====================================\n"
+"======================================\n"
"\n"
"Modulul Finante si contabilitate acopera:\n"
"--------------------------------------------\n"
@@ -899,7 +900,7 @@ msgid ""
msgstr ""
"\n"
"Meniul pentru Marketing.\n"
-"=======================\n"
+"========================\n"
"\n"
"Contine programul de instalare pentru modulele legate de marketing.\n"
" "
@@ -915,7 +916,7 @@ msgid ""
msgstr ""
"\n"
"Modulul OpenERP Web LinkedIn.\n"
-"============================\n"
+"=============================\n"
"Acest modul asigura Integrarea lui LinkedIn in OpenERP.\n"
" "
@@ -1072,7 +1073,7 @@ msgid ""
msgstr ""
"\n"
"Gestioneaza concediile si cererile de alocare\n"
-"=====================================\n"
+"=============================================\n"
"\n"
"Aceasta aplicatie controleaza programul de concedii al companiei "
"dumneavoastra. Le permite angajatilor sa solicite concedii. Astfel, "
@@ -1331,7 +1332,7 @@ msgid ""
msgstr ""
"\n"
"Managementul general OpenERP al Relatiilor cu Clientii\n"
-" ====================================================\n"
+"======================================================\n"
" \n"
"Aceasta aplicatie permite unui grup de oameni sa gestioneze in mod "
"inteligent si eficient piste, oportunitati, intalniri si apeluri "
@@ -1356,7 +1357,7 @@ msgstr ""
" \n"
"\n"
"Tabloul de bord pentru MRC va include:\n"
-" -------------------------------\n"
+"--------------------------------------\n"
" * Veniturile Planificate dupa Etapa si Utilizator (grafic)\n"
" * Oportunitati dupa Etapa (grafic)\n"
@@ -2011,7 +2012,7 @@ msgid ""
msgstr ""
"\n"
"Modulul de baza pentru gestionarea pranzului.\n"
-"================================\n"
+"=============================================\n"
"\n"
"Multe companii comanda sandwich-uri, pizza si alte mancaruri de la "
"furnizorii obisnuiti, pentru angajatii lor pentru a le oferi mai multe "
@@ -2250,7 +2251,7 @@ msgid ""
msgstr ""
"\n"
"Drepturi de acces contabile\n"
-"========================\n"
+"===========================\n"
"Ii ofera utilizatorului Administrator acces la toate caracteristicile "
"contabile, precum elemente ale jurnalului si planul de conturi.\n"
"\n"
@@ -2444,7 +2445,8 @@ msgstr ""
"\n"
"Sincronizarea inregistrarilor sarcinilor proiectului cu inregistrarile fisei "
"de pontaj.\n"
-"====================================================================\n"
+"============================================================================="
+"==========\n"
"\n"
"Acest modul va permite sa transferati inregistrarile sub sarcini definite "
"pentru Managementul\n"
@@ -2778,7 +2780,7 @@ msgid ""
msgstr ""
"\n"
"Inregistreaza si valideaza fisele de pontaj si prezenta cu usurinta\n"
-"=====================================================\n"
+"===================================================================\n"
"\n"
"Aceasta aplicatie ofera un ecran nou care va permite sa gestionati atat "
"prezenta (La intrare/La iesire) cat si inregistrare muncii (fisa de pontaj) "
@@ -2874,7 +2876,7 @@ msgid ""
msgstr ""
"\n"
"Permite anonimilor sa Acceseze Portalul.\n"
-"=================================\n"
+"========================================\n"
" "
#. module: base
@@ -3021,7 +3023,7 @@ msgid ""
msgstr ""
"\n"
"Managementul Serviciului de asistenta tehnica.\n"
-"===================================\n"
+"==============================================\n"
"\n"
"La fel ca si inregistrarea si prelucrarea reclamatiilor, Serviciul de "
"Asistenta tehnica este un instrument\n"
@@ -3095,7 +3097,8 @@ msgstr ""
"\n"
"Acesta este modulul de baza pentru gestionarea produselor si a listelor de "
"preturi in OpenERP.\n"
-"========================================================================\n"
+"============================================================================="
+"=================\n"
"\n"
"Produsele accepta variante, diferite metode de stabilire a pretului, "
"informatii referitoare la furnizori,\n"
@@ -3103,7 +3106,7 @@ msgstr ""
"proprietati.\n"
"\n"
"Listele de preturi accepta:\n"
-"--------------------\n"
+"---------------------------\n"
" * Niveluri multiple de reducere (dupa produs, categorie, cantitati)\n"
" * Calculeaza pretul pe baza unor criterii diferite:\n"
" * Alte liste de preturi\n"
@@ -3134,11 +3137,10 @@ msgid ""
msgstr ""
"\n"
"Configureaza valorile implicite pentru conturile dumneavoastra analitice.\n"
-"==============================================\n"
+"=========================================================================\n"
"\n"
"Permite selectarea automata a conturilor analitice pe baza criteriilor:\n"
-"-----------------------------------------------------------------------------"
-"------\n"
+"-----------------------------------------------------------------------\n"
" * Produs \n"
" * Partener\n"
" * Utilizator\n"
@@ -3216,7 +3218,8 @@ msgstr ""
"\n"
"Modulul de baza pentru gestionarea distribuirii analitice si a comenzilor de "
"vanzare.\n"
-"==========================================================================\n"
+"============================================================================="
+"========\n"
"\n"
"Folosind acest modul veti putea lega conturile analitice de comenzile de "
"vanzare.\n"
@@ -3292,7 +3295,7 @@ msgid ""
msgstr ""
"\n"
"Acesta este un sistem complet de gestionare a documentelor.\n"
-"==============================================\n"
+"===========================================================\n"
" * Autentificare Utilizator\n"
" * Indexare Documente:- fisierele .pptx si .docx nu sunt acceptate in "
"platforma Windows.\n"
@@ -3351,8 +3354,8 @@ msgid ""
" "
msgstr ""
"\n"
-"Gestiunea cotarilor si a comenzilor de vanzare\n"
-" ==================================\n"
+"Gestionare cotațiilor și a comenzilor de vânzare\n"
+"================================================\n"
"\n"
"Aceasta aplicatie va permite sa va gestionati obiectivele de vanzari intr-un "
"mod efectiv si eficient, tinand evidenta tuturor comenzilor de vanzare si "
@@ -3363,7 +3366,7 @@ msgstr ""
"* **Cotare** -> **Comanda de vanzare** -> **Factura**\n"
"\n"
"Preferinte (numai daca este instalat si modulul Gestionarea Depozitului)\n"
-"-----------------------------------------------------\n"
+"------------------------------------------------------------------------\n"
"\n"
"Daca ati instalat si Gestionarea Depozitului, puteti efectua urmatoarele:\n"
"\n"
@@ -3382,7 +3385,7 @@ msgstr ""
"\n"
"\n"
"Tabloul de bord pentru Managerul de Vanzari va include\n"
-"------------------------------------------------\n"
+"------------------------------------------------------\n"
"* Cotatiile mele\n"
"* Cifra de afaceri lunara (Grafic)\n"
" "
@@ -3414,7 +3417,7 @@ msgid ""
msgstr ""
"\n"
"Modul pentru atasarea unui document google oricarui model.\n"
-"================================================\n"
+"==========================================================\n"
#. module: base
#: code:addons/base/ir/ir_fields.py:333
diff --git a/openerp/addons/base/i18n/ru.po b/openerp/addons/base/i18n/ru.po
index 8462334a157..60c8819d04b 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-04 05:45+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:48+0000\n"
+"X-Generator: Launchpad (build 16985)\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 86494b394d0..c03d0de6f72 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-04 05:45+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:49+0000\n"
+"X-Generator: Launchpad (build 16985)\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 e6004537e27..a62f242e210 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-04 05:45+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:49+0000\n"
+"X-Generator: Launchpad (build 16985)\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 8b77e92d32d..a0be5625110 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-04 05:37+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:38+0000\n"
+"X-Generator: Launchpad (build 16985)\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 b82fd4dab1d..3ac25b53253 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-04 05:45+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:48+0000\n"
+"X-Generator: Launchpad (build 16985)\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 f7fa2e21503..30a9a0af3ab 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-04 05:50+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:55+0000\n"
+"X-Generator: Launchpad (build 16985)\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 4223fd22758..2975c159a09 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-04 05:46+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:49+0000\n"
+"X-Generator: Launchpad (build 16985)\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 24b5ef7dad9..ee2313badfc 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-04 05:46+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:50+0000\n"
+"X-Generator: Launchpad (build 16985)\n"
#. module: base
#: model:ir.module.module,description:base.module_account_check_writing
@@ -25,6 +25,9 @@ msgid ""
"================================================\n"
" "
msgstr ""
+"\n"
+"โมดูลสำหรับการเขียน และพิมพ์เช็ค\n"
+" "
#. module: base
#: model:res.country,name:base.sh
diff --git a/openerp/addons/base/i18n/tlh.po b/openerp/addons/base/i18n/tlh.po
index b472d2931e0..199dd5d4073 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-04 05:46+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:50+0000\n"
+"X-Generator: Launchpad (build 16985)\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 97a4bc9fdd9..7dc68a474cf 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-04 05:46+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:50+0000\n"
+"X-Generator: Launchpad (build 16985)\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 4f00b0773af..e678f952bb4 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-04 05:47+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:50+0000\n"
+"X-Generator: Launchpad (build 16985)\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 1abdedd391b..3c8880f3270 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-04 05:47+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:50+0000\n"
+"X-Generator: Launchpad (build 16985)\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 b8287330153..68616a81d7f 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-04 05:47+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:51+0000\n"
+"X-Generator: Launchpad (build 16985)\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 53ec0e4b92c..6e09a983dc1 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-04 05:49+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:54+0000\n"
+"X-Generator: Launchpad (build 16985)\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 86da82ca6d9..ea043ffc98c 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-04 05:47+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:51+0000\n"
+"X-Generator: Launchpad (build 16985)\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 8c8db3b3231..33bfe9db460 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-04 05:49+0000\n"
-"X-Generator: Launchpad (build 16916)\n"
+"X-Launchpad-Export-Date: 2014-04-17 05:53+0000\n"
+"X-Generator: Launchpad (build 16985)\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 b36fc27d9d3..4e177959792 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -24,6 +24,8 @@ import logging
import operator
import os
import time
+import datetime
+import dateutil
import openerp
from openerp import SUPERUSER_ID
@@ -102,7 +104,9 @@ class ir_actions_report_xml(osv.osv):
cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s", (name,))
r = cr.dictfetchone()
if r:
- if r['report_rml'] or r['report_rml_content_data']:
+ if r['report_type'] in ['qweb-pdf', 'qweb-html']:
+ return r['report_name']
+ elif r['report_rml'] or r['report_rml_content_data']:
if r['parser']:
kwargs = { 'parser': operator.attrgetter(r['parser'])(openerp.addons) }
else:
@@ -125,7 +129,11 @@ class ir_actions_report_xml(osv.osv):
Look up a report definition and render the report for the provided IDs.
"""
new_report = self._lookup_report(cr, name)
- return new_report.create(cr, uid, res_ids, data, context)
+ # in order to use current yml test files with qweb reports
+ if isinstance(new_report, (str, unicode)):
+ return self.pool['report'].get_pdf(cr, uid, res_ids, new_report, data=data, context=context), 'pdf'
+ else:
+ return new_report.create(cr, uid, res_ids, data, context)
_name = 'ir.actions.report.xml'
_inherit = 'ir.actions.actions'
@@ -133,35 +141,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'),
+ ('controller', 'Controller'),
+ ('pdf', 'RML pdf (deprecated)'),
+ ('sxw', 'RML sxw (deprecated)'),
+ ('webkit', 'Webkit (deprecated)'),
+ ], 'Report Type', required=True, help="HTML will open the report directly in your browser, PDF will use wkhtmltopdf to render the HTML into a PDF file and let you download it, Controller allows you to define the url of a custom controller outputting any kind of report."),
+ 'report_name': fields.char('Template Name', required=True, help="For QWeb reports, name of the template used in the rendering. The method 'render_html' of the model 'report.template_name' will be called (if any) to give the html. 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/controller', help="The path to the main report file/controller (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',
@@ -268,7 +283,7 @@ class ir_actions_act_window(osv.osv):
'filter': fields.boolean('Filter'),
'auto_search':fields.boolean('Auto Search'),
'search_view' : fields.function(_search_view, type='text', string='Search View'),
- 'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view"),
+ 'multi': fields.boolean('Restrict to lists', help="If checked and the action is bound to a model, it will only appear in the More menu on list views"),
}
_defaults = {
@@ -550,6 +565,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',
@@ -928,9 +944,14 @@ class ir_actions_server(osv.osv):
'obj': obj,
'pool': self.pool,
'time': time,
+ 'datetime': datetime,
+ 'dateutil': dateutil,
'cr': cr,
'uid': uid,
'user': user,
+ 'context': context,
+ 'workflow': workflow,
+ 'Warning': openerp.exceptions.Warning,
}
def run(self, cr, uid, ids, context=None):
@@ -955,7 +976,6 @@ class ir_actions_server(osv.osv):
if context is None:
context = {}
res = False
- active_ids = context.get('active_ids', [context.get('active_id')])
for action in self.browse(cr, uid, ids, context):
eval_context = self._get_eval_context(cr, uid, action, context=context)
condition = action.condition
@@ -963,9 +983,7 @@ class ir_actions_server(osv.osv):
# Void (aka False) conditions are considered as True
condition = True
if hasattr(self, 'run_action_%s_multi' % action.state):
- # set active_ids in context only needed if one active_id
- run_context = dict(context, active_ids=active_ids)
- eval_context["context"] = run_context
+ run_context = eval_context['context']
expr = eval(str(condition), eval_context)
if not expr:
continue
@@ -975,6 +993,8 @@ class ir_actions_server(osv.osv):
elif hasattr(self, 'run_action_%s' % action.state):
func = getattr(self, 'run_action_%s' % action.state)
+ active_id = context.get('active_id')
+ active_ids = context.get('active_ids', [active_id] if active_id else [])
for active_id in active_ids:
# run context dedicated to a particular active_id
run_context = dict(context, active_ids=[active_id], active_id=active_id)
diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml
index 7e6c1f9be4b..4eb869daa80 100644
--- a/openerp/addons/base/ir/ir_actions.xml
+++ b/openerp/addons/base/ir/ir_actions.xml
@@ -57,39 +57,36 @@
-
+
+
+
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
@@ -117,7 +114,8 @@
filter_domain="['|', '|', '|', '|', ('name','ilike',self), ('model','ilike',self), ('type','ilike',self), ('report_name','ilike',self), ('report_type','ilike',self)]"
string="Report"/>
-
+
+
@@ -180,6 +178,7 @@
+
@@ -411,6 +410,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..8478682eb7c 100644
--- a/openerp/addons/base/ir/ir_attachment.py
+++ b/openerp/addons/base/ir/ir_attachment.py
@@ -26,6 +26,8 @@ import os
import re
from openerp import tools
+from openerp.tools.translate import _
+from openerp.exceptions import AccessError
from openerp.osv import fields,osv
from openerp import SUPERUSER_ID
@@ -64,22 +66,56 @@ 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 tools.config.filestore(cr.dbname)
+
+ def force_storage(self, cr, uid, context=None):
+ """Force all attachments to be stored in the currently configured storage"""
+ if not self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager'):
+ raise AccessError(_('Only administrators can execute this action.'))
+
+ location = self._storage(cr, uid, context)
+ domain = {
+ 'db': [('store_fname', '!=', False)],
+ 'file': [('db_datas', '!=', False)],
+ }[location]
+
+ ids = self.search(cr, uid, domain, context=context)
+ for attach in self.browse(cr, uid, ids, context=context):
+ attach.write({'datas': attach.datas})
+ return True
+
# '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)
+ def _full_path(self, cr, uid, 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 _file_read(self, cr, uid, location, fname, bin_size=False):
- full_path = self._full_path(cr, uid, location, fname)
+ def _get_path(self, cr, uid, bin_data):
+ sha = hashlib.sha1(bin_data).hexdigest()
+
+ # retro compatibility
+ fname = sha[:3] + '/' + sha
+ full_path = self._full_path(cr, uid, 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, 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, fname, bin_size=False):
+ full_path = self._full_path(cr, uid, fname)
r = ''
try:
if bin_size:
@@ -90,26 +126,21 @@ class ir_attachment(osv.osv):
_logger.error("_read_file reading %s",full_path)
return r
- def _file_write(self, cr, uid, location, value):
+ def _file_write(self, cr, uid, 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, 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):
+ def _file_delete(self, cr, uid, fname):
count = self.search(cr, 1, [('store_fname','=',fname)], count=True)
- if count <= 1:
- full_path = self._full_path(cr, uid, location, fname)
+ full_path = self._full_path(cr, uid, fname)
+ if count <= 1 and os.path.exists(full_path):
try:
os.unlink(full_path)
except OSError:
@@ -122,11 +153,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')
bin_size = context.get('bin_size')
for attach in self.browse(cr, uid, ids, context=context):
- if location and attach.store_fname:
- result[attach.id] = self._file_read(cr, uid, location, attach.store_fname, bin_size)
+ if attach.store_fname:
+ result[attach.id] = self._file_read(cr, uid, attach.store_fname, bin_size)
else:
result[attach.id] = attach.db_datas
return result
@@ -137,17 +167,17 @@ 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:
- attach = self.browse(cr, uid, id, context=context)
- if attach.store_fname:
- self._file_delete(cr, uid, location, attach.store_fname)
- fname = self._file_write(cr, uid, location, value)
+ attach = self.browse(cr, uid, id, context=context)
+ if attach.store_fname:
+ self._file_delete(cr, uid, attach.store_fname)
+ if location != 'db':
+ fname = self._file_write(cr, uid, value)
# SUPERUSER_ID as probably don't have write access, trigger during create
- super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], {'store_fname': fname, 'file_size': file_size}, context=context)
+ super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], {'store_fname': fname, 'file_size': file_size, 'db_datas': False}, context=context)
else:
- super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], {'db_datas': value, 'file_size': file_size}, context=context)
+ super(ir_attachment, self).write(cr, SUPERUSER_ID, [id], {'db_datas': value, 'file_size': file_size, 'store_fname': False}, context=context)
return True
_name = 'ir.attachment'
@@ -285,11 +315,9 @@ 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:
- for attach in self.browse(cr, uid, ids, context=context):
- if attach.store_fname:
- self._file_delete(cr, uid, location, attach.store_fname)
+ for attach in self.browse(cr, uid, ids, context=context):
+ if attach.store_fname:
+ self._file_delete(cr, uid, attach.store_fname)
return super(ir_attachment, self).unlink(cr, uid, ids, context)
def create(self, cr, uid, values, context=None):
@@ -303,4 +331,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 18699110605..258d405deb5 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,19 +77,22 @@ 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
def _handle_exception(self, exception):
- # If handle exception return something different than None, it will be used as a response
- raise
+ # If handle_exception returns something different than None, it will be used as a response
+ return request._handle_exception(exception)
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:
@@ -135,6 +138,8 @@ class ir_http(osv.AbstractModel):
m = request.registry.get('ir.module.module')
ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')], context=request.context)
installed = set(x['name'] for x in m.read(cr, 1, ids, ['name'], context=request.context))
+ if openerp.tools.config['test_enable']:
+ installed.add(openerp.modules.module.current_test)
mods = [''] + openerp.conf.server_wide_modules + sorted(installed)
self._routing_map = http.routing_map(mods, False, converters=self._get_converters())
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..4608f63bd49 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):
@@ -144,7 +144,7 @@ class QWeb(orm.AbstractModel):
"""Add a parsed template in the context. Used to preprocess templates."""
qwebcontext.templates[name] = node
- def load_document(self, document, qwebcontext):
+ def load_document(self, document, res_id, qwebcontext):
"""
Loads an XML document and installs any contained template in the engine
"""
@@ -156,9 +156,13 @@ class QWeb(orm.AbstractModel):
dom = xml.dom.minidom.parse(document)
for node in dom.documentElement.childNodes:
- if node.nodeType == self.node.ELEMENT_NODE and node.getAttribute('t-name'):
- name = str(node.getAttribute("t-name"))
- self.add_template(qwebcontext, name, node)
+ if node.nodeType == self.node.ELEMENT_NODE:
+ if node.getAttribute('t-name'):
+ name = str(node.getAttribute("t-name"))
+ self.add_template(qwebcontext, name, node)
+ if res_id and node.tagName == "t":
+ self.add_template(qwebcontext, res_id, node)
+ res_id = None
def get_template(self, name, qwebcontext):
origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
@@ -166,8 +170,8 @@ 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)
- self.load_document(xml_doc, qwebcontext=qwebcontext)
+ raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
+ self.load_document(xml_doc, isinstance(name, (int, long)) and name or None, qwebcontext=qwebcontext)
if name in qwebcontext.templates:
return qwebcontext.templates[name]
@@ -179,7 +183,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 +211,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)))
@@ -241,7 +245,7 @@ class QWeb(orm.AbstractModel):
if attribute_name == "groups":
cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
- can_see = self.user_has_groups(cr, uid, groups=attribute_value)
+ can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
if not can_see:
return ''
continue
@@ -292,7 +296,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)
@@ -332,7 +336,9 @@ class QWeb(orm.AbstractModel):
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
- inner = werkzeug.utils.escape(self.eval_str(template_attributes["esc"], qwebcontext))
+ options = json.loads(template_attributes.get('esc-options') or '{}')
+ widget = self.get_widget_for(options.get('widget', ''))
+ inner = widget.format(template_attributes['esc'], options, qwebcontext)
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
@@ -383,7 +389,12 @@ class QWeb(orm.AbstractModel):
cr = d.get('request') and d['request'].cr or None
uid = d.get('request') and d['request'].uid or None
- return self.render(cr, uid, self.eval_format(template_attributes["call"], d), d)
+ template = self.eval_format(template_attributes["call"], d)
+ try:
+ template = int(template)
+ except ValueError:
+ pass
+ return self.render(cr, uid, template, d)
def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
if "value" in template_attributes:
@@ -416,8 +427,10 @@ class QWeb(orm.AbstractModel):
element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
def get_converter_for(self, field_type):
- return self.pool.get('ir.qweb.field.' + field_type,
- self.pool['ir.qweb.field'])
+ return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
+
+ def get_widget_for(self, widget):
+ return self.pool.get('ir.qweb.widget.' + widget, self.pool['ir.qweb.widget'])
#--------------------------------------------------------------------
# QWeb Fields converters
@@ -488,7 +501,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 +515,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)
@@ -602,7 +616,7 @@ class DateTimeConverter(osv.AbstractModel):
if isinstance(value, basestring):
value = datetime.datetime.strptime(
value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
- value = column.context_timestamp(
+ value = fields.datetime.context_timestamp(
cr, uid, timestamp=value, context=context)
if options and 'format' in options:
@@ -611,6 +625,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):
@@ -804,6 +821,93 @@ class RelativeDatetimeConverter(osv.AbstractModel):
return babel.dates.format_timedelta(
value - reference, add_direction=True, locale=locale)
+
+class Contact(orm.AbstractModel):
+ _name = 'ir.qweb.field.contact'
+ _inherit = 'ir.qweb.field.many2one'
+
+ def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+ if options is None:
+ options = {}
+ opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
+
+ if not getattr(record, field_name):
+ return None
+
+ id = getattr(record, field_name).id
+ field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context={"show_address": True})
+ value = werkzeug.utils.escape( field_browse.name_get()[0][1] )
+
+ val = {
+ 'name': value.split("\n")[0],
+ 'address': werkzeug.utils.escape("\n".join(value.split("\n")[1:])),
+ 'phone': field_browse.phone,
+ 'mobile': field_browse.mobile,
+ 'fax': field_browse.fax,
+ 'city': field_browse.city,
+ 'country_id': field_browse.country_id and field_browse.country_id.name_get()[0][1],
+ 'email': field_browse.email,
+ 'fields': opf,
+ 'object': field_browse,
+ 'options': options
+ }
+
+ html = self.pool["ir.ui.view"].render(cr, uid, "base.contact", val, engine='ir.qweb', context=context).decode('utf8')
+
+ return HTMLSafe(html)
+
+class QwebView(orm.AbstractModel):
+ _name = 'ir.qweb.field.qweb'
+ _inherit = 'ir.qweb.field.many2one'
+
+ def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+ if not getattr(record, field_name):
+ return None
+
+ view = getattr(record, field_name)
+
+ if view._model._name != "ir.ui.view":
+ _logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
+ return None
+
+ ctx = (context or {}).copy()
+ ctx['object'] = record
+ html = view.render(ctx, engine='ir.qweb', context=ctx).decode('utf8')
+
+ return HTMLSafe(html)
+
+class QwebWidget(osv.AbstractModel):
+ _name = 'ir.qweb.widget'
+
+ def _format(self, inner, options, qwebcontext):
+ return self.pool['ir.qweb'].eval_str(inner, qwebcontext)
+
+ def format(self, inner, options, qwebcontext):
+ return werkzeug.utils.escape(self._format(inner, options, qwebcontext))
+
+class QwebWidgetMonetary(osv.AbstractModel):
+ _name = 'ir.qweb.widget.monetary'
+ _inherit = 'ir.qweb.widget'
+
+ def _format(self, inner, options, qwebcontext):
+ inner = self.pool['ir.qweb'].eval(inner, qwebcontext)
+ display = self.pool['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
+ precision = int(round(math.log10(display.rounding)))
+ fmt = "%.{0}f".format(-precision if precision < 0 else 0)
+ lang_code = qwebcontext.context.get('lang') or 'en_US'
+ formatted_amount = self.pool['res.lang'].format(
+ qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True
+ )
+ pre = post = u''
+ if display.position == 'before':
+ pre = u'{symbol} '
+ else:
+ post = u' {symbol}'
+
+ return u'{pre}{0}{post}'.format(
+ formatted_amount, pre=pre, post=post
+ ).format(symbol=display.symbol,)
+
class HTMLSafe(object):
""" HTMLSafe string wrapper, Werkzeug's escape() has special handling for
objects with a ``__html__`` methods but AFAIK does not provide any such
diff --git a/openerp/addons/base/ir/ir_qweb.xml b/openerp/addons/base/ir/ir_qweb.xml
new file mode 100644
index 00000000000..a21a3fcf2af
--- /dev/null
+++ b/openerp/addons/base/ir/ir_qweb.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index fe62d9e80cc..314865ae066 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -73,12 +73,18 @@ class view_custom(osv.osv):
class view(osv.osv):
_name = 'ir.ui.view'
- def _get_model_data(self, cr, uid, ids, *args, **kwargs):
- ir_model_data = self.pool.get('ir.model.data')
- data_ids = ir_model_data.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)])
- result = dict(zip(ids, data_ids))
+ def _get_model_data(self, cr, uid, ids, fname, args, context=None):
+ result = dict.fromkeys(ids, False)
+ IMD = self.pool['ir.model.data']
+ data_ids = IMD.search_read(cr, uid, [('res_id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
+ result.update(map(itemgetter('res_id', 'id'), data_ids))
return result
+ def _views_from_model_data(self, cr, uid, ids, context=None):
+ IMD = self.pool['ir.model.data']
+ data_ids = IMD.search_read(cr, uid, [('id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
+ return map(itemgetter('res_id'), data_ids)
+
_columns = {
'name': fields.char('View Name', required=True),
'model': fields.char('Object', select=True),
@@ -97,7 +103,11 @@ class view(osv.osv):
'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
'field_parent': fields.char('Child Field'),
- 'model_data_id': fields.function(_get_model_data, type='many2one', relation='ir.model.data', string="Model Data", store=True),
+ 'model_data_id': fields.function(_get_model_data, type='many2one', relation='ir.model.data', string="Model Data",
+ store={
+ _name: (lambda s, c, u, i, ctx=None: i, None, 10),
+ 'ir.model.data': (_views_from_model_data, ['model', 'res_id'], 10),
+ }),
'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
help="ID of the view defined in xml file"),
'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
@@ -316,14 +326,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 +407,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 +418,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):
@@ -686,6 +698,16 @@ class view(osv.osv):
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
node.set(action, 'false')
+ if node.tag in ('kanban'):
+ group_by_field = node.get('default_group_by')
+ if group_by_field and Model._all_columns.get(group_by_field):
+ group_by_column = Model._all_columns[group_by_field].column
+ if group_by_column._type == 'many2one':
+ group_by_model = Model.pool.get(group_by_column._obj)
+ for action, operation in (('group_create', 'create'), ('group_delete', 'unlink'), ('group_edit', 'write')):
+ if not node.get(action) and not group_by_model.check_access_rights(cr, user, operation, raise_exception=False):
+ node.set(action, 'false')
+
arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
for k in fields.keys():
if k not in fields_def:
@@ -707,10 +729,13 @@ class view(osv.osv):
#------------------------------------------------------
@tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
def read_template(self, cr, uid, xml_id, context=None):
- if '.' not in xml_id:
- raise ValueError('Invalid template id: %r' % (xml_id,))
+ if isinstance(xml_id, (int, long)):
+ view_id = xml_id
+ else:
+ if '.' not in xml_id:
+ raise ValueError('Invalid template id: %r' % (xml_id,))
+ view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
- view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
arch_tree = etree.fromstring(arch)
@@ -726,9 +751,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,25 +782,30 @@ 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
# running index by tag type, for XPath query generation
indexes = collections.defaultdict(lambda: 0)
for child in e.iterchildren(tag=etree.Element):
- indexes[child.tag] += 1
- self.distribute_branding(child, distributed_branding,
- parent_xpath=node_path,
- index_map=indexes)
+ if child.get('data-oe-xpath'):
+ # injected by view inheritance, skip otherwise
+ # generated xpath is incorrect
+ 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
@@ -813,9 +860,6 @@ class view(osv.osv):
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
if isinstance(id_or_xml_id, list):
id_or_xml_id = id_or_xml_id[0]
- tname = id_or_xml_id
- if isinstance(tname, (int, long)):
- tname = self.get_view_xmlid(cr, uid, tname)
if not context:
context = {}
@@ -833,7 +877,7 @@ class view(osv.osv):
def loader(name):
return self.read_template(cr, uid, name, context=context)
- return self.pool[engine].render(cr, uid, tname, qcontext, loader=loader, context=context)
+ return self.pool[engine].render(cr, uid, id_or_xml_id, qcontext, loader=loader, context=context)
#------------------------------------------------------
# Misc
diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py
index a21b034d1b0..6af09b7ecbb 100644
--- a/openerp/addons/base/ir/ir_values.py
+++ b/openerp/addons/base/ir/ir_values.py
@@ -394,17 +394,16 @@ class ir_values(osv.osv):
for action in cr.dictfetchall():
if not action['value']:
continue # skip if undefined
- action_model,id = action['value'].split(',')
- fields = [
- field
- for field in self.pool[action_model]._all_columns
- if field not in EXCLUDED_FIELDS]
+ action_model_name, action_id = action['value'].split(',')
+ action_model = self.pool.get(action_model_name)
+ if not action_model:
+ continue # unknow model? skip it
+ fields = [field for field in action_model._all_columns if field not in EXCLUDED_FIELDS]
# FIXME: needs cleanup
try:
- action_def = self.pool[action_model].read(cr, uid, int(id), fields, context)
+ action_def = action_model.read(cr, uid, int(action_id), fields, context)
if action_def:
- if action_model in ('ir.actions.report.xml','ir.actions.act_window',
- 'ir.actions.wizard'):
+ if action_model_name in ('ir.actions.report.xml', 'ir.actions.act_window'):
groups = action_def.get('groups_id')
if groups:
cr.execute('SELECT 1 FROM res_groups_users_rel WHERE gid IN %s AND uid=%s',
diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py
index 6ad74857f39..b82e6b8242d 100644
--- a/openerp/addons/base/module/module.py
+++ b/openerp/addons/base/module/module.py
@@ -31,6 +31,7 @@ import shutil
import tempfile
import urllib
import urllib2
+import urlparse
import zipfile
import zipimport
import lxml.html
@@ -41,6 +42,7 @@ except ImportError:
from StringIO import StringIO # NOQA
import openerp
+import openerp.exceptions
from openerp import modules, tools
from openerp.modules.db import create_categories
from openerp.modules import get_module_resource
@@ -621,40 +623,14 @@ class module(osv.osv):
return res
def download(self, cr, uid, ids, download=True, context=None):
- res = []
- default_version = modules.adapt_version('1.0')
- for mod in self.browse(cr, uid, ids, context=context):
- if not mod.url:
- continue
- match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
- version = default_version
- if match:
- version = match.group(1)
- if parse_version(mod.installed_version) >= parse_version(version):
- continue
- res.append(mod.url)
- if not download:
- continue
- zip_content = urllib.urlopen(mod.url).read()
- fname = modules.get_module_path(str(mod.name) + '.zip', downloaded=True)
- try:
- with open(fname, 'wb') as fp:
- fp.write(zip_content)
- except Exception:
- _logger.exception('Error when trying to create module '
- 'file %s', fname)
- raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
- terp = self.get_module_info(mod.name)
- self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
- cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s', (mod.id,))
- self._update_dependencies(cr, uid, mod, terp.get('depends', []))
- self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized'))
- # Import module
- zimp = zipimport.zipimporter(fname)
- zimp.load_module(mod.name)
- return res
+ return []
def install_from_urls(self, cr, uid, urls, context=None):
+ if not self.pool['res.users'].has_group(cr, uid, 'base.group_system'):
+ raise openerp.exceptions.AccessDenied()
+
+ apps_server = urlparse.urlparse(self.get_apps_server(cr, uid, context=context))
+
OPENERP = 'openerp'
tmp = tempfile.mkdtemp()
_logger.debug('Install from url: %r', urls)
@@ -663,6 +639,11 @@ class module(osv.osv):
for module_name, url in urls.items():
if not url:
continue # nothing to download, local version is already the last one
+
+ up = urlparse.urlparse(url)
+ if up.scheme != apps_server.scheme or up.netloc != apps_server.netloc:
+ raise openerp.exceptions.AccessDenied()
+
try:
_logger.info('Downloading module `%s` from OpenERP Apps', module_name)
content = urllib2.urlopen(url).read()
@@ -727,8 +708,8 @@ class module(osv.osv):
finally:
shutil.rmtree(tmp)
- def install_by_names(self, cr, uid, names, context=None):
- raise NotImplementedError('# TODO')
+ def get_apps_server(self, cr, uid, context=None):
+ return tools.config.get('apps_server', 'https://apps.openerp.com/apps')
def _update_dependencies(self, cr, uid, mod_browse, depends=None):
if depends is None:
diff --git a/openerp/addons/base/module/module_data.xml b/openerp/addons/base/module/module_data.xml
index 2b7fd2bdab5..ad5998b091b 100644
--- a/openerp/addons/base/module/module_data.xml
+++ b/openerp/addons/base/module/module_data.xml
@@ -85,12 +85,6 @@
11
-
- Survey
- Lets you create, send, share and answer to surveys.
- 11
-
-
Marketing
Helps you manage your marketing campaigns step by step.
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 558afbeaae0..64ab5676ca2 100644
--- a/openerp/addons/base/res/res_company.py
+++ b/openerp/addons/base/res/res_company.py
@@ -124,7 +124,8 @@ class res_company(osv.osv):
'rml_footer': fields.text('Report Footer', help="Footer text displayed at the bottom of all reports."),
'rml_footer_readonly': fields.related('rml_footer', type='text', string='Report Footer', readonly=True),
'custom_footer': fields.boolean('Custom Footer', help="Check this to define the report footer manually. Otherwise it will be filled in automatically."),
- 'font': fields.many2one('res.font', string="Font",help="Set the font into the report header, it will be used as default font in the RML reports of the user company"),
+ 'font': fields.many2one('res.font', string="Font", domain=[('mode', 'in', ('Normal', 'Regular', 'all', 'Book'))],
+ help="Set the font into the report header, it will be used as default font in the RML reports of the user company"),
'logo': fields.related('partner_id', 'image', string="Logo", type="binary"),
'logo_web': fields.function(_get_logo_web, string="Logo Web", type="binary", store={
'res.company': (lambda s, c, u, i, x: i, ['partner_id'], 10),
@@ -147,7 +148,7 @@ class res_company(osv.osv):
'website': fields.related('partner_id', 'website', string="Website", type="char", size=64),
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),
'company_registry': fields.char('Company Registry', size=64),
- 'paper_format': fields.selection([('a4', 'A4'), ('us_letter', 'US Letter')], "Paper Format", required=True),
+ 'rml_paper_format': fields.selection([('a4', 'A4'), ('us_letter', 'US Letter')], "Paper Format", required=True, oldname='paper_format'),
}
_sql_constraints = [
('name_uniq', 'unique (name)', 'The company name must be unique !')
@@ -207,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):
"""
@@ -388,8 +388,8 @@ class res_company(osv.osv):
_header_a4 = _header_main % ('21.7cm', '27.7cm', '27.7cm', '27.7cm', '27.8cm', '27.3cm', '25.3cm', '25.0cm', '25.0cm', '24.6cm', '24.6cm', '24.5cm', '24.5cm')
_header_letter = _header_main % ('20cm', '26.0cm', '26.0cm', '26.0cm', '26.1cm', '25.6cm', '23.6cm', '23.3cm', '23.3cm', '22.9cm', '22.9cm', '22.8cm', '22.8cm')
- def onchange_paper_format(self, cr, uid, ids, paper_format, context=None):
- if paper_format == 'us_letter':
+ def onchange_rml_paper_format(self, cr, uid, ids, rml_paper_format, context=None):
+ if rml_paper_format == 'us_letter':
return {'value': {'rml_header': self._header_letter}}
return {'value': {'rml_header': self._header_a4}}
@@ -398,7 +398,7 @@ class res_company(osv.osv):
_defaults = {
'currency_id': _get_euro,
- 'paper_format': 'a4',
+ 'rml_paper_format': 'a4',
'rml_header':_get_header,
'rml_header2': _header2,
'rml_header3': _header3,
diff --git a/openerp/addons/base/res/res_company_view.xml b/openerp/addons/base/res/res_company_view.xml
index c037b0e76ac..5139623031f 100644
--- a/openerp/addons/base/res/res_company_view.xml
+++ b/openerp/addons/base/res/res_company_view.xml
@@ -23,9 +23,6 @@
-
-
-
@@ -75,29 +72,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -106,6 +80,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py
index 4fe6ff1989e..5bbd377f29d 100644
--- a/openerp/addons/base/res/res_config.py
+++ b/openerp/addons/base/res/res_config.py
@@ -493,6 +493,10 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
return res
def execute(self, cr, uid, ids, context=None):
+ if context is None:
+ context = {}
+
+ context = dict(context, active_test=False)
if uid != SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager'):
raise openerp.exceptions.AccessError(_("Only administrators can change the settings"))
@@ -500,7 +504,7 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
ir_module = self.pool['ir.module.module']
res_groups = self.pool['res.groups']
- classified = self._get_classified_fields(cr, uid, context)
+ classified = self._get_classified_fields(cr, uid, context=context)
config = self.browse(cr, uid, ids[0], context)
diff --git a/openerp/addons/base/res/res_country.py b/openerp/addons/base/res/res_country.py
index df68d211a97..df507dcbac9 100644
--- a/openerp/addons/base/res/res_country.py
+++ b/openerp/addons/base/res/res_country.py
@@ -56,6 +56,7 @@ addresses belonging to this country.\n\nYou can use the python-style string pate
\n%(country_name)s: the name of the country
\n%(country_code)s: the code of the country"""),
'currency_id': fields.many2one('res.currency', 'Currency'),
+ 'image': fields.binary("Image"),
}
_sql_constraints = [
('name_uniq', 'unique (name)',
diff --git a/openerp/addons/base/res/res_country_data.xml b/openerp/addons/base/res/res_country_data.xml
index 13c1025cf6b..eff97fb0aac 100644
--- a/openerp/addons/base/res/res_country_data.xml
+++ b/openerp/addons/base/res/res_country_data.xml
@@ -5,46 +5,55 @@
Andorra, Principality of
ad
+
United Arab Emirates
ae
+
Afghanistan, Islamic State of
af
+
Antigua and Barbuda
ag
+
Anguilla
ai
+
Albania
al
+
Armenia
am
+
Netherlands Antilles
an
+
Angola
ao
+
@@ -55,103 +64,123 @@
Argentina
ar
+
American Samoa
as
+
Austria
at
+
Australia
au
+
Aruba
aw
+
Åland Islands
ax
+
Azerbaijan
az
+
Bosnia-Herzegovina
ba
+
Barbados
bb
+
Bangladesh
bd
+
Belgium
be
+
Burkina Faso
bf
+
Bulgaria
bg
+
Bahrain
bh
+
Burundi
bi
+
Benin
bj
+
Saint Barthélémy
bl
+
Bermuda
bm
+
Brunei Darussalam
bn
+
Bolivia
bo
+
@@ -162,17 +191,20 @@
Brazil
br
+
Bahamas
bs
+
Bhutan
bt
+
@@ -183,158 +215,189 @@
Botswana
bw
+
Belarus
by
+
Belize
bz
+
Canada
ca
+
Cocos (Keeling) Islands
cc
+
Central African Republic
cf
+
Congo, Democratic Republic of the
cd
+
Congo
cg
+
Switzerland
ch
+
Ivory Coast (Cote D'Ivoire)
ci
+
Cook Islands
ck
+
Chile
cl
+
Cameroon
cm
+
China
cn
+
Colombia
co
+
Costa Rica
cr
+
Cuba
cu
+
Cape Verde
cv
+
Curaçao
cw
+
Christmas Island
cx
+
Cyprus
cy
+
Czech Republic
cz
+
Germany
de
+
Djibouti
dj
+
Denmark
dk
+
Dominica
dm
+
Dominican Republic
do
+
Algeria
dz
+
Ecuador
ec
+
Estonia
ee
+
Egypt
eg
+
@@ -345,63 +408,75 @@
Eritrea
er
+
Spain
es
+
Ethiopia
et
+
Finland
fi
+
Fiji
fj
+
Falkland Islands
fk
+
Micronesia
fm
+
Faroe Islands
fo
+
France
fr
+
Gabon
ga
+
Grenada
gd
+
Georgia
ge
+
@@ -412,31 +487,37 @@
Ghana
gh
+
Gibraltar
gi
+
Guernsey
gg
+
Greenland
gl
+
Gambia
gm
+
Guinea
gn
+
@@ -447,41 +528,49 @@
Equatorial Guinea
gq
+
Greece
gr
+
South Georgia and the South Sandwich Islands
gs
+
Guatemala
gt
+
Guam (USA)
gu
+
Guinea Bissau
gw
+
Guyana
gy
+
Hong Kong
hk
+
@@ -492,222 +581,266 @@
Honduras
hn
+
Croatia
hr
+
Haiti
ht
+
Hungary
hu
+
Indonesia
id
+
Ireland
ie
+
Israel
il
+
Isle of Man
im
+
India
in
+
British Indian Ocean Territory
io
+
Iraq
iq
+
Iran
ir
+
Iceland
is
+
Italy
it
+
Jersey
je
+
Jamaica
jm
+
Jordan
jo
+
Japan
jp
+
Kenya
ke
+
Kyrgyz Republic (Kyrgyzstan)
kg
+
Cambodia, Kingdom of
kh
+
Kiribati
ki
+
Comoros
km
+
Saint Kitts & Nevis Anguilla
kn
+
North Korea
kp
+
South Korea
kr
+
Kuwait
kw
+
Cayman Islands
ky
+
Kazakhstan
kz
+
Laos
la
+
Lebanon
lb
+
Saint Lucia
lc
+
Liechtenstein
li
+
Sri Lanka
lk
+
Liberia
lr
+
Lesotho
ls
+
Lithuania
lt
+
Luxembourg
lu
+
Latvia
lv
+
Libya
ly
+
Morocco
ma
+
Monaco
mc
+
Moldavia
md
+
Montenegro
me
+
@@ -718,142 +851,170 @@
Madagascar
mg
+
Marshall Islands
mh
+
Macedonia, the former Yugoslav Republic of
mk
+
Mali
ml
+
Myanmar
mm
+
Mongolia
mn
+
Macau
mo
+
Northern Mariana Islands
mp
+
Martinique (French)
mq
+
Mauritania
mr
+
Montserrat
ms
+
Malta
mt
+
Mauritius
mu
+
Maldives
mv
+
Malawi
mw
+
Mexico
mx
+
Malaysia
my
+
Mozambique
mz
+
Namibia
na
+
New Caledonia (French)
nc
+
Niger
ne
+
Norfolk Island
nf
+
Nigeria
ng
+
Nicaragua
ni
+
Netherlands
nl
+
Norway
no
+
Nepal
np
+
Nauru
nr
+
@@ -864,66 +1025,79 @@
Niue
nu
+
New Zealand
nz
+
Oman
om
+
Panama
pa
+
Peru
pe
+
Polynesia (French)
pf
+
Papua New Guinea
pg
+
Philippines
ph
+
Pakistan
pk
+
Poland
pl
+
Saint Pierre and Miquelon
pm
+
Pitcairn Island
pn
+
Puerto Rico
pr
+
@@ -934,21 +1108,25 @@
Portugal
pt
+
Palau
pw
+
Paraguay
py
+
Qatar
qa
+
@@ -959,61 +1137,73 @@
Romania
ro
+
Serbia
rs
+
Russian Federation
ru
+
Rwanda
rw
+
Saudi Arabia
sa
+
Solomon Islands
sb
+
Seychelles
sc
+
Sudan
sd
+
Sweden
se
+
Singapore
sg
+
Saint Helena
sh
+
Slovenia
si
+
@@ -1024,131 +1214,157 @@
Slovakia
sk
+
Sierra Leone
sl
+
San Marino
sm
+
Senegal
sn
+
Somalia
so
+
Suriname
sr
+
South Sudan
ss
+
Saint Tome (Sao Tome) and Principe
st
+
El Salvador
sv
+
Sint Maarten (Dutch part)
sx
+
Syria
sy
+
Swaziland
sz
+
Turks and Caicos Islands
tc
+
Chad
td
+
French Southern Territories
tf
+
Togo
tg
+
Thailand
th
+
Tajikistan
tj
+
Tokelau
tk
+
Turkmenistan
tm
+
Tunisia
tn
+
Tonga
to
+
East Timor
tp
+
Turkey
tr
+
Trinidad and Tobago
tt
+
Tuvalu
tv
+
@@ -1159,23 +1375,27 @@
Tanzania
tz
+
Ukraine
ua
+
Uganda
ug
+
United Kingdom
gb
+
@@ -1186,52 +1406,62 @@
United States
us
+
Uruguay
uy
+
Uzbekistan
uz
+
Holy See (Vatican City State)
va
+
Saint Vincent & Grenadines
vc
+
Venezuela
ve
+
Virgin Islands (British)
vg
+
Virgin Islands (USA)
vi
+
Vietnam
vn
+
Vanuatu
vu
+
@@ -1242,11 +1472,13 @@
Samoa
ws
+
Yemen
ye
+
@@ -1262,11 +1494,13 @@
South Africa
za
+
Zambia
zm
+
@@ -1279,6 +1513,7 @@
Zimbabwe
zw
+
diff --git a/openerp/addons/base/res/res_country_view.xml b/openerp/addons/base/res/res_country_view.xml
index 8f64d330440..e4784c3d9ca 100644
--- a/openerp/addons/base/res/res_country_view.xml
+++ b/openerp/addons/base/res/res_country_view.xml
@@ -22,6 +22,7 @@
res.country
diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml
index a0e32ab83bd..a5f04a120a7 100644
--- a/openerp/addons/base/res/res_partner_view.xml
+++ b/openerp/addons/base/res/res_partner_view.xml
@@ -158,8 +158,8 @@
-
+ attrs="{'invisible': ['|', ('is_company', '=', True),('parent_id', '=', False)]}"/>
+
@@ -188,7 +188,7 @@
-
+
diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py
index ad017381409..32e81ee600f 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)]
@@ -387,7 +388,7 @@ class res_users(osv.osv):
if not password:
return False
user_id = False
- cr = self.pool.db.cursor()
+ cr = self.pool.cursor()
try:
# autocommit: our single update request will be performed atomically.
# (In this way, there is no opportunity to have two transactions
@@ -438,7 +439,7 @@ class res_users(osv.osv):
# Successfully logged in as admin!
# Attempt to guess the web base url...
if user_agent_env and user_agent_env.get('base_location'):
- cr = self.pool.db.cursor()
+ cr = self.pool.cursor()
try:
base = user_agent_env['base_location']
ICP = self.pool['ir.config_parameter']
@@ -459,7 +460,7 @@ class res_users(osv.osv):
raise openerp.exceptions.AccessDenied()
if self._uid_cache.get(db, {}).get(uid) == passwd:
return
- cr = self.pool.db.cursor()
+ cr = self.pool.cursor()
try:
self.check_credentials(cr, uid, passwd)
if self._uid_cache.has_key(db):
@@ -585,7 +586,7 @@ class groups_implied(osv.osv):
res = super(groups_implied, self).write(cr, uid, ids, values, context)
if values.get('users') or values.get('implied_ids'):
# add all implied groups (to all users of each group)
- for g in self.browse(cr, uid, ids):
+ for g in self.browse(cr, uid, ids, context=context):
gids = map(int, g.trans_implied_ids)
vals = {'users': [(4, u.id) for u in g.users]}
super(groups_implied, self).write(cr, uid, gids, vals, context)
@@ -899,16 +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)
- return {
- 'type': 'ir.actions.act_window_close',
- }
+ 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').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 9c84d4326f4..8a3f372af7a 100644
--- a/openerp/addons/base/res/res_users_view.xml
+++ b/openerp/addons/base/res/res_users_view.xml
@@ -231,7 +231,7 @@
res.users
-
+
@@ -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/static/img/country_flags/ad.png b/openerp/addons/base/static/img/country_flags/ad.png
new file mode 100644
index 00000000000..3806c28c6d8
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ad.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ae.png b/openerp/addons/base/static/img/country_flags/ae.png
new file mode 100644
index 00000000000..4d5c0a3a657
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ae.png differ
diff --git a/openerp/addons/base/static/img/country_flags/af.png b/openerp/addons/base/static/img/country_flags/af.png
new file mode 100644
index 00000000000..299482a1fad
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/af.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ag.png b/openerp/addons/base/static/img/country_flags/ag.png
new file mode 100644
index 00000000000..29ec9e8ad1e
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ag.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ai.png b/openerp/addons/base/static/img/country_flags/ai.png
new file mode 100644
index 00000000000..343a6dfc294
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ai.png differ
diff --git a/openerp/addons/base/static/img/country_flags/al.png b/openerp/addons/base/static/img/country_flags/al.png
new file mode 100644
index 00000000000..0d1b5f87cb3
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/al.png differ
diff --git a/openerp/addons/base/static/img/country_flags/am.png b/openerp/addons/base/static/img/country_flags/am.png
new file mode 100644
index 00000000000..8023ad7a4b2
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/am.png differ
diff --git a/openerp/addons/base/static/img/country_flags/an.png b/openerp/addons/base/static/img/country_flags/an.png
new file mode 100644
index 00000000000..701d55fbda9
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/an.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ao.png b/openerp/addons/base/static/img/country_flags/ao.png
new file mode 100644
index 00000000000..b599322efb8
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ao.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ar.png b/openerp/addons/base/static/img/country_flags/ar.png
new file mode 100644
index 00000000000..943ebb2be73
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ar.png differ
diff --git a/openerp/addons/base/static/img/country_flags/as.png b/openerp/addons/base/static/img/country_flags/as.png
new file mode 100644
index 00000000000..068d34174b6
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/as.png differ
diff --git a/openerp/addons/base/static/img/country_flags/at.png b/openerp/addons/base/static/img/country_flags/at.png
new file mode 100644
index 00000000000..758147d4bbb
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/at.png differ
diff --git a/openerp/addons/base/static/img/country_flags/au.png b/openerp/addons/base/static/img/country_flags/au.png
new file mode 100644
index 00000000000..6e864267043
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/au.png differ
diff --git a/openerp/addons/base/static/img/country_flags/aw.png b/openerp/addons/base/static/img/country_flags/aw.png
new file mode 100644
index 00000000000..0f362bbb3fc
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/aw.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ax.png b/openerp/addons/base/static/img/country_flags/ax.png
new file mode 100644
index 00000000000..e546ec92d5b
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ax.png differ
diff --git a/openerp/addons/base/static/img/country_flags/az.png b/openerp/addons/base/static/img/country_flags/az.png
new file mode 100644
index 00000000000..334567372f2
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/az.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ba.png b/openerp/addons/base/static/img/country_flags/ba.png
new file mode 100644
index 00000000000..2abf146fc11
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ba.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bb.png b/openerp/addons/base/static/img/country_flags/bb.png
new file mode 100644
index 00000000000..8fbafd7d12a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bb.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bd.png b/openerp/addons/base/static/img/country_flags/bd.png
new file mode 100644
index 00000000000..1754328d655
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bd.png differ
diff --git a/openerp/addons/base/static/img/country_flags/be.png b/openerp/addons/base/static/img/country_flags/be.png
new file mode 100644
index 00000000000..42e4ff638bc
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/be.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bf.png b/openerp/addons/base/static/img/country_flags/bf.png
new file mode 100644
index 00000000000..bf69a1318e8
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bf.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bg.png b/openerp/addons/base/static/img/country_flags/bg.png
new file mode 100644
index 00000000000..dbe55253e01
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bh.png b/openerp/addons/base/static/img/country_flags/bh.png
new file mode 100644
index 00000000000..50944f1ee6f
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bh.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bi.png b/openerp/addons/base/static/img/country_flags/bi.png
new file mode 100644
index 00000000000..6a04c66cdbb
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bi.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bj.png b/openerp/addons/base/static/img/country_flags/bj.png
new file mode 100644
index 00000000000..aac7f881956
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bj.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bl.png b/openerp/addons/base/static/img/country_flags/bl.png
new file mode 100644
index 00000000000..8fdf4c45717
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bl.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bm.png b/openerp/addons/base/static/img/country_flags/bm.png
new file mode 100644
index 00000000000..47753a3c677
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bn.png b/openerp/addons/base/static/img/country_flags/bn.png
new file mode 100644
index 00000000000..97aa098d88f
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bo.png b/openerp/addons/base/static/img/country_flags/bo.png
new file mode 100644
index 00000000000..44a066b1601
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bo.png differ
diff --git a/openerp/addons/base/static/img/country_flags/br.png b/openerp/addons/base/static/img/country_flags/br.png
new file mode 100644
index 00000000000..7231f543ffb
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/br.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bs.png b/openerp/addons/base/static/img/country_flags/bs.png
new file mode 100644
index 00000000000..d7482884694
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bs.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bt.png b/openerp/addons/base/static/img/country_flags/bt.png
new file mode 100644
index 00000000000..7f889b3c4b7
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bt.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bw.png b/openerp/addons/base/static/img/country_flags/bw.png
new file mode 100644
index 00000000000..f3bcf5663ef
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bw.png differ
diff --git a/openerp/addons/base/static/img/country_flags/by.png b/openerp/addons/base/static/img/country_flags/by.png
new file mode 100644
index 00000000000..fe37c575ac4
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/by.png differ
diff --git a/openerp/addons/base/static/img/country_flags/bz.png b/openerp/addons/base/static/img/country_flags/bz.png
new file mode 100644
index 00000000000..bbb9903a237
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/bz.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ca.png b/openerp/addons/base/static/img/country_flags/ca.png
new file mode 100644
index 00000000000..12807f498ec
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ca.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cc.png b/openerp/addons/base/static/img/country_flags/cc.png
new file mode 100644
index 00000000000..84ccc4ecaeb
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cc.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cd.png b/openerp/addons/base/static/img/country_flags/cd.png
new file mode 100644
index 00000000000..e96bce99ea7
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cd.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cf.png b/openerp/addons/base/static/img/country_flags/cf.png
new file mode 100644
index 00000000000..366d4cb55e5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cf.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cg.png b/openerp/addons/base/static/img/country_flags/cg.png
new file mode 100644
index 00000000000..e049d47c408
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ch.png b/openerp/addons/base/static/img/country_flags/ch.png
new file mode 100644
index 00000000000..815b6664b49
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ch.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ci.png b/openerp/addons/base/static/img/country_flags/ci.png
new file mode 100644
index 00000000000..61e92e63a9d
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ci.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ck.png b/openerp/addons/base/static/img/country_flags/ck.png
new file mode 100644
index 00000000000..fb487762817
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ck.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cl.png b/openerp/addons/base/static/img/country_flags/cl.png
new file mode 100644
index 00000000000..5ad2daaf25c
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cl.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cm.png b/openerp/addons/base/static/img/country_flags/cm.png
new file mode 100644
index 00000000000..80a639ce7dc
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cn.png b/openerp/addons/base/static/img/country_flags/cn.png
new file mode 100644
index 00000000000..5b28787b636
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/co.png b/openerp/addons/base/static/img/country_flags/co.png
new file mode 100644
index 00000000000..53609723452
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/co.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cr.png b/openerp/addons/base/static/img/country_flags/cr.png
new file mode 100644
index 00000000000..40ee573607b
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cu.png b/openerp/addons/base/static/img/country_flags/cu.png
new file mode 100644
index 00000000000..31c2bf7dd0c
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cu.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cv.png b/openerp/addons/base/static/img/country_flags/cv.png
new file mode 100644
index 00000000000..d4fd1c54552
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cv.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cw.png b/openerp/addons/base/static/img/country_flags/cw.png
new file mode 100644
index 00000000000..71fb770b24c
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cw.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cx.png b/openerp/addons/base/static/img/country_flags/cx.png
new file mode 100644
index 00000000000..d093d2e8b3b
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cx.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cy.png b/openerp/addons/base/static/img/country_flags/cy.png
new file mode 100644
index 00000000000..abbf15ce0e3
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cy.png differ
diff --git a/openerp/addons/base/static/img/country_flags/cz.png b/openerp/addons/base/static/img/country_flags/cz.png
new file mode 100644
index 00000000000..bd3cacfe4bb
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/cz.png differ
diff --git a/openerp/addons/base/static/img/country_flags/de.png b/openerp/addons/base/static/img/country_flags/de.png
new file mode 100644
index 00000000000..a479fee6662
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/de.png differ
diff --git a/openerp/addons/base/static/img/country_flags/dj.png b/openerp/addons/base/static/img/country_flags/dj.png
new file mode 100644
index 00000000000..9ac6493eff3
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/dj.png differ
diff --git a/openerp/addons/base/static/img/country_flags/dk.png b/openerp/addons/base/static/img/country_flags/dk.png
new file mode 100644
index 00000000000..d2882e82a16
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/dk.png differ
diff --git a/openerp/addons/base/static/img/country_flags/dm.png b/openerp/addons/base/static/img/country_flags/dm.png
new file mode 100644
index 00000000000..f70973854b4
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/dm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/do.png b/openerp/addons/base/static/img/country_flags/do.png
new file mode 100644
index 00000000000..24614878d56
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/do.png differ
diff --git a/openerp/addons/base/static/img/country_flags/dz.png b/openerp/addons/base/static/img/country_flags/dz.png
new file mode 100644
index 00000000000..86c293aebef
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/dz.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ec.png b/openerp/addons/base/static/img/country_flags/ec.png
new file mode 100644
index 00000000000..15ba2c3f2e9
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ec.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ee.png b/openerp/addons/base/static/img/country_flags/ee.png
new file mode 100644
index 00000000000..fbf210ef42e
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ee.png differ
diff --git a/openerp/addons/base/static/img/country_flags/eg.png b/openerp/addons/base/static/img/country_flags/eg.png
new file mode 100644
index 00000000000..0d8132a03a9
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/eg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/er.png b/openerp/addons/base/static/img/country_flags/er.png
new file mode 100644
index 00000000000..c89d5a95644
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/er.png differ
diff --git a/openerp/addons/base/static/img/country_flags/es.png b/openerp/addons/base/static/img/country_flags/es.png
new file mode 100644
index 00000000000..ea384d4d516
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/es.png differ
diff --git a/openerp/addons/base/static/img/country_flags/et.png b/openerp/addons/base/static/img/country_flags/et.png
new file mode 100644
index 00000000000..d2d1e4255ee
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/et.png differ
diff --git a/openerp/addons/base/static/img/country_flags/fi.png b/openerp/addons/base/static/img/country_flags/fi.png
new file mode 100644
index 00000000000..195226a5042
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/fi.png differ
diff --git a/openerp/addons/base/static/img/country_flags/fj.png b/openerp/addons/base/static/img/country_flags/fj.png
new file mode 100644
index 00000000000..daee24a3746
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/fj.png differ
diff --git a/openerp/addons/base/static/img/country_flags/fk.png b/openerp/addons/base/static/img/country_flags/fk.png
new file mode 100644
index 00000000000..f202be3118f
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/fk.png differ
diff --git a/openerp/addons/base/static/img/country_flags/fm.png b/openerp/addons/base/static/img/country_flags/fm.png
new file mode 100644
index 00000000000..c157b518251
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/fm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/fo.png b/openerp/addons/base/static/img/country_flags/fo.png
new file mode 100644
index 00000000000..eceef2d8d73
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/fo.png differ
diff --git a/openerp/addons/base/static/img/country_flags/fr.png b/openerp/addons/base/static/img/country_flags/fr.png
new file mode 100644
index 00000000000..8fdf4c45717
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/fr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ga.png b/openerp/addons/base/static/img/country_flags/ga.png
new file mode 100644
index 00000000000..d4a2b71f2d5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ga.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gb.png b/openerp/addons/base/static/img/country_flags/gb.png
new file mode 100644
index 00000000000..bf90e9b8b9c
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gb.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gd.png b/openerp/addons/base/static/img/country_flags/gd.png
new file mode 100644
index 00000000000..807755f284f
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gd.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ge.png b/openerp/addons/base/static/img/country_flags/ge.png
new file mode 100644
index 00000000000..6e813b3b310
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ge.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gg.png b/openerp/addons/base/static/img/country_flags/gg.png
new file mode 100644
index 00000000000..20f3b15c0fd
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gh.png b/openerp/addons/base/static/img/country_flags/gh.png
new file mode 100644
index 00000000000..5eb077085c7
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gh.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gi.png b/openerp/addons/base/static/img/country_flags/gi.png
new file mode 100644
index 00000000000..79dbd3333d5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gi.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gl.png b/openerp/addons/base/static/img/country_flags/gl.png
new file mode 100644
index 00000000000..f7e82893f1a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gl.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gm.png b/openerp/addons/base/static/img/country_flags/gm.png
new file mode 100644
index 00000000000..8c539e2d23c
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gn.png b/openerp/addons/base/static/img/country_flags/gn.png
new file mode 100644
index 00000000000..c0d4c72c87a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gq.png b/openerp/addons/base/static/img/country_flags/gq.png
new file mode 100644
index 00000000000..375a7b78f62
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gq.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gr.png b/openerp/addons/base/static/img/country_flags/gr.png
new file mode 100644
index 00000000000..7c4471b86e2
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gs.png b/openerp/addons/base/static/img/country_flags/gs.png
new file mode 100644
index 00000000000..b0269d14acf
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gs.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gt.png b/openerp/addons/base/static/img/country_flags/gt.png
new file mode 100644
index 00000000000..6d6998c4c6c
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gt.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gu.png b/openerp/addons/base/static/img/country_flags/gu.png
new file mode 100644
index 00000000000..847d59520f5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gu.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gw.png b/openerp/addons/base/static/img/country_flags/gw.png
new file mode 100644
index 00000000000..88ba1d8a531
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gw.png differ
diff --git a/openerp/addons/base/static/img/country_flags/gy.png b/openerp/addons/base/static/img/country_flags/gy.png
new file mode 100644
index 00000000000..de7bef45285
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/gy.png differ
diff --git a/openerp/addons/base/static/img/country_flags/hk.png b/openerp/addons/base/static/img/country_flags/hk.png
new file mode 100644
index 00000000000..62f6453b8b7
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/hk.png differ
diff --git a/openerp/addons/base/static/img/country_flags/hn.png b/openerp/addons/base/static/img/country_flags/hn.png
new file mode 100644
index 00000000000..e1201738ca7
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/hn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/hr.png b/openerp/addons/base/static/img/country_flags/hr.png
new file mode 100644
index 00000000000..5e24e9a52d4
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/hr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ht.png b/openerp/addons/base/static/img/country_flags/ht.png
new file mode 100644
index 00000000000..58ec58e41be
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ht.png differ
diff --git a/openerp/addons/base/static/img/country_flags/hu.png b/openerp/addons/base/static/img/country_flags/hu.png
new file mode 100644
index 00000000000..e1f237f802d
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/hu.png differ
diff --git a/openerp/addons/base/static/img/country_flags/id.png b/openerp/addons/base/static/img/country_flags/id.png
new file mode 100644
index 00000000000..1dc8207c288
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/id.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ie.png b/openerp/addons/base/static/img/country_flags/ie.png
new file mode 100644
index 00000000000..fb8cf9382f1
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ie.png differ
diff --git a/openerp/addons/base/static/img/country_flags/il.png b/openerp/addons/base/static/img/country_flags/il.png
new file mode 100644
index 00000000000..c4c4f21e128
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/il.png differ
diff --git a/openerp/addons/base/static/img/country_flags/im.png b/openerp/addons/base/static/img/country_flags/im.png
new file mode 100644
index 00000000000..6befe9efe3a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/im.png differ
diff --git a/openerp/addons/base/static/img/country_flags/in.png b/openerp/addons/base/static/img/country_flags/in.png
new file mode 100644
index 00000000000..a0daccd9397
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/in.png differ
diff --git a/openerp/addons/base/static/img/country_flags/io.png b/openerp/addons/base/static/img/country_flags/io.png
new file mode 100644
index 00000000000..a112651851b
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/io.png differ
diff --git a/openerp/addons/base/static/img/country_flags/iq.png b/openerp/addons/base/static/img/country_flags/iq.png
new file mode 100644
index 00000000000..1095c9d312a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/iq.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ir.png b/openerp/addons/base/static/img/country_flags/ir.png
new file mode 100644
index 00000000000..8b9c5fac4d0
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ir.png differ
diff --git a/openerp/addons/base/static/img/country_flags/iran.png b/openerp/addons/base/static/img/country_flags/iran.png
new file mode 100644
index 00000000000..9ca5513ace0
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/iran.png differ
diff --git a/openerp/addons/base/static/img/country_flags/is.png b/openerp/addons/base/static/img/country_flags/is.png
new file mode 100644
index 00000000000..c5d0ffb1ec3
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/is.png differ
diff --git a/openerp/addons/base/static/img/country_flags/it.png b/openerp/addons/base/static/img/country_flags/it.png
new file mode 100644
index 00000000000..1cd74ec0ee0
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/it.png differ
diff --git a/openerp/addons/base/static/img/country_flags/je.png b/openerp/addons/base/static/img/country_flags/je.png
new file mode 100644
index 00000000000..ca62fe471e3
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/je.png differ
diff --git a/openerp/addons/base/static/img/country_flags/jm.png b/openerp/addons/base/static/img/country_flags/jm.png
new file mode 100644
index 00000000000..7458372eea8
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/jm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/jo.png b/openerp/addons/base/static/img/country_flags/jo.png
new file mode 100644
index 00000000000..b5e88a3c251
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/jo.png differ
diff --git a/openerp/addons/base/static/img/country_flags/jp.png b/openerp/addons/base/static/img/country_flags/jp.png
new file mode 100644
index 00000000000..21f38c35847
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/jp.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ke.png b/openerp/addons/base/static/img/country_flags/ke.png
new file mode 100644
index 00000000000..57ef9e3f20b
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ke.png differ
diff --git a/openerp/addons/base/static/img/country_flags/kg.png b/openerp/addons/base/static/img/country_flags/kg.png
new file mode 100644
index 00000000000..1b550f6cfaa
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/kg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/kh.png b/openerp/addons/base/static/img/country_flags/kh.png
new file mode 100644
index 00000000000..299ad93e08e
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/kh.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ki.png b/openerp/addons/base/static/img/country_flags/ki.png
new file mode 100644
index 00000000000..cf730409013
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ki.png differ
diff --git a/openerp/addons/base/static/img/country_flags/km.png b/openerp/addons/base/static/img/country_flags/km.png
new file mode 100644
index 00000000000..df5c343fce5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/km.png differ
diff --git a/openerp/addons/base/static/img/country_flags/kn.png b/openerp/addons/base/static/img/country_flags/kn.png
new file mode 100644
index 00000000000..9ecfa0ab9c4
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/kn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/kp.png b/openerp/addons/base/static/img/country_flags/kp.png
new file mode 100644
index 00000000000..14db96ff065
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/kp.png differ
diff --git a/openerp/addons/base/static/img/country_flags/kr.png b/openerp/addons/base/static/img/country_flags/kr.png
new file mode 100644
index 00000000000..8b66b25f75d
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/kr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/kw.png b/openerp/addons/base/static/img/country_flags/kw.png
new file mode 100644
index 00000000000..33c04a64f70
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/kw.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ky.png b/openerp/addons/base/static/img/country_flags/ky.png
new file mode 100644
index 00000000000..84279c83bcf
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ky.png differ
diff --git a/openerp/addons/base/static/img/country_flags/kz.png b/openerp/addons/base/static/img/country_flags/kz.png
new file mode 100644
index 00000000000..f756682c74f
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/kz.png differ
diff --git a/openerp/addons/base/static/img/country_flags/la.png b/openerp/addons/base/static/img/country_flags/la.png
new file mode 100644
index 00000000000..27d4e2be994
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/la.png differ
diff --git a/openerp/addons/base/static/img/country_flags/lb.png b/openerp/addons/base/static/img/country_flags/lb.png
new file mode 100644
index 00000000000..8df863b7ca1
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/lb.png differ
diff --git a/openerp/addons/base/static/img/country_flags/lc.png b/openerp/addons/base/static/img/country_flags/lc.png
new file mode 100644
index 00000000000..15af8fee7ff
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/lc.png differ
diff --git a/openerp/addons/base/static/img/country_flags/li.png b/openerp/addons/base/static/img/country_flags/li.png
new file mode 100644
index 00000000000..f86197e9af6
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/li.png differ
diff --git a/openerp/addons/base/static/img/country_flags/lk.png b/openerp/addons/base/static/img/country_flags/lk.png
new file mode 100644
index 00000000000..46bab54aabc
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/lk.png differ
diff --git a/openerp/addons/base/static/img/country_flags/lr.png b/openerp/addons/base/static/img/country_flags/lr.png
new file mode 100644
index 00000000000..3f2cc830120
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/lr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ls.png b/openerp/addons/base/static/img/country_flags/ls.png
new file mode 100644
index 00000000000..6b6d6b17531
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ls.png differ
diff --git a/openerp/addons/base/static/img/country_flags/lt.png b/openerp/addons/base/static/img/country_flags/lt.png
new file mode 100644
index 00000000000..a25f6a31aae
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/lt.png differ
diff --git a/openerp/addons/base/static/img/country_flags/lu.png b/openerp/addons/base/static/img/country_flags/lu.png
new file mode 100644
index 00000000000..2899c7bbba1
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/lu.png differ
diff --git a/openerp/addons/base/static/img/country_flags/lv.png b/openerp/addons/base/static/img/country_flags/lv.png
new file mode 100644
index 00000000000..674a7c6c9b0
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/lv.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ly.png b/openerp/addons/base/static/img/country_flags/ly.png
new file mode 100644
index 00000000000..0e4eb2a5627
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ly.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ma.png b/openerp/addons/base/static/img/country_flags/ma.png
new file mode 100644
index 00000000000..b43ea8c8597
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ma.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mc.png b/openerp/addons/base/static/img/country_flags/mc.png
new file mode 100644
index 00000000000..119460a5e7f
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mc.png differ
diff --git a/openerp/addons/base/static/img/country_flags/md.png b/openerp/addons/base/static/img/country_flags/md.png
new file mode 100644
index 00000000000..a05ba191843
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/md.png differ
diff --git a/openerp/addons/base/static/img/country_flags/me.png b/openerp/addons/base/static/img/country_flags/me.png
new file mode 100644
index 00000000000..aead438a92f
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/me.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mg.png b/openerp/addons/base/static/img/country_flags/mg.png
new file mode 100644
index 00000000000..75242401c76
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mh.png b/openerp/addons/base/static/img/country_flags/mh.png
new file mode 100644
index 00000000000..4cd0d244a49
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mh.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mk.png b/openerp/addons/base/static/img/country_flags/mk.png
new file mode 100644
index 00000000000..f313b092e4c
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mk.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ml.png b/openerp/addons/base/static/img/country_flags/ml.png
new file mode 100644
index 00000000000..eb5579c08d5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ml.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mm.png b/openerp/addons/base/static/img/country_flags/mm.png
new file mode 100644
index 00000000000..6723bbb3b03
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mn.png b/openerp/addons/base/static/img/country_flags/mn.png
new file mode 100644
index 00000000000..607caac7659
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mo.png b/openerp/addons/base/static/img/country_flags/mo.png
new file mode 100644
index 00000000000..f64c674be59
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mo.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mp.png b/openerp/addons/base/static/img/country_flags/mp.png
new file mode 100644
index 00000000000..3453b270cc0
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mp.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mq.png b/openerp/addons/base/static/img/country_flags/mq.png
new file mode 100644
index 00000000000..54ba3e92b83
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mq.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mr.png b/openerp/addons/base/static/img/country_flags/mr.png
new file mode 100644
index 00000000000..76b032b47b8
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ms.png b/openerp/addons/base/static/img/country_flags/ms.png
new file mode 100644
index 00000000000..4f291816856
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ms.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mt.png b/openerp/addons/base/static/img/country_flags/mt.png
new file mode 100644
index 00000000000..dbdcbf9d503
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mt.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mu.png b/openerp/addons/base/static/img/country_flags/mu.png
new file mode 100644
index 00000000000..6eae60e796b
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mu.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mv.png b/openerp/addons/base/static/img/country_flags/mv.png
new file mode 100644
index 00000000000..6d2597d66b1
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mv.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mw.png b/openerp/addons/base/static/img/country_flags/mw.png
new file mode 100644
index 00000000000..f2f3454334b
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mw.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mx.png b/openerp/addons/base/static/img/country_flags/mx.png
new file mode 100644
index 00000000000..4c5c32d0589
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mx.png differ
diff --git a/openerp/addons/base/static/img/country_flags/my.png b/openerp/addons/base/static/img/country_flags/my.png
new file mode 100644
index 00000000000..7ee43cc1925
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/my.png differ
diff --git a/openerp/addons/base/static/img/country_flags/mz.png b/openerp/addons/base/static/img/country_flags/mz.png
new file mode 100644
index 00000000000..4d47b29b02b
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/mz.png differ
diff --git a/openerp/addons/base/static/img/country_flags/na.png b/openerp/addons/base/static/img/country_flags/na.png
new file mode 100644
index 00000000000..0e09fd2f12a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/na.png differ
diff --git a/openerp/addons/base/static/img/country_flags/nc.png b/openerp/addons/base/static/img/country_flags/nc.png
new file mode 100644
index 00000000000..5e9b0ca9f73
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/nc.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ne.png b/openerp/addons/base/static/img/country_flags/ne.png
new file mode 100644
index 00000000000..e96acdedbc0
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ne.png differ
diff --git a/openerp/addons/base/static/img/country_flags/nf.png b/openerp/addons/base/static/img/country_flags/nf.png
new file mode 100644
index 00000000000..2bb62238166
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/nf.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ng.png b/openerp/addons/base/static/img/country_flags/ng.png
new file mode 100644
index 00000000000..052ec0e9761
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ng.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ni.png b/openerp/addons/base/static/img/country_flags/ni.png
new file mode 100644
index 00000000000..4e455f6777e
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ni.png differ
diff --git a/openerp/addons/base/static/img/country_flags/nl.png b/openerp/addons/base/static/img/country_flags/nl.png
new file mode 100644
index 00000000000..1b5938306f5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/nl.png differ
diff --git a/openerp/addons/base/static/img/country_flags/no.png b/openerp/addons/base/static/img/country_flags/no.png
new file mode 100644
index 00000000000..bd2840d96c1
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/no.png differ
diff --git a/openerp/addons/base/static/img/country_flags/np.png b/openerp/addons/base/static/img/country_flags/np.png
new file mode 100644
index 00000000000..4c4beda56fe
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/np.png differ
diff --git a/openerp/addons/base/static/img/country_flags/nr.png b/openerp/addons/base/static/img/country_flags/nr.png
new file mode 100644
index 00000000000..923bea03faf
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/nr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/nu.png b/openerp/addons/base/static/img/country_flags/nu.png
new file mode 100644
index 00000000000..1f745218efa
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/nu.png differ
diff --git a/openerp/addons/base/static/img/country_flags/nz.png b/openerp/addons/base/static/img/country_flags/nz.png
new file mode 100644
index 00000000000..6d669ae696e
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/nz.png differ
diff --git a/openerp/addons/base/static/img/country_flags/om.png b/openerp/addons/base/static/img/country_flags/om.png
new file mode 100644
index 00000000000..5c61b15a0a5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/om.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pa.png b/openerp/addons/base/static/img/country_flags/pa.png
new file mode 100644
index 00000000000..b4857d13574
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pa.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pe.png b/openerp/addons/base/static/img/country_flags/pe.png
new file mode 100644
index 00000000000..14f7e1c86d3
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pe.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pf.png b/openerp/addons/base/static/img/country_flags/pf.png
new file mode 100644
index 00000000000..db134fc81c4
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pf.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pg.png b/openerp/addons/base/static/img/country_flags/pg.png
new file mode 100644
index 00000000000..56e85161956
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ph.png b/openerp/addons/base/static/img/country_flags/ph.png
new file mode 100644
index 00000000000..e74d917eb3d
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ph.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pk.png b/openerp/addons/base/static/img/country_flags/pk.png
new file mode 100644
index 00000000000..adea341ccec
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pk.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pl.png b/openerp/addons/base/static/img/country_flags/pl.png
new file mode 100644
index 00000000000..4d0f9d19f65
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pl.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pm.png b/openerp/addons/base/static/img/country_flags/pm.png
new file mode 100644
index 00000000000..b34299ce47b
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pn.png b/openerp/addons/base/static/img/country_flags/pn.png
new file mode 100644
index 00000000000..890c9593342
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pr.png b/openerp/addons/base/static/img/country_flags/pr.png
new file mode 100644
index 00000000000..5e2c7c6078a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pt.png b/openerp/addons/base/static/img/country_flags/pt.png
new file mode 100644
index 00000000000..5fe2ae2cf63
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pt.png differ
diff --git a/openerp/addons/base/static/img/country_flags/pw.png b/openerp/addons/base/static/img/country_flags/pw.png
new file mode 100644
index 00000000000..fe8ef26d2e6
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/pw.png differ
diff --git a/openerp/addons/base/static/img/country_flags/py.png b/openerp/addons/base/static/img/country_flags/py.png
new file mode 100644
index 00000000000..0a04748f182
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/py.png differ
diff --git a/openerp/addons/base/static/img/country_flags/qa.png b/openerp/addons/base/static/img/country_flags/qa.png
new file mode 100644
index 00000000000..35c44fc076d
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/qa.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ro.png b/openerp/addons/base/static/img/country_flags/ro.png
new file mode 100644
index 00000000000..0cb3f29f11d
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ro.png differ
diff --git a/openerp/addons/base/static/img/country_flags/rs.png b/openerp/addons/base/static/img/country_flags/rs.png
new file mode 100644
index 00000000000..6f7d83de2e5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/rs.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ru.png b/openerp/addons/base/static/img/country_flags/ru.png
new file mode 100644
index 00000000000..1a99c506234
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ru.png differ
diff --git a/openerp/addons/base/static/img/country_flags/rw.png b/openerp/addons/base/static/img/country_flags/rw.png
new file mode 100644
index 00000000000..2abba014617
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/rw.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sa.png b/openerp/addons/base/static/img/country_flags/sa.png
new file mode 100644
index 00000000000..6c106a540a1
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sa.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sb.png b/openerp/addons/base/static/img/country_flags/sb.png
new file mode 100644
index 00000000000..a23c5bc58ba
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sb.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sc.png b/openerp/addons/base/static/img/country_flags/sc.png
new file mode 100644
index 00000000000..d1acc28403e
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sc.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sd.png b/openerp/addons/base/static/img/country_flags/sd.png
new file mode 100644
index 00000000000..e7694590811
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sd.png differ
diff --git a/openerp/addons/base/static/img/country_flags/se.png b/openerp/addons/base/static/img/country_flags/se.png
new file mode 100644
index 00000000000..a30498df565
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/se.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sg.png b/openerp/addons/base/static/img/country_flags/sg.png
new file mode 100644
index 00000000000..16bb5445c58
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sh.png b/openerp/addons/base/static/img/country_flags/sh.png
new file mode 100644
index 00000000000..cb48a6a2e2f
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sh.png differ
diff --git a/openerp/addons/base/static/img/country_flags/si.png b/openerp/addons/base/static/img/country_flags/si.png
new file mode 100644
index 00000000000..b749349245d
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/si.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sk.png b/openerp/addons/base/static/img/country_flags/sk.png
new file mode 100644
index 00000000000..1609f2c8d9a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sk.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sl.png b/openerp/addons/base/static/img/country_flags/sl.png
new file mode 100644
index 00000000000..ef67f96c22a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sl.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sm.png b/openerp/addons/base/static/img/country_flags/sm.png
new file mode 100644
index 00000000000..b515c509030
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sn.png b/openerp/addons/base/static/img/country_flags/sn.png
new file mode 100644
index 00000000000..2cb1d0a36c3
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/so.png b/openerp/addons/base/static/img/country_flags/so.png
new file mode 100644
index 00000000000..d6772bdd702
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/so.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sr.png b/openerp/addons/base/static/img/country_flags/sr.png
new file mode 100644
index 00000000000..9be5dc7b952
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ss.png b/openerp/addons/base/static/img/country_flags/ss.png
new file mode 100644
index 00000000000..a07a513e189
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ss.png differ
diff --git a/openerp/addons/base/static/img/country_flags/st.png b/openerp/addons/base/static/img/country_flags/st.png
new file mode 100644
index 00000000000..b3abd9852f3
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/st.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sv.png b/openerp/addons/base/static/img/country_flags/sv.png
new file mode 100644
index 00000000000..4685c62e305
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sv.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sx.png b/openerp/addons/base/static/img/country_flags/sx.png
new file mode 100644
index 00000000000..d8d40f75924
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sx.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sy.png b/openerp/addons/base/static/img/country_flags/sy.png
new file mode 100644
index 00000000000..a45853957cc
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sy.png differ
diff --git a/openerp/addons/base/static/img/country_flags/sz.png b/openerp/addons/base/static/img/country_flags/sz.png
new file mode 100644
index 00000000000..49b2a9360c1
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/sz.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tc.png b/openerp/addons/base/static/img/country_flags/tc.png
new file mode 100644
index 00000000000..078b226810d
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tc.png differ
diff --git a/openerp/addons/base/static/img/country_flags/td.png b/openerp/addons/base/static/img/country_flags/td.png
new file mode 100644
index 00000000000..2e8603a6012
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/td.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tf.png b/openerp/addons/base/static/img/country_flags/tf.png
new file mode 100644
index 00000000000..945c7afb0b1
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tf.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tg.png b/openerp/addons/base/static/img/country_flags/tg.png
new file mode 100644
index 00000000000..3154c5cb830
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/th.png b/openerp/addons/base/static/img/country_flags/th.png
new file mode 100644
index 00000000000..f39ce45eeee
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/th.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tj.png b/openerp/addons/base/static/img/country_flags/tj.png
new file mode 100644
index 00000000000..15d59605186
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tj.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tk.png b/openerp/addons/base/static/img/country_flags/tk.png
new file mode 100644
index 00000000000..b191458e187
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tk.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tl.png b/openerp/addons/base/static/img/country_flags/tl.png
new file mode 100644
index 00000000000..0031a4e41f9
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tl.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tm.png b/openerp/addons/base/static/img/country_flags/tm.png
new file mode 100644
index 00000000000..6699276a8ed
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tn.png b/openerp/addons/base/static/img/country_flags/tn.png
new file mode 100644
index 00000000000..04587942bb7
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/to.png b/openerp/addons/base/static/img/country_flags/to.png
new file mode 100644
index 00000000000..51a1556d5e9
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/to.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tp.png b/openerp/addons/base/static/img/country_flags/tp.png
new file mode 100644
index 00000000000..2ec85c15394
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tp.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tr.png b/openerp/addons/base/static/img/country_flags/tr.png
new file mode 100644
index 00000000000..ebf2a31e807
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tr.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tt.png b/openerp/addons/base/static/img/country_flags/tt.png
new file mode 100644
index 00000000000..e49c3682a50
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tt.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tv.png b/openerp/addons/base/static/img/country_flags/tv.png
new file mode 100644
index 00000000000..ab10c93d5d7
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tv.png differ
diff --git a/openerp/addons/base/static/img/country_flags/tz.png b/openerp/addons/base/static/img/country_flags/tz.png
new file mode 100644
index 00000000000..d4e99a62df7
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/tz.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ua.png b/openerp/addons/base/static/img/country_flags/ua.png
new file mode 100644
index 00000000000..57cf957010d
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ua.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ug.png b/openerp/addons/base/static/img/country_flags/ug.png
new file mode 100644
index 00000000000..908784213f0
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ug.png differ
diff --git a/openerp/addons/base/static/img/country_flags/us.png b/openerp/addons/base/static/img/country_flags/us.png
new file mode 100644
index 00000000000..801ef4e8abb
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/us.png differ
diff --git a/openerp/addons/base/static/img/country_flags/uy.png b/openerp/addons/base/static/img/country_flags/uy.png
new file mode 100644
index 00000000000..7af80710a3f
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/uy.png differ
diff --git a/openerp/addons/base/static/img/country_flags/uz.png b/openerp/addons/base/static/img/country_flags/uz.png
new file mode 100644
index 00000000000..54840bd935c
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/uz.png differ
diff --git a/openerp/addons/base/static/img/country_flags/va.png b/openerp/addons/base/static/img/country_flags/va.png
new file mode 100644
index 00000000000..28933fb7e04
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/va.png differ
diff --git a/openerp/addons/base/static/img/country_flags/vc.png b/openerp/addons/base/static/img/country_flags/vc.png
new file mode 100644
index 00000000000..62b9091dc9a
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/vc.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ve.png b/openerp/addons/base/static/img/country_flags/ve.png
new file mode 100644
index 00000000000..7ab81205f84
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ve.png differ
diff --git a/openerp/addons/base/static/img/country_flags/vg.png b/openerp/addons/base/static/img/country_flags/vg.png
new file mode 100644
index 00000000000..9a33f8cf9e6
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/vg.png differ
diff --git a/openerp/addons/base/static/img/country_flags/vi.png b/openerp/addons/base/static/img/country_flags/vi.png
new file mode 100644
index 00000000000..b6361f49e57
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/vi.png differ
diff --git a/openerp/addons/base/static/img/country_flags/vn.png b/openerp/addons/base/static/img/country_flags/vn.png
new file mode 100644
index 00000000000..25753977ec5
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/vn.png differ
diff --git a/openerp/addons/base/static/img/country_flags/vu.png b/openerp/addons/base/static/img/country_flags/vu.png
new file mode 100644
index 00000000000..124c7cf7b56
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/vu.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ws.png b/openerp/addons/base/static/img/country_flags/ws.png
new file mode 100644
index 00000000000..787354394c8
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ws.png differ
diff --git a/openerp/addons/base/static/img/country_flags/ye.png b/openerp/addons/base/static/img/country_flags/ye.png
new file mode 100644
index 00000000000..07c4c8ef1fb
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/ye.png differ
diff --git a/openerp/addons/base/static/img/country_flags/za.png b/openerp/addons/base/static/img/country_flags/za.png
new file mode 100644
index 00000000000..dc4fb4df547
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/za.png differ
diff --git a/openerp/addons/base/static/img/country_flags/zm.png b/openerp/addons/base/static/img/country_flags/zm.png
new file mode 100644
index 00000000000..6275c83148e
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/zm.png differ
diff --git a/openerp/addons/base/static/img/country_flags/zw.png b/openerp/addons/base/static/img/country_flags/zw.png
new file mode 100644
index 00000000000..de98aa7f4d8
Binary files /dev/null and b/openerp/addons/base/static/img/country_flags/zw.png differ
diff --git a/openerp/addons/base/static/src/js/apps.js b/openerp/addons/base/static/src/js/apps.js
index e339ed2230e..115d35fcc4f 100644
--- a/openerp/addons/base/static/src/js/apps.js
+++ b/openerp/addons/base/static/src/js/apps.js
@@ -63,8 +63,8 @@ openerp.base = function(instance) {
if (instance.base.apps_client) {
return check_client_available(instance.base.apps_client);
} else {
- var ICP = new instance.web.Model('ir.config_parameter');
- return ICP.call('get_param', ['apps.server', 'https://apps.openerp.com/apps']).then(function(u) {
+ var Mod = new instance.web.Model('ir.module.module');
+ return Mod.call('get_apps_server').then(function(u) {
var link = $(_.str.sprintf('', u))[0];
var host = _.str.sprintf('%s//%s', link.protocol, link.host);
var dbname = link.pathname;
diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py
index 6358dffa45b..2e55989ba19 100644
--- a/openerp/addons/base/tests/__init__.py
+++ b/openerp/addons/base/tests/__init__.py
@@ -20,7 +20,6 @@ import test_res_config
import test_res_lang
import test_search
import test_translate
-import test_ui
#import test_uninstall
import test_view_validation
import test_views
diff --git a/openerp/addons/base/tests/test_base.py b/openerp/addons/base/tests/test_base.py
index 20382737529..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,18 +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'},
- ]:
- 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'")
+
+ # 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')
+
class test_partner_recursion(common.TransactionCase):
@@ -375,6 +457,59 @@ class test_translation(common.TransactionCase):
fr_context_cat = self.res_category.browse(cr, uid, self.new_fr_cat_id, context={'lang':'fr_FR'})
self.assertEqual(fr_context_cat.name, 'Clients (copie)', "Did not used default value for translated value")
+test_state = None
+#: Stores state information across multiple test classes
+def setUpModule():
+ global test_state
+ test_state = {}
+def tearDownModule():
+ global test_state
+ test_state = None
+
+class TestPhaseInstall00(unittest2.TestCase):
+ """
+ WARNING: Relies on tests being run in alphabetical order
+ """
+ @classmethod
+ def setUpClass(cls):
+ cls.state = None
+
+ def test_00_setup(self):
+ type(self).state = 'init'
+
+ @common.at_install(False)
+ def test_01_no_install(self):
+ type(self).state = 'error'
+
+ def test_02_check(self):
+ self.assertEqual(
+ self.state, 'init',
+ "Testcase state should not have been transitioned from 00")
+
+class TestPhaseInstall01(unittest2.TestCase):
+ at_install = False
+
+ def test_default_norun(self):
+ self.fail("An unmarket test in a non-at-install case should not run")
+
+ @common.at_install(True)
+ def test_set_run(self):
+ test_state['set_at_install'] = True
+
+class TestPhaseInstall02(unittest2.TestCase):
+ """
+ Can't put the check for test_set_run in the same class: if
+ @common.at_install does not work for test_set_run, it won't work for
+ the other one either. Thus move checking of whether test_set_run has
+ correctly run indeed to a separate class.
+
+ Warning: relies on *classes* being run in alphabetical order in test
+ modules
+ """
+ def test_check_state(self):
+ self.assertTrue(
+ test_state.get('set_at_install'),
+ "The flag should be set if local overriding of runstate")
if __name__ == '__main__':
unittest2.main()
diff --git a/openerp/addons/base/tests/test_db_cursor.py b/openerp/addons/base/tests/test_db_cursor.py
index b3ce36467bb..d60f38185b9 100644
--- a/openerp/addons/base/tests/test_db_cursor.py
+++ b/openerp/addons/base/tests/test_db_cursor.py
@@ -21,7 +21,7 @@ class test_cr_execute(unittest2.TestCase):
"""
Try to use iterable but non-list or int params in query parameters.
"""
- with registry().cursor(auto_commit=False) as cr:
+ with registry().cursor() as cr:
with self.assertRaises(ValueError):
cr.execute("SELECT id FROM res_users WHERE login=%s", 'admin')
with self.assertRaises(ValueError):
diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py
index a05009b0252..de7e0472479 100644
--- a/openerp/addons/base/tests/test_expression.py
+++ b/openerp/addons/base/tests/test_expression.py
@@ -1,6 +1,7 @@
import unittest2
import openerp
+from openerp.osv.expression import get_unaccent_wrapper
from openerp.osv.orm import BaseModel
import openerp.tests.common as common
@@ -124,6 +125,7 @@ class test_expression(common.TransactionCase):
def test_20_auto_join(self):
registry, cr, uid = self.registry, self.cr, self.uid
+ unaccent = get_unaccent_wrapper(cr)
# Get models
partner_obj = registry('res.partner')
@@ -180,8 +182,11 @@ class test_expression(common.TransactionCase):
sql_query = self.query_list[0].get_sql()
self.assertIn('res_partner_bank', sql_query[0],
"_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect main table")
- self.assertIn('"res_partner_bank"."name" like %s', sql_query[1],
+
+ expected = "%s::text like %s" % (unaccent('"res_partner_bank"."name"'), unaccent('%s'))
+ self.assertIn(expected, sql_query[1],
"_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect where condition")
+
self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]),
"_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect parameter")
sql_query = self.query_list[2].get_sql()
@@ -217,8 +222,11 @@ class test_expression(common.TransactionCase):
"_auto_join on: ('bank_ids.name', 'like', '..') query incorrect main table")
self.assertIn('"res_partner_bank" as "res_partner__bank_ids"', sql_query[0],
"_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join")
- self.assertIn('"res_partner__bank_ids"."name" like %s', sql_query[1],
+
+ expected = "%s::text like %s" % (unaccent('"res_partner__bank_ids"."name"'), unaccent('%s'))
+ self.assertIn(expected, sql_query[1],
"_auto_join on: ('bank_ids.name', 'like', '..') query incorrect where condition")
+
self.assertIn('"res_partner"."id"="res_partner__bank_ids"."partner_id"', sql_query[1],
"_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join condition")
self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]),
@@ -296,8 +304,11 @@ class test_expression(common.TransactionCase):
sql_query = self.query_list[0].get_sql()
self.assertIn('"res_country"', sql_query[0],
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table")
- self.assertIn('"res_country"."code" like %s', sql_query[1],
+
+ expected = "%s::text like %s" % (unaccent('"res_country"."code"'), unaccent('%s'))
+ self.assertIn(expected, sql_query[1],
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition")
+
self.assertEqual(['%' + name_test + '%'], sql_query[2],
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect parameter")
sql_query = self.query_list[1].get_sql()
@@ -327,8 +338,11 @@ class test_expression(common.TransactionCase):
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table")
self.assertIn('"res_country" as "res_country_state__country_id"', sql_query[0],
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join")
- self.assertIn('"res_country_state__country_id"."code" like %s', sql_query[1],
+
+ expected = "%s::text like %s" % (unaccent('"res_country_state__country_id"."code"'), unaccent('%s'))
+ self.assertIn(expected, sql_query[1],
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition")
+
self.assertIn('"res_country_state"."country_id"="res_country_state__country_id"."id"', sql_query[1],
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join condition")
self.assertEqual(['%' + name_test + '%'], sql_query[2],
@@ -358,8 +372,11 @@ class test_expression(common.TransactionCase):
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join")
self.assertIn('"res_country" as "res_partner__state_id__country_id"', sql_query[0],
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join")
- self.assertIn('"res_partner__state_id__country_id"."code" like %s', sql_query[1],
+
+ expected = "%s::text like %s" % (unaccent('"res_partner__state_id__country_id"."code"'), unaccent('%s'))
+ self.assertIn(expected, sql_query[1],
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect where condition")
+
self.assertIn('"res_partner"."state_id"="res_partner__state_id"."id"', sql_query[1],
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join condition")
self.assertIn('"res_partner__state_id"."country_id"="res_partner__state_id__country_id"."id"', sql_query[1],
@@ -385,7 +402,9 @@ class test_expression(common.TransactionCase):
"_auto_join on one2many with domains incorrect result")
# Test produced queries that domains effectively present
sql_query = self.query_list[0].get_sql()
- self.assertIn('"res_partner__child_ids__bank_ids"."acc_number" like %s', sql_query[1],
+
+ expected = "%s::text like %s" % (unaccent('"res_partner__child_ids__bank_ids"."acc_number"'), unaccent('%s'))
+ self.assertIn(expected, sql_query[1],
"_auto_join on one2many with domains incorrect result")
# TDE TODO: check first domain has a correct table name
self.assertIn('"res_partner__child_ids"."name" = %s', sql_query[1],
@@ -447,6 +466,19 @@ class test_expression(common.TransactionCase):
domain = [('x', 'in', ['y', 'z']), ('a.v', '=', 'e'), '|', '|', ('a', '=', 'b'), '!', ('c', '>', 'd'), ('e', '!=', 'f'), ('g', '=', 'h')]
norm_domain = ['&', '&', '&'] + domain
assert norm_domain == expression.normalize_domain(domain), "Non-normalized domains should be properly normalized"
+
+ def test_translate_search(self):
+ Country = self.registry('res.country')
+ be = self.ref('base.be')
+ domains = [
+ [('name', '=', 'Belgium')],
+ [('name', 'ilike', 'Belgi')],
+ [('name', 'in', ['Belgium', 'Care Bears'])],
+ ]
+
+ for domain in domains:
+ ids = Country.search(self.cr, self.uid, domain)
+ self.assertListEqual([be], ids)
if __name__ == '__main__':
unittest2.main()
diff --git a/openerp/addons/base/tests/test_fields.py b/openerp/addons/base/tests/test_fields.py
index 6dd016ccaf9..ce34bceb014 100644
--- a/openerp/addons/base/tests/test_fields.py
+++ b/openerp/addons/base/tests/test_fields.py
@@ -120,4 +120,110 @@ class TestRelatedField(common.TransactionCase):
# restore res.partner fields
self.partner._columns = old_columns
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+
+class TestPropertyField(common.TransactionCase):
+
+ def setUp(self):
+ super(TestPropertyField, self).setUp()
+ self.user = self.registry('res.users')
+ self.partner = self.registry('res.partner')
+ self.company = self.registry('res.company')
+ self.country = self.registry('res.country')
+ self.property = self.registry('ir.property')
+ self.imd = self.registry('ir.model.data')
+
+ def test_1_property_multicompany(self):
+ cr, uid = self.cr, self.uid
+
+ parent_company_id = self.imd.get_object_reference(cr, uid, 'base', 'main_company')[1]
+ country_be = self.imd.get_object_reference(cr, uid, 'base', 'be')[1]
+ country_fr = self.imd.get_object_reference(cr, uid, 'base', 'fr')[1]
+ group_partner_manager = self.imd.get_object_reference(cr, uid, 'base', 'group_partner_manager')[1]
+ group_multi_company = self.imd.get_object_reference(cr, uid, 'base', 'group_multi_company')[1]
+
+ sub_company = self.company.create(cr, uid, {'name': 'MegaCorp', 'parent_id': parent_company_id})
+ alice = self.user.create(cr, uid, {'name': 'Alice',
+ 'login':'alice',
+ 'email':'alice@youcompany.com',
+ 'company_id':parent_company_id,
+ 'company_ids':[(6, 0, [parent_company_id, sub_company])],
+ 'country_id':country_be,
+ 'groups_id': [(6, 0, [group_partner_manager, group_multi_company])]
+ })
+ bob = self.user.create(cr, uid, {'name': 'Bob',
+ 'login':'bob',
+ 'email':'bob@megacorp.com',
+ 'company_id':sub_company,
+ 'company_ids':[(6, 0, [parent_company_id, sub_company])],
+ 'country_id':country_fr,
+ 'groups_id': [(6, 0, [group_partner_manager, group_multi_company])]
+ })
+
+ self.partner._columns = dict(self.partner._columns)
+ self.partner._columns.update({
+ 'property_country': fields.property(type='many2one', relation="res.country", string="Country by company"),
+ })
+ self.partner._all_columns.update({
+ 'property_country': fields.column_info('property_country', self.partner._columns['property_country'], None, None, None),
+ })
+ self.partner._field_create(cr)
+
+ partner_id = self.partner.create(cr, alice, {
+ 'name': 'An International Partner',
+ 'email': 'partner@example.com',
+ 'company_id': parent_company_id,
+ })
+ self.partner.write(cr, bob, [partner_id], {'property_country': country_fr})
+ self.assertEqual(self.partner.browse(cr, bob, partner_id).property_country.id, country_fr, "Bob does not see the value he has set on the property field")
+
+ 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..18b56daa58c 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
@@ -20,7 +21,7 @@ def registry(model):
return openerp.modules.registry.RegistryManager.get(DB)[model]
def cursor():
- return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
+ return openerp.modules.registry.RegistryManager.get(DB).cursor()
def drop_sequence(code):
@@ -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_orm.py b/openerp/addons/base/tests/test_orm.py
index fcd81965b8c..29a5a2880d1 100644
--- a/openerp/addons/base/tests/test_orm.py
+++ b/openerp/addons/base/tests/test_orm.py
@@ -134,13 +134,17 @@ class TestORM(common.TransactionCase):
rg = self.partner.read_group(self.cr, self.uid, domain, ['date'], 'date' + ':' + interval)
result = {}
for r in rg:
- result[r['date']] = set(self.partner.search(self.cr, self.uid, r['__domain']))
+ result[r['date:' + interval]] = set(self.partner.search(self.cr, self.uid, r['__domain']))
return result
self.assertEqual(len(read_group('day')), len(partners_by_day))
self.assertEqual(len(read_group('month')), len(partners_by_month))
self.assertEqual(len(read_group('year')), len(partners_by_year))
+ rg = self.partner.read_group(self.cr, self.uid, [('id', 'in', all_partners)],
+ ['date'], ['date:month', 'date:day'], lazy=False)
+ self.assertEqual(len(rg), len(all_partners))
+
class TestInherits(common.TransactionCase):
""" test the behavior of the orm for models that use _inherits;
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_ui.py b/openerp/addons/base/tests/test_ui.py
deleted file mode 100644
index 63136b36a31..00000000000
--- a/openerp/addons/base/tests/test_ui.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- coding: utf-8 -*-
-import os
-
-import openerp
-
-class TestUi(openerp.tests.HttpCase):
- def test_js(self):
- self.phantom_js('/',"console.log('ok')","console", login=None)
-
- def test_jsfile(self):
- self.phantom_jsfile(os.path.join(os.path.dirname(__file__), 'test_ui_hello.js'))
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/addons/base/tests/test_ui_hello.js b/openerp/addons/base/tests/test_ui_hello.js
deleted file mode 100644
index 6a453e947aa..00000000000
--- a/openerp/addons/base/tests/test_ui_hello.js
+++ /dev/null
@@ -1,5 +0,0 @@
-console.log("hello from test_phantom_hello.js");
-console.log("ok");
-// For a failure use:
-//console.log("error");
-phantom.exit();
diff --git a/openerp/addons/base/tests/test_uninstall.py b/openerp/addons/base/tests/test_uninstall.py
index 2425249afa4..b5c51c81405 100644
--- a/openerp/addons/base/tests/test_uninstall.py
+++ b/openerp/addons/base/tests/test_uninstall.py
@@ -13,7 +13,7 @@ def registry(model):
return openerp.modules.registry.RegistryManager.get(DB)[model]
def cursor():
- return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
+ return openerp.modules.registry.RegistryManager.get(DB).cursor()
def get_module(module_name):
registry = openerp.modules.registry.RegistryManager.get(DB)
diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py
index d93734f48cb..f0967836726 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,7 +10,25 @@ from openerp.tests import common
Field = E.field
-class TestNodeLocator(common.BaseCase):
+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.TransactionCase):
"""
The node locator returns None when it can not find a node, and the first
match when it finds something (no jquery-style node sets)
@@ -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,14 +431,186 @@ 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(sarch, self.arch)
+
+class TestTemplating(ViewCase):
+ def setUp(self):
+ import openerp.modules
+ super(TestTemplating, self).setUp()
+ self._pool = openerp.modules.registry.RegistryManager.get(common.DB)
+ self._init = self._pool._init
+ # fuck off
+ self._pool._init = False
+
+ def tearDown(self):
+ self._pool._init = self._init
+ super(TestTemplating, self).tearDown()
+
+ def test_branding_inherit(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': """
+
+
+ """
+ })
+
+ 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)
+
+ [initial] = arch.xpath('//item[@order=1]')
self.assertEqual(
- ET.tostring(sarch, encoding='utf-8'),
- ET.tostring(self.arch, encoding='utf-8'))
+ str(id),
+ initial.get('data-oe-id'),
+ "initial should come from the root view")
+ self.assertEqual(
+ '/root[1]/item[1]',
+ initial.get('data-oe-xpath'),
+ "initial's xpath should be within the root view only")
+ [second] = arch.xpath('//item[@order=2]')
+ self.assertEqual(
+ str(id2),
+ 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')
@@ -534,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)
- )),
- '')
+ ),
+ 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')
@@ -601,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)
- )),
- '')
-
+ ),
+ 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/base/tests/test_xmlrpc.py b/openerp/addons/base/tests/test_xmlrpc.py
index ab1d3821c40..854e345078a 100644
--- a/openerp/addons/base/tests/test_xmlrpc.py
+++ b/openerp/addons/base/tests/test_xmlrpc.py
@@ -3,26 +3,23 @@ import time
import unittest2
import xmlrpclib
-from openerp.tests import common
+import openerp
-DB = common.DB
-ADMIN_USER = common.ADMIN_USER
-ADMIN_USER_ID = common.ADMIN_USER_ID
-ADMIN_PASSWORD = common.ADMIN_PASSWORD
+DB = openerp.tests.common.DB
-class test_xmlrpc(common.HttpCase):
+class test_xmlrpc(openerp.tests.common.HttpCase):
def test_01_xmlrpc_login(self):
""" Try to login on the common service. """
- uid = self.xmlrpc_common.login(DB, ADMIN_USER, ADMIN_PASSWORD)
- self.assertTrue(uid == ADMIN_USER_ID)
+ uid = self.xmlrpc_common.login(DB, 'admin', 'admin')
+ self.assertTrue(uid == 1)
def test_xmlrpc_ir_model_search(self):
""" Try a search on the object service. """
o = self.xmlrpc_object
- ids = o.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', [])
+ ids = o.execute(DB, 1, 'admin', 'ir.model', 'search', [])
self.assertIsInstance(ids, list)
- ids = o.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', [], {})
+ ids = o.execute(DB, 1, 'admin', 'ir.model', 'search', [], {})
self.assertIsInstance(ids, list)
# This test was written to test the creation of a new RPC endpoint, not
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..18ac69c38e2 100644
--- a/openerp/cli/server.py
+++ b/openerp/cli/server.py
@@ -29,6 +29,7 @@ GNU Public Licence.
(c) 2003-TODAY, Fabien Pinckaers - OpenERP SA
"""
+import atexit
import logging
import os
import signal
@@ -72,22 +73,29 @@ 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'])]:
_logger.info("%s: %s", name, value)
+def rm_pid_file():
+ config = openerp.tools.config
+ if not openerp.evented and os.path.exists(config['pidfile']):
+ os.unlink(config['pidfile'])
+
def setup_pid_file():
""" Create a file with the process id written in it.
This function assumes the configuration has been initialized.
"""
config = openerp.tools.config
- if config['pidfile']:
+ if not openerp.evented and config['pidfile']:
with open(config['pidfile'], 'w') as fd:
pidtext = "%d" % (os.getpid())
fd.write(pidtext)
+ atexit.register(rm_pid_file)
+
def export_translation():
config = openerp.tools.config
@@ -103,7 +111,7 @@ def export_translation():
fileformat = os.path.splitext(config["translate_out"])[-1][1:].lower()
buf = file(config["translate_out"], "w")
registry = openerp.modules.registry.RegistryManager.new(dbname)
- cr = registry.db.cursor()
+ cr = registry.cursor()
openerp.tools.trans_export(config["language"],
config["translate_modules"] or ["all"], buf, fileformat, cr)
cr.close()
@@ -117,7 +125,7 @@ def import_translation():
dbname = config['db_name']
registry = openerp.modules.registry.RegistryManager.new(dbname)
- cr = registry.db.cursor()
+ cr = registry.cursor()
openerp.tools.trans_load( cr, config["translate_in"], config["language"],
context=context)
cr.commit()
@@ -127,7 +135,6 @@ def main(args):
check_root_user()
openerp.tools.config.parse_config(args)
check_postgres_user()
- openerp.netsvc.init_logger()
report_configuration()
config = openerp.tools.config
@@ -156,8 +163,6 @@ def main(args):
setup_pid_file()
rc = openerp.service.server.start(preload=preload, stop=stop)
- if config['pidfile']:
- os.unlink(config['pidfile'])
sys.exit(rc)
class Server(Command):
diff --git a/openerp/http.py b/openerp/http.py
index fe5b113d286..8d7497fc40d 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,7 @@ import werkzeug.wsgi
import openerp
from openerp.service import security, model as service_model
-import openerp.tools
+from openerp.tools.func import lazy_property
_logger = logging.getLogger(__name__)
@@ -50,6 +51,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 +194,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:
@@ -168,10 +235,7 @@ class WebRequest(object):
"""
# some magic to lazy create the cr
if not self._cr:
- if openerp.tools.config['test_enable'] and self.session_id in openerp.tests.common.HTTP_SESSION:
- self._cr = openerp.tests.common.HTTP_SESSION[self.session_id]
- else:
- self._cr = self.registry.db.cursor()
+ self._cr = self.registry.cursor()
return self._cr
def __enter__(self):
@@ -180,49 +244,55 @@ class WebRequest(object):
def __exit__(self, exc_type, exc_value, traceback):
_request_stack.pop()
- if self._cr and not (openerp.tools.config['test_enable'] and self.session_id in openerp.tests.common.HTTP_SESSION):
- if exc_type is None:
+
+ if self._cr:
+ if exc_type is None and not self._failed:
self._cr.commit()
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 _handle_exception(self, exception):
+ """Called within an except block to allow converting exceptions
+ to abitrary responses. Anything returned (except None) will
+ be used as response."""
+ raise
+
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):
@@ -262,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):
@@ -335,35 +420,15 @@ class JsonRequest(WebRequest):
self.params = dict(self.jsonrequest.get("params", {}))
self.context = self.params.pop('context', dict(self.session.context))
- def dispatch(self):
- """ Calls the method asked for by the JSON-RPC2 or JSONP request
- """
- if self.jsonp_handler:
- return self.jsonp_handler()
- response = {"jsonrpc": "2.0" }
- error = None
-
- try:
- response['id'] = self.jsonrequest.get('id')
- response["result"] = self._call_function(**self.params)
- except AuthenticationError, e:
- _logger.exception("Exception during JSON request handling.")
- se = serialize_exception(e)
- error = {
- 'code': 100,
- 'message': "OpenERP Session Invalid",
- 'data': se
- }
- except Exception, e:
- _logger.exception("Exception during JSON request handling.")
- se = serialize_exception(e)
- error = {
- 'code': 200,
- 'message': "OpenERP Server Error",
- 'data': se
- }
- if error:
- response["error"] = error
+ def _json_response(self, result=None, error=None):
+ response = {
+ 'jsonrpc': '2.0',
+ 'id': self.jsonrequest.get('id')
+ }
+ if error is not None:
+ response['error'] = error
+ if result is not None:
+ response['result'] = result
if self.jsonp:
# If we use jsonp, that's mean we are called from another host
@@ -376,8 +441,36 @@ class JsonRequest(WebRequest):
mime = 'application/json'
body = simplejson.dumps(response)
- r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
- return r
+ return Response(
+ body, headers=[('Content-Type', mime),
+ ('Content-Length', len(body))])
+
+ def _handle_exception(self, exception):
+ """Called within an except block to allow converting exceptions
+ to abitrary responses. Anything returned (except None) will
+ be used as response."""
+ _logger.exception("Exception during JSON request handling.")
+ self._failed = exception # prevent tx commit
+ error = {
+ 'code': 200,
+ 'message': "OpenERP Server Error",
+ 'data': serialize_exception(exception)
+ }
+ if isinstance(exception, AuthenticationError):
+ error['code'] = 100
+ error['message'] = "OpenERP Session Invalid"
+ return self._json_response(error=error)
+
+ def dispatch(self):
+ """ Calls the method asked for by the JSON-RPC2 or JSONP request
+ """
+ if self.jsonp_handler:
+ return self.jsonp_handler()
+ try:
+ result = self._call_function(**self.params)
+ return self._json_response(result)
+ except Exception, e:
+ return self._handle_exception(e)
def serialize_exception(e):
tmp = {
@@ -434,23 +527,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):
@@ -467,12 +553,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
"""
@@ -527,7 +625,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)
@@ -550,9 +656,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"):
@@ -580,7 +696,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
@@ -588,14 +704,14 @@ 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
class Model(object):
"""
.. deprecated:: 8.0
- Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
+ Use the resistry and cursor in ``openerp.http.request`` instead.
"""
def __init__(self, session, model):
self.session = session
@@ -661,7 +777,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
@@ -695,7 +811,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):
"""
@@ -765,14 +881,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)
@@ -889,19 +1005,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):
@@ -925,51 +1073,33 @@ 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)
+ self._loaded = False
- # TODO should we move this to ir.http so that only configured modules are served ?
- _logger.info("HTTP Configuring static files")
- self.load_addons()
-
+ @lazy_property
+ def nodb_routing_map(self):
_logger.info("Generating nondb routing")
- self.nodb_routing_map = routing_map([''] + openerp.conf.server_wide_modules, True)
+ return routing_map([''] + openerp.conf.server_wide_modules, True)
def __call__(self, environ, start_response):
""" Handle a WSGI request
"""
+ if not self._loaded:
+ self._loaded = True
+ self.load_addons()
return self.dispatch(environ, start_response)
def load_addons(self):
- """ Load all addons from addons patch containg static files and
+ """ Load all addons from addons path containing static files and
controllers and configure them. """
+ # TODO should we move this to ir.http so that only configured modules are served ?
statics = {}
for addons_path in openerp.modules.module.ad_paths:
@@ -983,12 +1113,16 @@ class Root(object):
_logger.debug("Loading %s", module)
if 'openerp.addons' in sys.modules:
m = __import__('openerp.addons.' + module)
+ else:
+ m = None
addons_module[module] = m
addons_manifest[module] = manifest
statics['/%s/static' % module] = path_static
- app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
- self.dispatch = DisableCacheMiddleware(app)
+ if statics:
+ _logger.info("HTTP Configuring static files")
+ app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
+ self.dispatch = DisableCacheMiddleware(app)
def setup_session(self, httprequest):
# recover or create session
@@ -1036,9 +1170,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)
@@ -1046,7 +1180,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
@@ -1061,16 +1195,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):
@@ -1124,12 +1248,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)]
@@ -1169,18 +1293,10 @@ 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)
+ return 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
-
-root = None
-
-def wsgi_postload():
- global root
- root = Root()
- openerp.service.wsgi_server.register_wsgi_handler(root)
+# register main wsgi handler
+root = Root()
+openerp.service.wsgi_server.register_wsgi_handler(root)
# vim:et:ts=4:sw=4:
diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py
index 7ca0a6785d6..5635e42e780 100644
--- a/openerp/modules/loading.py
+++ b/openerp/modules/loading.py
@@ -42,6 +42,7 @@ from openerp import SUPERUSER_ID
from openerp.tools.translate import _
from openerp.modules.module import initialize_sys_path, \
load_openerp_module, init_module_models, adapt_version
+from module import runs_post_install
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('openerp.tests')
@@ -187,6 +188,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)
@@ -421,8 +426,13 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
for model in registry.models.values():
model._register_hook(cr)
+ # STEP 9: Run the post-install tests
+ cr.commit()
+ if openerp.tools.config['test_enable']:
+ cr.execute("SELECT name FROM ir_module_module WHERE state='installed'")
+ for module_name in cr.fetchall():
+ report.record_result(openerp.modules.module.run_unit_tests(module_name[0], cr.dbname, position=runs_post_install))
finally:
cr.close()
-
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/modules/module.py b/openerp/modules/module.py
index e3b8e10cc97..82e86e83ce2 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
@@ -20,15 +20,14 @@
#
##############################################################################
-import base64
+import functools
import imp
import itertools
import logging
import os
import re
import sys
-import types
-from cStringIO import StringIO
+import unittest
from os.path import join as opj
import unittest2
@@ -39,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 = []
@@ -50,8 +45,6 @@ ad_paths = []
# Modules already loaded
loaded = []
-_logger = logging.getLogger(__name__)
-
class AddonsImportHook(object):
"""
Import hook to load OpenERP addons from multiple paths.
@@ -70,13 +63,10 @@ class AddonsImportHook(object):
return self # We act as a loader too.
def load_module(self, module_name):
+ if module_name in sys.modules:
+ return sys.modules[module_name]
- module_parts = module_name.split('.')
- if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
- module_part = module_parts[2]
- if module_name in sys.modules:
- return sys.modules[module_name]
-
+ _1, _2, module_part = module_name.split('.')
# Note: we don't support circular import.
f, path, descr = imp.find_module(module_part, ad_paths)
mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
@@ -96,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):
@@ -114,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
@@ -174,13 +169,15 @@ def get_module_icon(module):
return ('/' + module + '/') + '/'.join(iconpath)
return '/base/' + '/'.join(iconpath)
-def load_information_from_description_file(module):
+def load_information_from_description_file(module, mod_path=None):
"""
:param module: The name of the module (sale, purchase, ...)
+ :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...)
"""
- terp_file = get_module_resource(module, '__openerp__.py')
- mod_path = get_module_path(module)
+ if not mod_path:
+ mod_path = get_module_path(module)
+ terp_file = opj(mod_path, '__openerp__.py')
if terp_file:
info = {}
if os.path.isfile(terp_file):
@@ -328,7 +325,7 @@ def get_test_modules(module):
# Try to import the module
module = 'openerp.addons.' + module + '.tests'
try:
- m = __import__(module)
+ __import__(module)
except Exception, e:
# If module has no `tests` sub-module, no problem.
if str(e) != 'No module named tests':
@@ -336,15 +333,16 @@ def get_test_modules(module):
return []
# include submodules too
- result = []
- for name in sys.modules:
- if name.startswith(module) and sys.modules[name]:
- result.append(sys.modules[name])
+ result = [mod_obj for name, mod_obj in sys.modules.iteritems()
+ if mod_obj # mod_obj can be None
+ if name.startswith(module)
+ if re.search(r'test_\w+$', name)]
return result
# 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
@@ -356,24 +354,74 @@ class TestStream(object):
if not first:
c = '` ' + c
first = False
- _test_logger.info(c)
+ self.logger.info(c)
-def run_unit_tests(module_name, dbname):
+current_test = None
+
+def runs_at(test, hook, default):
+ # by default, tests do not run post install
+ test_runs = getattr(test, hook, default)
+
+ # for a test suite, we're done
+ if not isinstance(test, unittest.TestCase):
+ return test_runs
+
+ # otherwise check the current test method to see it's been set to a
+ # different state
+ method = getattr(test, test._testMethodName)
+ return getattr(method, hook, test_runs)
+
+runs_at_install = functools.partial(runs_at, hook='at_install', default=True)
+runs_post_install = functools.partial(runs_at, hook='post_install', default=False)
+
+def run_unit_tests(module_name, dbname, position=runs_at_install):
"""
- Return True or False if some tests were found and succeeded or failed.
- Return None if no test was found.
+ :returns: ``True`` if all of ``module_name``'s tests succeeded, ``False``
+ if any of them failed.
+ :rtype: bool
"""
+ global current_test
+ current_test = module_name
mods = get_test_modules(module_name)
r = True
for m in mods:
- suite = unittest2.TestSuite()
- for t in unittest2.TestLoader().loadTestsFromModule(m):
- suite.addTest(t)
- _logger.log(logging.INFO, 'module %s: running test %s.', module_name, m.__name__)
- result = unittest2.TextTestRunner(verbosity=2, stream=TestStream()).run(suite)
+ tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m))
+ suite = unittest2.TestSuite(itertools.ifilter(position, tests))
+ _logger.info('running %s tests.', m.__name__)
+
+ result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite)
+
if not result.wasSuccessful():
r = False
- _logger.error('module %s: at least one error occurred in a test', module_name)
+ _logger.error("Module %s: %d failures, %d errors",
+ module_name, len(result.failures), len(result.errors))
+ current_test = None
return r
+def unwrap_suite(test):
+ """
+ Attempts to unpack testsuites (holding suites or cases) in order to
+ generate a single stream of terminals (either test cases or customized
+ test suites). These can then be checked for run/skip attributes
+ individually.
+
+ An alternative would be to use a variant of @unittest2.skipIf with a state
+ flag of some sort e.g. @unittest2.skipIf(common.runstate != 'at_install'),
+ but then things become weird with post_install as tests should *not* run
+ by default there
+ """
+ if isinstance(test, unittest.TestCase):
+ yield test
+ return
+
+ subtests = list(test)
+ # custom test suite (no test cases)
+ if not len(subtests):
+ yield test
+ return
+
+ for item in itertools.chain.from_iterable(
+ itertools.imap(unwrap_suite, subtests)):
+ yield item
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py
index a277d4f6402..eb591c53521 100644
--- a/openerp/modules/registry.py
+++ b/openerp/modules/registry.py
@@ -58,7 +58,10 @@ class Registry(Mapping):
self._init_modules = set()
self.db_name = db_name
- self.db = openerp.sql_db.db_connect(db_name)
+ self._db = openerp.sql_db.db_connect(db_name)
+
+ # special cursor for test mode; None means "normal" mode
+ self.test_cr = None
# Indicates that the registry is
self.ready = False
@@ -75,7 +78,7 @@ class Registry(Mapping):
# Useful only in a multi-process context.
self._any_cache_cleared = False
- cr = self.db.cursor()
+ cr = self.cursor()
has_unaccent = openerp.modules.db.has_unaccent(cr)
if openerp.tools.config['unaccent'] and not has_unaccent:
_logger.warning("The option --unaccent was given but no unaccent() function was found in database.")
@@ -102,6 +105,10 @@ class Registry(Mapping):
""" Return the model with the given name or raise KeyError if it doesn't exist."""
return self.models[model_name]
+ def __call__(self, model_name):
+ """ Same as ``self[model_name]``. """
+ return self.models[model_name]
+
def do_parent_store(self, cr):
for o in self._init_parent:
self.get(o)._parent_store_compute(cr)
@@ -183,41 +190,44 @@ class Registry(Mapping):
r, c)
return r, c
- @contextmanager
- def cursor(self, auto_commit=True):
- cr = self.db.cursor()
- try:
- yield cr
- if auto_commit:
- cr.commit()
- finally:
- cr.close()
+ def enter_test_mode(self):
+ """ Enter the 'test' mode, where one cursor serves several requests. """
+ assert self.test_cr is None
+ self.test_cr = self._db.test_cursor()
+ RegistryManager.enter_test_mode()
-class TestRLock(object):
- def __init__(self):
- self._lock = threading.RLock()
+ def leave_test_mode(self):
+ """ Leave the test mode. """
+ assert self.test_cr is not None
+ self.test_cr.force_close()
+ self.test_cr = None
+ RegistryManager.leave_test_mode()
+
+ def cursor(self):
+ """ Return a new cursor for the database. The cursor itself may be used
+ as a context manager to commit/rollback and close automatically.
+ """
+ cr = self.test_cr
+ if cr is not None:
+ # While in test mode, we use one special cursor across requests. The
+ # test cursor uses a reentrant lock to serialize accesses. The lock
+ # is granted here by cursor(), and automatically released by the
+ # cursor itself in its method close().
+ cr.acquire()
+ return cr
+ return self._db.cursor()
+
+class DummyRLock(object):
+ """ Dummy reentrant lock, to be used while running rpc and js tests """
def acquire(self):
- if openerp.tools.config['test_enable']:
- return
- return self._lock.acquire()
+ pass
def release(self):
- if openerp.tools.config['test_enable']:
- return
- return self._lock.release()
+ pass
def __enter__(self):
self.acquire()
def __exit__(self, type, value, traceback):
self.release()
-# def __enter__(self, *l, **kw):
-# if openerp.tools.config['test_enable']:
-# return
-# return super(TestRlock, self).__enter__(*l, **kw)
-# def __exit__(self, *l, **kw):
-# if openerp.tools.config['test_enable']:
-# return
-# return super(TestRlock, self).__exit__(*l, **kw)
-
class RegistryManager(object):
""" Model registries manager.
@@ -228,12 +238,30 @@ class RegistryManager(object):
# Mapping between db name and model registry.
# Accessed through the methods below.
registries = {}
- registries_lock = TestRLock()
+ _lock = threading.RLock()
+ _saved_lock = None
+
+ @classmethod
+ def lock(cls):
+ """ Return the current registry lock. """
+ return cls._lock
+
+ @classmethod
+ def enter_test_mode(cls):
+ """ Enter the 'test' mode, where the registry is no longer locked. """
+ assert cls._saved_lock is None
+ cls._lock, cls._saved_lock = DummyRLock(), cls._lock
+
+ @classmethod
+ def leave_test_mode(cls):
+ """ Leave the 'test' mode. """
+ assert cls._saved_lock is not None
+ cls._lock, cls._saved_lock = cls._saved_lock, None
@classmethod
def get(cls, db_name, force_demo=False, status=None, update_module=False):
""" Return a registry for a given database name."""
- with cls.registries_lock:
+ with cls.lock():
try:
return cls.registries[db_name]
except KeyError:
@@ -253,7 +281,7 @@ class RegistryManager(object):
"""
import openerp.modules
- with cls.registries_lock:
+ with cls.lock():
registry = Registry(db_name)
# Initializing a registry will call general code which will in turn
@@ -268,7 +296,7 @@ class RegistryManager(object):
registry.base_registry_signaling_sequence = seq_registry
registry.base_cache_signaling_sequence = seq_cache
# This should be a method on Registry
- openerp.modules.load_modules(registry.db, force_demo, status, update_module)
+ openerp.modules.load_modules(registry._db, force_demo, status, update_module)
except Exception:
del cls.registries[db_name]
raise
@@ -278,7 +306,7 @@ class RegistryManager(object):
# Yeah, crazy.
registry = cls.registries[db_name]
- cr = registry.db.cursor()
+ cr = registry.cursor()
try:
registry.do_parent_store(cr)
cr.commit()
@@ -295,7 +323,7 @@ class RegistryManager(object):
@classmethod
def delete(cls, db_name):
"""Delete the registry linked to a given database. """
- with cls.registries_lock:
+ with cls.lock():
if db_name in cls.registries:
cls.registries[db_name].clear_caches()
del cls.registries[db_name]
@@ -303,7 +331,7 @@ class RegistryManager(object):
@classmethod
def delete_all(cls):
"""Delete all the registries. """
- with cls.registries_lock:
+ with cls.lock():
for db_name in cls.registries.keys():
cls.delete(db_name)
@@ -318,7 +346,7 @@ class RegistryManager(object):
This method is given to spare you a ``RegistryManager.get(db_name)``
that would loads the given database if it was not already loaded.
"""
- with cls.registries_lock:
+ with cls.lock():
if db_name in cls.registries:
cls.registries[db_name].clear_caches()
@@ -334,7 +362,7 @@ class RegistryManager(object):
changed = False
if openerp.multi_process and db_name in cls.registries:
registry = cls.get(db_name)
- cr = registry.db.cursor()
+ cr = registry.cursor()
try:
cr.execute("""
SELECT base_registry_signaling.last_value,
@@ -380,7 +408,7 @@ class RegistryManager(object):
registry = cls.get(db_name)
if registry.any_cache_cleared():
_logger.info("At least one model cache has been cleared, signaling through the database.")
- cr = registry.db.cursor()
+ cr = registry.cursor()
r = 1
try:
cr.execute("select nextval('base_cache_signaling')")
@@ -395,7 +423,7 @@ class RegistryManager(object):
if openerp.multi_process and db_name in cls.registries:
_logger.info("Registry changed, signaling through the database")
registry = cls.get(db_name)
- cr = registry.db.cursor()
+ cr = registry.cursor()
r = 1
try:
cr.execute("select nextval('base_registry_signaling')")
diff --git a/openerp/netsvc.py b/openerp/netsvc.py
index 755f6513b25..15f02645d52 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', ct_db, 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/expression.py b/openerp/osv/expression.py
index 648a8447292..d3231c54462 100644
--- a/openerp/osv/expression.py
+++ b/openerp/osv/expression.py
@@ -429,6 +429,10 @@ def select_distinct_from_where_not_null(cr, select_field, from_table):
cr.execute('SELECT distinct("%s") FROM "%s" where "%s" is not null' % (select_field, from_table, select_field))
return [r[0] for r in cr.fetchall()]
+def get_unaccent_wrapper(cr):
+ if openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent:
+ return lambda x: "unaccent(%s)" % (x,)
+ return lambda x: x
# --------------------------------------------------
# ExtendedLeaf class for managing leafs and contexts
@@ -630,7 +634,7 @@ class expression(object):
:attr list expression: the domain expression, that will be normalized
and prepared
"""
- self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent
+ self._unaccent = get_unaccent_wrapper(cr)
self.joins = []
self.root_model = table
@@ -1012,10 +1016,10 @@ class expression(object):
else:
if field._type == 'datetime' and right and len(right) == 10:
- if operator in ('>', '>=', '='):
- right += ' 00:00:00'
- elif operator in ('<', '<='):
+ if operator in ('>', '<='):
right += ' 23:59:59'
+ else:
+ right += ' 00:00:00'
push(create_substitution_leaf(leaf, (left, operator, right), working_model))
elif field.translate and right:
@@ -1030,33 +1034,37 @@ class expression(object):
sql_operator = sql_operator[4:] if sql_operator[:3] == 'not' else '='
inselect_operator = 'not inselect'
- subselect = '( SELECT res_id' \
- ' FROM ir_translation' \
- ' WHERE name = %s' \
- ' AND lang = %s' \
- ' AND type = %s'
- instr = ' %s'
- #Covering in,not in operators with operands (%s,%s) ,etc.
- if sql_operator == 'in':
- instr = ','.join(['%s'] * len(right))
- subselect += ' AND value ' + sql_operator + ' ' + " (" + instr + ")" \
- ') UNION (' \
- ' SELECT id' \
- ' FROM "' + working_model._table + '"' \
- ' WHERE "' + left + '" ' + sql_operator + ' ' + " (" + instr + "))"
- else:
- subselect += ' AND value ' + sql_operator + instr + \
- ') UNION (' \
- ' SELECT id' \
- ' FROM "' + working_model._table + '"' \
- ' WHERE "' + left + '" ' + sql_operator + instr + ")"
+ unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x
- params = [working_model._name + ',' + left,
- context.get('lang', False) or 'en_US',
- 'model',
- right,
- right,
- ]
+ trans_left = unaccent('value')
+ quote_left = unaccent(_quote(left))
+ instr = unaccent('%s')
+
+ if sql_operator == 'in':
+ # params will be flatten by to_sql() => expand the placeholders
+ instr = '(%s)' % ', '.join(['%s'] * len(right))
+
+ subselect = """(SELECT res_id
+ FROM ir_translation
+ WHERE name = %s
+ AND lang = %s
+ AND type = %s
+ AND {trans_left} {operator} {right}
+ ) UNION (
+ SELECT id
+ FROM "{table}"
+ WHERE {left} {operator} {right}
+ )
+ """.format(trans_left=trans_left, operator=sql_operator,
+ right=instr, table=working_model._table, left=quote_left)
+
+ params = (
+ working_model._name + ',' + left,
+ context.get('lang') or 'en_US',
+ 'model',
+ right,
+ right,
+ )
push(create_substitution_leaf(leaf, ('id', inselect_operator, (subselect, params)), working_model))
else:
@@ -1174,15 +1182,15 @@ class expression(object):
else:
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
+ cast = '::text' if sql_operator.endswith('like') else ''
if left in model._columns:
format = need_wildcard and '%s' or model._columns[left]._symbol_set[0]
- if self.has_unaccent and sql_operator in ('ilike', 'not ilike'):
- query = '(unaccent(%s."%s") %s unaccent(%s))' % (table_alias, left, sql_operator, format)
- else:
- query = '(%s."%s" %s %s)' % (table_alias, left, sql_operator, format)
+ unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x
+ column = '%s.%s' % (table_alias, _quote(left))
+ query = '(%s%s %s %s)' % (unaccent(column), cast, sql_operator, unaccent(format))
elif left in MAGIC_COLUMNS:
- query = "(%s.\"%s\" %s %%s)" % (table_alias, left, sql_operator)
+ query = "(%s.\"%s\"%s %s %%s)" % (table_alias, left, cast, sql_operator)
params = right
else: # Must not happen
raise ValueError("Invalid field %r in domain term %r" % (left, leaf))
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index 190453d477a..439463b962a 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__
@@ -335,6 +344,30 @@ class date(_column):
exc_info=True)
return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
+ @staticmethod
+ def date_to_datetime(model, cr, uid, userdate, context=None):
+ """ Convert date values expressed in user's timezone to
+ server-side UTC timestamp, assuming a default arbitrary
+ time of 12:00 AM - because a time is needed.
+
+ :param str userdate: date string in in user time zone
+ :return: UTC datetime string for server-side use
+ """
+ user_date = DT.datetime.strptime(userdate, tools.DEFAULT_SERVER_DATE_FORMAT)
+ if context and context.get('tz'):
+ tz_name = context['tz']
+ else:
+ tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
+ if tz_name:
+ utc = pytz.timezone('UTC')
+ context_tz = pytz.timezone(tz_name)
+ user_datetime = user_date + DT.timedelta(hours=12.0)
+ local_timestamp = context_tz.localize(user_datetime, is_dst=False)
+ user_datetime = local_timestamp.astimezone(utc)
+ return user_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ return user_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+
+
class datetime(_column):
_type = 'datetime'
@@ -639,7 +672,10 @@ class one2many(_column):
else:
cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],))
elif act[0] == 4:
- cr.execute("select 1 from {0} where id=%s and {1}=%s".format(_table, self._fields_id), (act[1], id))
+ # table of the field (parent_model in case of inherit)
+ field_model = self._fields_id in obj.pool[self._obj]._columns and self._obj or obj.pool[self._obj]._all_columns[self._fields_id].parent_model
+ field_table = obj.pool[field_model]._table
+ cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id))
if not cr.fetchone():
# Must use write() to recompute parent_store structure if needed and check access rules
obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {})
@@ -1179,42 +1215,69 @@ class function(_column):
return self._fnct_search(obj, cr, uid, obj, name, args, context=context)
def postprocess(self, cr, uid, obj, field, value=None, context=None):
+ return self._postprocess_batch(cr, uid, obj, field, {0: value}, context=context)[0]
+
+ def _postprocess_batch(self, cr, uid, obj, field, values, context=None):
+ if not values:
+ return values
+
if context is None:
context = {}
- result = value
+
field_type = obj._columns[field]._type
- if field_type == "many2one":
- # make the result a tuple if it is not already one
- if isinstance(value, (int,long)) and hasattr(obj._columns[field], 'relation'):
- obj_model = obj.pool[obj._columns[field].relation]
- dict_names = dict(obj_model.name_get(cr, SUPERUSER_ID, [value], context))
- result = (value, dict_names[value])
+ new_values = dict(values)
- if field_type == 'binary':
- if context.get('bin_size'):
- # client requests only the size of binary fields
- result = get_nice_size(value)
- elif not context.get('bin_raw'):
- result = sanitize_binary_value(value)
-
- if field_type == "integer" and value > xmlrpclib.MAXINT:
+ if field_type == "integer":
# integer/long values greater than 2^31-1 are not supported
# in pure XMLRPC, so we have to pass them as floats :-(
# This is not needed for stored fields and non-functional integer
# fields, as their values are constrained by the database backend
# to the same 32bits signed int limit.
- result = __builtin__.float(value)
- return result
+ for rid, value in values.iteritems():
+ if value and value > xmlrpclib.MAXINT:
+ new_values[rid] = __builtin__.float(value)
+
+ elif field_type == 'binary':
+ if context.get('bin_size'):
+ # client requests only the size of binary fields
+ for rid, value in values.iteritems():
+ if value:
+ new_values[rid] = get_nice_size(value)
+ elif not context.get('bin_raw'):
+ for rid, value in values.iteritems():
+ if value:
+ new_values[rid] = sanitize_binary_value(value)
+
+ elif field_type == "many2one" and hasattr(obj._columns[field], 'relation'):
+ # make the result a tuple if it is not already one
+ if all(isinstance(value, (int, long)) for value in values.values() if value):
+ obj_model = obj.pool[obj._columns[field].relation]
+ ids = [i for i in values.values() if i]
+ dict_names = dict(obj_model.name_get(cr, SUPERUSER_ID, ids, context))
+ for rid, value in values.iteritems():
+ if value:
+ new_values[rid] = (value, dict_names[value])
+
+ return new_values
def get(self, cr, obj, ids, name, uid=False, context=None, values=None):
result = self._fnct(obj, cr, uid, ids, name, self._arg, context)
- for id in ids:
- if self._multi and id in result:
- for field, value in result[id].iteritems():
- if value:
- result[id][field] = self.postprocess(cr, uid, obj, field, value, context)
- elif result.get(id):
- result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
+ if self._multi:
+ swap = {}
+ for rid, values in result.iteritems():
+ for f, v in values.iteritems():
+ if f not in name:
+ continue
+ swap.setdefault(f, {})[rid] = v
+
+ for field, values in swap.iteritems():
+ new_values = self._postprocess_batch(cr, uid, obj, field, values, context)
+ for rid, value in new_values.iteritems():
+ result[rid][field] = value
+
+ else:
+ result = self._postprocess_batch(cr, uid, obj, name, result, context)
+
return result
def set(self, cr, obj, id, name, value, user=None, context=None):
@@ -1461,11 +1524,9 @@ class property(function):
def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
prop = obj.pool.get('ir.property')
vids = [obj._name + ',' + str(oid) for oid in ids]
- def_id = self._field_get(cr, uid, obj._name, prop_name[0])
- company = obj.pool.get('res.company')
- cid = company._company_default_get(cr, uid, obj._name, def_id, context=context)
- domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name), ('company_id', '=', cid)]
- #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
+ domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
+ if context and context.get('company_id'):
+ domain += [('company_id', '=', context.get('company_id'))]
if vids:
domain = [('res_id', 'in', vids)] + domain
return prop.search(cr, uid, domain, context=context)
@@ -1475,7 +1536,12 @@ class property(function):
if context is None:
context = {}
- nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
+ def_id = self._field_get(cr, uid, obj._name, prop_name)
+ company = obj.pool.get('res.company')
+ cid = company._company_default_get(cr, uid, obj._name, def_id, context=context)
+ # TODO for trunk: add new parameter company_id to _get_by_id method
+ context_company = dict(context, company_id=cid)
+ nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context_company)
if nids:
cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
@@ -1489,10 +1555,6 @@ class property(function):
property_create = True
if property_create:
- def_id = self._field_get(cr, uid, obj._name, prop_name)
- company = obj.pool.get('res.company')
- cid = company._company_default_get(cr, uid, obj._name, def_id,
- context=context)
propdef = obj.pool.get('ir.model.fields').browse(cr, uid, def_id,
context=context)
prop = obj.pool.get('ir.property')
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index 6ac65125620..a198e013746 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -55,7 +55,7 @@ import traceback
import types
import babel.dates
-import dateutil.parser
+import dateutil.relativedelta
import psycopg2
from lxml import etree
@@ -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
@@ -469,7 +469,12 @@ class browse_record(object):
else:
new_data[field_name] = browse_null()
elif field_column._type in ('one2many', 'many2many') and len(result_line[field_name]):
- new_data[field_name] = self._list_class([browse_record(self._cr, self._uid, id, self._table.pool[field_column._obj], self._cache, context=self._context, list_class=self._list_class, fields_process=self._fields_process) for id in result_line[field_name]], self._context)
+ new_data[field_name] = self._list_class(
+ (browse_record(self._cr, self._uid, id, self._table.pool.get(field_column._obj),
+ self._cache, context=self._context, list_class=self._list_class,
+ fields_process=self._fields_process)
+ for id in result_line[field_name]),
+ context=self._context)
elif field_column._type == 'reference':
if result_line[field_name]:
if isinstance(result_line[field_name], browse_record):
@@ -770,8 +775,6 @@ class BaseModel(object):
(name_id, context['module'], 'ir.model', model_id)
)
- cr.commit()
-
cr.execute("SELECT * FROM ir_model_fields WHERE model=%s", (self._name,))
cols = {}
for rec in cr.dictfetchall():
@@ -838,7 +841,6 @@ class BaseModel(object):
for key, val in vals.items():
if cols[k][key] != vals[key]:
cr.execute('update ir_model_fields set field_description=%s where model=%s and name=%s', (vals['field_description'], vals['model'], vals['name']))
- cr.commit()
cr.execute("""UPDATE ir_model_fields SET
model_id=%s, field_description=%s, ttype=%s, relation=%s,
select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s, serialization_field_id=%s
@@ -849,7 +851,6 @@ class BaseModel(object):
vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['serialization_field_id'], vals['model'], vals['name']
))
break
- cr.commit()
#
# Goal: try to apply inheritance at the instanciation level and
@@ -1104,7 +1105,7 @@ class BaseModel(object):
self._rec_name = 'name'
- def __export_row(self, cr, uid, row, fields, context=None):
+ def __export_row(self, cr, uid, row, fields, raw_data=False, context=None):
if context is None:
context = {}
@@ -1128,7 +1129,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']:
@@ -1138,13 +1139,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__',
})
@@ -1197,8 +1198,7 @@ class BaseModel(object):
break
for row2 in r:
- lines2 = row2._model.__export_row(cr, uid, row2, fields2,
- context)
+ lines2 = row2._model.__export_row(cr, uid, row2, fields2, context=context)
if first:
for fpos2 in range(len(fields)):
if lines2 and lines2[0][fpos2]:
@@ -1220,14 +1220,22 @@ class BaseModel(object):
lines += lines2
break
i += 1
+
if i == len(f):
if isinstance(r, browse_record):
r = self.pool[r._table_name].name_get(cr, uid, [r.id], context=context)
r = r and r[0] and r[0][1] or ''
- data[fpos] = tools.ustr(r or '')
+ if raw_data and cols and cols._type in ('integer', 'boolean', 'float'):
+ data[fpos] = r
+ elif raw_data and cols and cols._type == 'date':
+ data[fpos] = datetime.datetime.strptime(r, tools.DEFAULT_SERVER_DATE_FORMAT).date()
+ elif raw_data and cols and cols._type == 'datetime':
+ data[fpos] = datetime.datetime.strptime(r, tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ else:
+ data[fpos] = tools.ustr(r or '')
return [data] + lines
- def export_data(self, cr, uid, ids, fields_to_export, context=None):
+ def export_data(self, cr, uid, ids, fields_to_export, raw_data=False, context=None):
"""
Export fields for selected objects
@@ -1235,6 +1243,7 @@ class BaseModel(object):
:param uid: current user id
:param ids: list of ids
:param fields_to_export: list of fields
+ :param raw_data: True to return value in fields type, False for string values
:param context: context arguments, like lang, time zone
:rtype: dictionary with a *datas* matrix
@@ -1249,7 +1258,7 @@ class BaseModel(object):
fields_to_export = map(fix_import_export_id_paths, fields_to_export)
datas = []
for row in self.browse(cr, uid, ids, context):
- datas += self.__export_row(cr, uid, row, fields_to_export, context)
+ datas += self.__export_row(cr, uid, row, fields_to_export, raw_data=raw_data, context=context)
return {'datas': datas}
def import_data(self, cr, uid, fields, datas, mode='init', current_module='', noupdate=False, context=None, filename=None):
@@ -1906,8 +1915,33 @@ class BaseModel(object):
}
return result
+ def get_formview_id(self, cr, uid, id, context=None):
+ """ Return an view id to open the document with. This method is meant to be
+ overridden in addons that want to give specific view ids for example.
+
+ :param int id: id of the document to open
+ """
+ return False
+
+ def get_formview_action(self, cr, uid, id, context=None):
+ """ Return an action to open the document. This method is meant to be
+ overridden in addons that want to give specific view ids for example.
+
+ :param int id: id of the document to open
+ """
+ view_id = self.get_formview_id(cr, uid, id, context=context)
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': self._name,
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'views': [(view_id, 'form')],
+ 'target': 'current',
+ 'res_id': id,
+ }
+
def _view_look_dom_arch(self, cr, uid, node, view_id, context=None):
- return self['ir.ui.view'].postprocess_and_fields(
+ return self.pool['ir.ui.view'].postprocess_and_fields(
cr, uid, self._name, node, view_id, context=context)
def search_count(self, cr, user, args, context=None):
@@ -2119,7 +2153,7 @@ class BaseModel(object):
pass
- def _read_group_fill_results(self, cr, uid, domain, groupby, groupby_list, aggregated_fields,
+ def _read_group_fill_results(self, cr, uid, domain, groupby, remaining_groupbys, aggregated_fields,
read_group_result, read_group_order=None, context=None):
"""Helper method for filling in empty groups for all possible values of
the field being grouped by"""
@@ -2139,8 +2173,8 @@ class BaseModel(object):
result_template = dict.fromkeys(aggregated_fields, False)
result_template[groupby + '_count'] = 0
- if groupby_list and len(groupby_list) > 1:
- result_template['__context'] = {'group_by': groupby_list[1:]}
+ if remaining_groupbys:
+ result_template['__context'] = {'group_by': remaining_groupbys}
# Merge the left_side (current results as dicts) with the right_side (all
# possible values as m2o pairs). Both lists are supposed to be using the
@@ -2189,7 +2223,141 @@ class BaseModel(object):
r['__fold'] = folded.get(r[groupby] and r[groupby][0], False)
return result
- def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
+ def _read_group_prepare(self, orderby, aggregated_fields, annotated_groupbys, query):
+ """
+ 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 annotated_groupbys: list of dictionaries returned by _read_group_process_groupby
+ These dictionaries contains the qualified name of each groupby
+ (fully qualified SQL name for the corresponding field),
+ and the (non raw) field name.
+ :param osv.Query query: the query under construction
+ :return: (groupby_terms, orderby_terms)
+ """
+ orderby_terms = []
+ groupby_terms = [gb['qualified_field'] for gb in annotated_groupbys]
+ groupby_fields = [gb['groupby'] for gb in annotated_groupbys]
+ if not orderby:
+ return groupby_terms, orderby_terms
+
+ self._check_qorder(orderby)
+ for order_part in orderby.split(','):
+ order_split = order_part.split()
+ order_field = order_split[0]
+ if order_field in groupby_fields:
+
+ if self._all_columns[order_field.split(':')[0]].column._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:
+ order = '"%s" %s' % (order_field, '' if len(order_split) == 1 else order_split[1])
+ orderby_terms.append(order)
+ 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_process_groupby(self, gb, query, context):
+ """
+ Helper method to collect important information about groupbys: raw
+ field name, type, time informations, qualified name, ...
+ """
+ split = gb.split(':')
+ field_type = self._all_columns[split[0]].column._type
+ gb_function = split[1] if len(split) == 2 else None
+ temporal = field_type in ('date', 'datetime')
+ tz_convert = field_type == 'datetime' and context.get('tz') in pytz.all_timezones
+ qualified_field = self._inherits_join_calc(split[0], query)
+ if temporal:
+ display_formats = {
+ 'day': 'dd MMM YYYY',
+ 'week': "'W'w YYYY",
+ 'month': 'MMMM YYYY',
+ 'quarter': 'QQQ YYYY',
+ 'year': 'YYYY'
+ }
+ time_intervals = {
+ 'day': dateutil.relativedelta.relativedelta(days=1),
+ 'week': datetime.timedelta(days=7),
+ 'month': dateutil.relativedelta.relativedelta(months=1),
+ 'quarter': dateutil.relativedelta.relativedelta(months=3),
+ 'year': dateutil.relativedelta.relativedelta(years=1)
+ }
+ if tz_convert:
+ qualified_field = "timezone('%s', timezone('UTC',%s))" % (context.get('tz', 'UTC'), qualified_field)
+ qualified_field = "date_trunc('%s', %s)" % (gb_function or 'month', qualified_field)
+ if field_type == 'boolean':
+ qualified_field = "coalesce(%s,false)" % qualified_field
+ return {
+ 'field': split[0],
+ 'groupby': gb,
+ 'type': field_type,
+ 'display_format': display_formats[gb_function or 'month'] if temporal else None,
+ 'interval': time_intervals[gb_function or 'month'] if temporal else None,
+ 'tz_convert': tz_convert,
+ 'qualified_field': qualified_field
+ }
+
+ def _read_group_prepare_data(self, key, value, groupby_dict, context):
+ """
+ Helper method to sanitize the data received by read_group. The None
+ values are converted to False, and the date/datetime are formatted,
+ and corrected according to the timezones.
+ """
+ value = False if value is None else value
+ gb = groupby_dict.get(key)
+ if gb and gb['type'] in ('date', 'datetime') and value:
+ if isinstance(value, basestring):
+ dt_format = DEFAULT_SERVER_DATETIME_FORMAT if gb['type'] == 'datetime' else DEFAULT_SERVER_DATE_FORMAT
+ value = datetime.datetime.strptime(value, dt_format)
+ if gb['tz_convert']:
+ value = pytz.timezone(context['tz']).localize(value)
+ return value
+
+ def _read_group_get_domain(self, groupby, value):
+ """
+ Helper method to construct the domain corresponding to a groupby and
+ a given value. This is mostly relevant for date/datetime.
+ """
+ if groupby['type'] in ('date', 'datetime') and value:
+ dt_format = DEFAULT_SERVER_DATETIME_FORMAT if groupby['type'] == 'datetime' else DEFAULT_SERVER_DATE_FORMAT
+ domain_dt_begin = value
+ domain_dt_end = value + groupby['interval']
+ if groupby['tz_convert']:
+ domain_dt_begin = domain_dt_begin.astimezone(pytz.utc)
+ domain_dt_end = domain_dt_end.astimezone(pytz.utc)
+ return [(groupby['field'], '>=', domain_dt_begin.strftime(dt_format)),
+ (groupby['field'], '<', domain_dt_end.strftime(dt_format))]
+ if groupby['type'] == 'many2one' and value:
+ value = value[0]
+ return [(groupby['field'], '=', value)]
+
+ def _read_group_format_result(self, data, annotated_groupbys, groupby, groupby_dict, domain, context):
+ """
+ Helper method to format the data contained in the dictianary data by
+ adding the domain corresponding to its values, the groupbys in the
+ context and by properly formatting the date/datetime values.
+ """
+ domain_group = [dom for gb in annotated_groupbys for dom in self._read_group_get_domain(gb, data[gb['groupby']])]
+ for k,v in data.iteritems():
+ gb = groupby_dict.get(k)
+ if gb and gb['type'] in ('date', 'datetime') and v:
+ data[k] = babel.dates.format_date(v, format=gb['display_format'], locale=context.get('lang', 'en_US'))
+
+ data['__domain'] = domain_group + domain
+ if len(groupby) - len(annotated_groupbys) >= 1:
+ data['__context'] = { 'group_by': groupby[len(annotated_groupbys):]}
+ del data['id']
+ return data
+
+ def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
"""
Get the list of records in list view grouped by the given ``groupby`` fields
@@ -2209,6 +2377,9 @@ class BaseModel(object):
overriding the natural sort ordering of the
groups, see also :py:meth:`~osv.osv.osv.search`
(supported only for many2one fields currently)
+ :param bool lazy: if true, the results are only grouped by the first groupby and the
+ remaining groupbys are put in the __context key. If false, all the groupbys are
+ done in one call.
:return: list of dictionaries(one dictionary for each record) containing:
* the values of fields grouped by the fields in ``groupby`` argument
@@ -2217,167 +2388,97 @@ class BaseModel(object):
:rtype: [{'field_name_1': value, ...]
:raise AccessError: * if user has no read rights on the requested object
* if user tries to bypass access rules for read on the requested object
-
"""
- context = context or {}
+ if context is None:
+ context = {}
self.check_access_rights(cr, uid, 'read')
- if not fields:
- fields = self._columns.keys()
+ query = self._where_calc(cr, uid, domain, context=context)
+ fields = fields or self._columns.keys()
+
+ groupby = [groupby] if isinstance(groupby, basestring) else groupby
+ groupby_list = groupby[:1] if lazy else groupby
+ annotated_groupbys = [self._read_group_process_groupby(gb, query, context)
+ for gb in groupby_list]
+ groupby_fields = [g['field'] for g in annotated_groupbys]
+ order = orderby or ','.join([g for g in groupby_list])
+ groupby_dict = {gb['groupby']: gb for gb in annotated_groupbys}
- query = self._where_calc(cr, uid, domain, context=context)
self._apply_ir_rules(cr, uid, query, 'read', context=context)
-
- # Take care of adding join(s) if groupby is an '_inherits'ed field
- groupby_list = groupby
- qualified_groupby_field = groupby
- if groupby:
- if isinstance(groupby, list):
- groupby = groupby[0]
- splitted_groupby = groupby.split(':')
- if len(splitted_groupby) == 2:
- groupby = splitted_groupby[0]
- groupby_function = splitted_groupby[1]
- else:
- groupby_function = False
- qualified_groupby_field = self._inherits_join_calc(groupby, query)
-
- if groupby:
- assert not groupby or groupby in fields, "Fields in 'groupby' must appear in the list of fields to read (perhaps it's missing in the list view?)"
- groupby_def = self._columns.get(groupby) or (self._inherit_fields.get(groupby) and self._inherit_fields.get(groupby)[2])
+ for gb in groupby_fields:
+ assert gb in fields, "Fields in 'groupby' must appear in the list of fields to read (perhaps it's missing in the list view?)"
+ groupby_def = self._columns.get(gb) or (self._inherit_fields.get(gb) and self._inherit_fields.get(gb)[2])
assert groupby_def and groupby_def._classic_write, "Fields in 'groupby' must be regular database-persisted fields (no function or related fields), or function fields with store=True"
-
- # 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 = {}
- if groupby:
- if fget.get(groupby):
- groupby_type = fget[groupby]['type']
- if groupby_type in ('date', 'datetime'):
- if groupby_function:
- interval = groupby_function
- else:
- interval = 'month'
-
- if interval == 'day':
- display_format = 'dd MMM YYYY'
- elif interval == 'week':
- display_format = "'W'w YYYY"
- elif interval == 'month':
- display_format = 'MMMM YYYY'
- elif interval == 'quarter':
- display_format = 'QQQ YYYY'
- elif interval == 'year':
- display_format = 'YYYY'
-
- if groupby_type == 'datetime' and context.get('tz') in pytz.all_timezones:
- # Convert groupby result to user TZ to avoid confusion!
- # PostgreSQL is compatible with all pytz timezone names, so we can use them
- # directly for conversion, starting with timestamps stored in UTC.
- 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
- else:
+ if not (gb in self._all_columns):
# Don't allow arbitrary values, as this would be a SQL injection vector!
raise except_orm(_('Invalid group_by'),
- _('Invalid group_by specification: "%s".\nA group_by specification must be a list of valid fields.')%(groupby,))
+ _('Invalid group_by specification: "%s".\nA group_by specification must be a list of valid fields.')%(gb,))
aggregated_fields = [
f for f in fields
if f not in ('id', 'sequence')
- 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)
+ if f not in groupby_fields
+ if self._all_columns[f].column._type in ('integer', 'float')
+ if getattr(self._all_columns[f].column, '_classic_write')]
- gb = groupby and (' GROUP BY ' + qualified_groupby_field) or ''
+ field_formatter = lambda f: (self._all_columns[f].column.group_operator or 'sum', self._inherits_join_calc(f, query), f)
+ select_terms = ["%s(%s) AS %s" % field_formatter(f) for f in aggregated_fields]
+ for gb in annotated_groupbys:
+ select_terms.append('%s as "%s" ' % (gb['qualified_field'], gb['groupby']))
+
+ groupby_terms, orderby_terms = self._read_group_prepare(order, aggregated_fields, annotated_groupbys, query)
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 + limit_str + offset_str, where_clause_params)
- alldata = {}
- groupby = group_by
- for r in cr.dictfetchall():
- for fld, val in r.items():
- if val is None: r[fld] = False
- alldata[r['id']] = r
- del r['id']
-
- order = orderby or groupby
- data_ids = self.search(cr, uid, [('id', 'in', alldata.keys())], order=order, context=context)
-
- # the IDs of records that have groupby field value = False or '' should be included too
- data_ids += set(alldata.keys()).difference(data_ids)
-
- 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):
- data_dict = dict((d['id'], d[groupby] ) for d in data)
- result = [{'id': i, groupby: data_dict[i]} for i in data_ids]
+ if lazy and (len(groupby_fields) >= 2 or not context.get('group_by_no_leaf')):
+ count_field = groupby_fields[0] if len(groupby_fields) >= 1 else '_'
else:
- result = [{'id': i} for i in data_ids]
+ count_field = '_'
- for d in result:
- if groupby:
- d['__domain'] = [(groupby, '=', alldata[d['id']][groupby] or False)] + domain
- if not isinstance(groupby_list, (str, unicode)):
- if groupby or not context.get('group_by_no_leaf', False):
- d['__context'] = {'group_by': groupby_list[1:]}
- if groupby and groupby in fget:
- groupby_type = fget[groupby]['type']
- if d[groupby] and groupby_type in ('date', 'datetime'):
- groupby_datetime = alldata[d['id']][groupby]
- if isinstance(groupby_datetime, basestring):
- _default = datetime.datetime(1970, 1, 1) # force starts of month
- groupby_datetime = dateutil.parser.parse(groupby_datetime, default=_default)
- tz_convert = groupby_type == 'datetime' and context.get('tz') in pytz.all_timezones
- if tz_convert:
- groupby_datetime = pytz.timezone(context['tz']).localize(groupby_datetime)
- d[groupby] = babel.dates.format_date(
- groupby_datetime, format=display_format, locale=context.get('lang', 'en_US'))
- domain_dt_begin = groupby_datetime
- if interval == 'quarter':
- domain_dt_end = groupby_datetime + dateutil.relativedelta.relativedelta(months=3)
- elif interval == 'month':
- domain_dt_end = groupby_datetime + dateutil.relativedelta.relativedelta(months=1)
- elif interval == 'week':
- domain_dt_end = groupby_datetime + datetime.timedelta(days=7)
- elif interval == 'day':
- domain_dt_end = groupby_datetime + datetime.timedelta(days=1)
- else:
- domain_dt_end = groupby_datetime + dateutil.relativedelta.relativedelta(years=1)
- if tz_convert:
- # the time boundaries were all computed in the apparent TZ of the user,
- # so we need to convert them to UTC to have proper server-side values.
- domain_dt_begin = domain_dt_begin.astimezone(pytz.utc)
- domain_dt_end = domain_dt_end.astimezone(pytz.utc)
- dt_format = DEFAULT_SERVER_DATETIME_FORMAT if groupby_type == 'datetime' else DEFAULT_SERVER_DATE_FORMAT
- d['__domain'] = [(groupby, '>=', domain_dt_begin.strftime(dt_format)),
- (groupby, '<', domain_dt_end.strftime(dt_format))] + domain
- del alldata[d['id']][groupby]
- d.update(alldata[d['id']])
- del d['id']
+ prefix_terms = lambda prefix, terms: (prefix + " " + ",".join(terms)) if terms else ''
+ prefix_term = lambda prefix, term: ('%s %s' % (prefix, term)) if term else ''
- if groupby and groupby in self._group_by_full:
- result = self._read_group_fill_results(cr, uid, domain, groupby, groupby_list,
- aggregated_fields, result, read_group_order=order,
- context=context)
+ 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)
+ fetched_data = cr.dictfetchall()
+ if not groupby_fields:
+ return {r.pop('id'): r for r in fetched_data}
+
+ many2onefields = [gb['field'] for gb in annotated_groupbys if gb['type'] == 'many2one']
+ if many2onefields:
+ data_ids = [r['id'] for r in fetched_data]
+ many2onefields = list(set(many2onefields))
+ data_dict = {d['id']: d for d in self.read(cr, uid, data_ids, many2onefields, context=context)}
+ for d in fetched_data:
+ d.update(data_dict[d['id']])
+
+ data = map(lambda r: {k: self._read_group_prepare_data(k,v, groupby_dict, context) for k,v in r.iteritems()}, fetched_data)
+ result = [self._read_group_format_result(d, annotated_groupbys, groupby, groupby_dict, domain, context) for d in data]
+ if lazy and groupby_fields[0] in self._group_by_full:
+ # Right now, read_group only fill results in lazy mode (by default).
+ # If you need to have the empty groups in 'eager' mode, then the
+ # method _read_group_fill_results need to be completely reimplemented
+ # in a sane way
+ result = self._read_group_fill_results(cr, uid, domain, groupby_fields[0], groupby[len(annotated_groupbys):],
+ aggregated_fields, result, read_group_order=order,
+ context=context)
return result
def _inherits_join_add(self, current_model, parent_model_name, query):
@@ -2694,7 +2795,7 @@ class BaseModel(object):
f_pg_notnull = res['attnotnull']
if isinstance(f, fields.function) and not f.store and\
not getattr(f, 'nodrop', False):
- _logger.info('column %s (%s) in table %s removed: converted to a function !\n',
+ _logger.info('column %s (%s) converted to a function, removed from table %s',
k, f.string, self._table)
cr.execute('ALTER TABLE "%s" DROP COLUMN "%s" CASCADE' % (self._table, k))
cr.commit()
@@ -2716,10 +2817,16 @@ class BaseModel(object):
('float8', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]),
]
if f_pg_type == 'varchar' and f._type == 'char' and ((f.size is None and f_pg_size) or f_pg_size < f.size):
- cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k))
- cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, pg_varchar(f.size)))
- cr.execute('UPDATE "%s" SET "%s"=temp_change_size::%s' % (self._table, k, pg_varchar(f.size)))
- cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,))
+ try:
+ with cr.savepoint():
+ cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" TYPE %s' % (self._table, k, pg_varchar(f.size)))
+ except psycopg2.NotSupportedError:
+ # In place alter table cannot be done because a view is depending of this field.
+ # Do a manual copy. This will drop the view (that will be recreated later)
+ cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k))
+ cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, pg_varchar(f.size)))
+ cr.execute('UPDATE "%s" SET "%s"=temp_change_size::%s' % (self._table, k, pg_varchar(f.size)))
+ cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,))
cr.commit()
_schema.debug("Table '%s': column '%s' (type varchar) changed size from %s to %s",
self._table, k, f_pg_size or 'unlimited', f.size or 'unlimited')
@@ -2727,10 +2834,10 @@ class BaseModel(object):
if (f_pg_type==c[0]) and (f._type==c[1]):
if f_pg_type != f_obj_type:
ok = True
- cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k))
+ cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO __temp_type_cast' % (self._table, k))
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, c[2]))
- cr.execute(('UPDATE "%s" SET "%s"=temp_change_size'+c[3]) % (self._table, k))
- cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,))
+ cr.execute(('UPDATE "%s" SET "%s"= __temp_type_cast'+c[3]) % (self._table, k))
+ cr.execute('ALTER TABLE "%s" DROP COLUMN __temp_type_cast CASCADE' % (self._table,))
cr.commit()
_schema.debug("Table '%s': column '%s' changed type from %s to %s",
self._table, k, c[0], c[1])
@@ -2807,7 +2914,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)
@@ -2842,7 +2949,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]
@@ -4187,7 +4294,7 @@ class BaseModel(object):
if isinstance(select, (int, long)):
return browse_record(cr, uid, select, self, cache, context=context, list_class=self._list_class, fields_process=fields_process)
elif isinstance(select, list):
- return self._list_class([browse_record(cr, uid, id, self, cache, context=context, list_class=self._list_class, fields_process=fields_process) for id in select], context=context)
+ return self._list_class((browse_record(cr, uid, id, self, cache, context=context, list_class=self._list_class, fields_process=fields_process) for id in select), context=context)
else:
return browse_null()
@@ -4622,24 +4729,22 @@ class BaseModel(object):
blacklist_given_fields(self)
- fields_to_read = [f for f in self.check_field_access_rights(cr, uid, 'read', None)
- if f not in blacklist]
- data = self.read(cr, uid, [id], fields_to_read, context=context)
+
+ fields_to_copy = dict((f,fi) for f, fi in self._all_columns.iteritems()
+ if f not in default
+ if f not in blacklist
+ if not isinstance(fi.column, fields.function))
+
+ data = self.read(cr, uid, [id], fields_to_copy.keys(), context=context)
if data:
data = data[0]
else:
- raise IndexError(_("Record #%d of %s not found, cannot copy!") % (id, self._name))
+ raise IndexError( _("Record #%d of %s not found, cannot copy!") %( id, self._name))
res = dict(default)
- for f, colinfo in self._all_columns.items():
+ for f, colinfo in fields_to_copy.iteritems():
field = colinfo.column
- if f in default:
- pass
- elif f in blacklist:
- pass
- elif isinstance(field, fields.function):
- pass
- elif field._type == 'many2one':
+ if field._type == 'many2one':
res[f] = data[f] and data[f][0]
elif field._type == 'one2many':
other = self.pool[field._obj]
diff --git a/openerp/pooler.py b/openerp/pooler.py
index 0df6f88c819..5ada4c2a014 100644
--- a/openerp/pooler.py
+++ b/openerp/pooler.py
@@ -36,7 +36,7 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False)
assert openerp.conf.deprecation.openerp_pooler
_logger.warning('openerp.pooler.get_db_and_pool() is deprecated.')
registry = RegistryManager.get(db_name, force_demo, status, update_module)
- return registry.db, registry
+ return registry._db, registry
def restart_pool(db_name, force_demo=False, status=None, update_module=False):
@@ -44,7 +44,7 @@ def restart_pool(db_name, force_demo=False, status=None, update_module=False):
_logger.warning('openerp.pooler.restart_pool() is deprecated.')
assert openerp.conf.deprecation.openerp_pooler
registry = RegistryManager.new(db_name, force_demo, status, update_module)
- return registry.db, registry
+ return registry._db, registry
def get_db(db_name):
"""Return a database connection. The corresponding registry is initialized."""
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/report/render/rml2pdf/trml2pdf.py b/openerp/report/render/rml2pdf/trml2pdf.py
index ad1ff656e1d..6ce69578a8e 100644
--- a/openerp/report/render/rml2pdf/trml2pdf.py
+++ b/openerp/report/render/rml2pdf/trml2pdf.py
@@ -88,50 +88,30 @@ def _open_image(filename, path=None):
class NumberedCanvas(canvas.Canvas):
def __init__(self, *args, **kwargs):
canvas.Canvas.__init__(self, *args, **kwargs)
- self._codes = []
- self._flag=False
- self._pageCount=0
- self._currentPage =0
- self._pageCounter=0
- self.pages={}
+ self._saved_page_states = []
def showPage(self):
- self._currentPage +=1
- if not self._flag:
- self._pageCount += 1
- else:
- self.pages.update({self._currentPage:self._pageCount})
- self._codes.append({'code': self._code, 'stack': self._codeStack})
+ self._saved_page_states.append(dict(self.__dict__))
self._startPage()
- self._flag=False
- def pageCount(self):
- if self.pages.get(self._pageCounter,False):
- self._pageNumber=0
- self._pageCounter +=1
- key=self._pageCounter
- if not self.pages.get(key,False):
- while not self.pages.get(key,False):
- key += 1
+ def save(self):
+ """add page info to each page (page x of y)"""
+ for state in self._saved_page_states:
+ self.__dict__.update(state)
+ self.draw_page_number()
+ canvas.Canvas.showPage(self)
+ canvas.Canvas.save(self)
+
+ def draw_page_number(self):
+ page_count = len(self._saved_page_states)
self.setFont("Helvetica", 8)
self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40),
" %(this)i / %(total)i" % {
'this': self._pageNumber+1,
- 'total': self.pages.get(key,False),
+ 'total': page_count,
}
)
- def save(self):
- """add page info to each page (page x of y)"""
- # reset page counter
- self._pageNumber = 0
- for code in self._codes:
- self._code = code['code']
- self._codeStack = code['stack']
- self.pageCount()
- canvas.Canvas.showPage(self)
-# self.restoreState()
- self._doc.SaveToFile(self._filename, self)
class PageCount(platypus.Flowable):
def __init__(self, story_count=0):
@@ -303,6 +283,9 @@ class _rml_doc(object):
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
+ if mode:
+ mode = mode.lower()
+
if fontname not in pdfmetrics._fonts:
pdfmetrics.registerFont(TTFont(fontname, filename))
if mode == 'all':
@@ -310,14 +293,14 @@ class _rml_doc(object):
addMapping(face, 0, 1, fontname) #italic
addMapping(face, 1, 0, fontname) #bold
addMapping(face, 1, 1, fontname) #italic and bold
- elif (mode== 'normal') or (mode == 'regular') or (mode == 'book'):
- addMapping(face, 0, 0, fontname) #normal
- elif mode == 'italic':
+ elif mode in ['italic', 'oblique']:
addMapping(face, 0, 1, fontname) #italic
elif mode == 'bold':
addMapping(face, 1, 0, fontname) #bold
- elif mode == 'bolditalic':
+ elif mode in ('bolditalic', 'bold italic','boldoblique', 'bold oblique'):
addMapping(face, 1, 1, fontname) #italic and bold
+ else:
+ addMapping(face, 0, 0, fontname) #normal
def _textual_image(self, node):
rc = ''
@@ -982,7 +965,7 @@ class _rml_template(object):
}
pageSize = A4
if self.localcontext.get('company'):
- pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4)
+ pageSize = pagesize_map.get(self.localcontext.get('company').rml_paper_format, A4)
if node.get('pageSize'):
ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
diff --git a/openerp/service/db.py b/openerp/service/db.py
index fa1bc1336fc..bb97e72f992 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,19 @@ 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))
+
+ from_fs = openerp.tools.config.filestore(db_original_name)
+ to_fs = openerp.tools.config.filestore(db_name)
+ if os.path.exists(from_fs) and not os.path.exists(to_fs):
+ shutil.copy(from_fs, to_fs)
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 +148,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 +160,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:
@@ -176,96 +183,146 @@ def exp_drop(db_name):
raise Exception("Couldn't drop database %s: %s" % (db_name, e))
else:
_logger.info('DROP DB: %s', db_name)
+
+ fs = openerp.tools.config.filestore(db_name)
+ if os.path.exists(fs):
+ shutil.rmtree(fs)
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 +330,21 @@ 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)
+ old_fs = openerp.tools.config.filestore(old_name)
+ new_fs = openerp.tools.config.filestore(new_name)
+ if os.path.exists(old_fs) and not os.path.exists(new_fs):
+ shutil.move(old_fs, new_fs)
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))
@@ -311,7 +369,7 @@ def exp_list(document=False):
cr.execute("select datname from pg_database where datdba=(select usesysid from pg_user where usename=%s) and datname not in %s order by datname", (db_user, templates_list))
else:
cr.execute("select datname from pg_database where datname not in %s order by datname", (templates_list,))
- res = [str(name) for (name,) in cr.fetchall()]
+ res = [openerp.tools.ustr(name) for (name,) in cr.fetchall()]
except Exception:
res = []
res.sort()
diff --git a/openerp/service/http_server.py b/openerp/service/http_server.py
deleted file mode 100644
index e25e75dba62..00000000000
--- a/openerp/service/http_server.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright P. Christeas 2008-2010
-# Copyright 2010 OpenERP SA. (http://www.openerp.com)
-#
-#
-# WARNING: This program as such is intended to be used by professional
-# programmers who take the whole responsibility of assessing all potential
-# consequences resulting from its eventual inadequacies and bugs
-# End users who are looking for a ready-to-use solution with commercial
-# guarantees and support are strongly advised to contract a Free Software
-# Service Company
-#
-# This program is Free Software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-###############################################################################
-
-
-""" This module offers the family of HTTP-based servers. These are not a single
- class/functionality, but a set of network stack layers, implementing
- extendable HTTP protocols.
-
- The OpenERP server defines a single instance of a HTTP server, listening at
- the standard 8069, 8071 ports (well, it is 2 servers, and ports are
- configurable, of course). This "single" server then uses a `MultiHTTPHandler`
- to dispatch requests to the appropriate channel protocol, like the XML-RPC,
- static HTTP, DAV or other.
-"""
-
-import base64
-import posixpath
-import urllib
-import os
-import logging
-
-from websrv_lib import *
-import openerp.tools as tools
-
-try:
- import fcntl
-except ImportError:
- fcntl = None
-
-try:
- from ssl import SSLError
-except ImportError:
- class SSLError(Exception): pass
-
-_logger = logging.getLogger(__name__)
-
-# TODO delete this for 6.2, it is still needed for 6.1.
-class HttpLogHandler:
- """ helper class for uniform log handling
- Please define self._logger at each class that is derived from this
- """
- _logger = None
-
- def log_message(self, format, *args):
- self._logger.debug(format % args) # todo: perhaps other level
-
- def log_error(self, format, *args):
- self._logger.error(format % args)
-
- def log_exception(self, format, *args):
- self._logger.exception(format, *args)
-
- def log_request(self, code='-', size='-'):
- self._logger.debug('"%s" %s %s',
- self.requestline, str(code), str(size))
-
-class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler):
- _logger = logging.getLogger(__name__)
-
- _HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD'] }
-
- def __init__(self,request, client_address, server):
- HTTPHandler.__init__(self,request,client_address,server)
- document_root = tools.config.get('static_http_document_root', False)
- assert document_root, "Please specify static_http_document_root in configuration, or disable static-httpd!"
- self.__basepath = document_root
-
- def translate_path(self, path):
- """Translate a /-separated PATH to the local filename syntax.
-
- Components that mean special things to the local file system
- (e.g. drive or directory names) are ignored. (XXX They should
- probably be diagnosed.)
-
- """
- # abandon query parameters
- path = path.split('?',1)[0]
- path = path.split('#',1)[0]
- path = posixpath.normpath(urllib.unquote(path))
- words = path.split('/')
- words = filter(None, words)
- path = self.__basepath
- for word in words:
- if word in (os.curdir, os.pardir): continue
- path = os.path.join(path, word)
- return path
-
-def init_static_http():
- if not tools.config.get('static_http_enable', False):
- return
-
- document_root = tools.config.get('static_http_document_root', False)
- assert document_root, "Document root must be specified explicitly to enable static HTTP service (option --static-http-document-root)"
-
- base_path = tools.config.get('static_http_url_prefix', '/')
-
- reg_http_service(base_path, StaticHTTPHandler)
-
- _logger.info("Registered HTTP dir %s for %s", document_root, base_path)
-
-import security
-
-class OpenERPAuthProvider(AuthProvider):
- """ Require basic authentication."""
- def __init__(self,realm='OpenERP User'):
- self.realm = realm
- self.auth_creds = {}
- self.auth_tries = 0
- self.last_auth = None
-
- def authenticate(self, db, user, passwd, client_address):
- try:
- uid = security.login(db,user,passwd)
- if uid is False:
- return False
- return user, passwd, db, uid
- except Exception,e:
- _logger.debug("Fail auth: %s" % e )
- return False
-
- def checkRequest(self,handler,path, db=False):
- auth_str = handler.headers.get('Authorization',False)
- try:
- if not db:
- db = handler.get_db_from_path(path)
- except Exception:
- if path.startswith('/'):
- path = path[1:]
- psp= path.split('/')
- if len(psp)>1:
- db = psp[0]
- else:
- #FIXME!
- _logger.info("Wrong path: %s, failing auth" %path)
- raise AuthRejectedExc("Authorization failed. Wrong sub-path.")
- if self.auth_creds.get(db):
- return True
- if auth_str and auth_str.startswith('Basic '):
- auth_str=auth_str[len('Basic '):]
- (user,passwd) = base64.decodestring(auth_str).split(':')
- _logger.info("Found user=\"%s\", passwd=\"***\" for db=\"%s\"", user, db)
- acd = self.authenticate(db,user,passwd,handler.client_address)
- if acd != False:
- self.auth_creds[db] = acd
- self.last_auth = db
- return True
- if self.auth_tries > 5:
- _logger.info("Failing authorization after 5 requests w/o password")
- raise AuthRejectedExc("Authorization failed.")
- self.auth_tries += 1
- raise AuthRequiredExc(atype='Basic', realm=self.realm)
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/service/model.py b/openerp/service/model.py
index a637e234295..a91baf8ca08 100644
--- a/openerp/service/model.py
+++ b/openerp/service/model.py
@@ -161,21 +161,10 @@ def execute_cr(cr, uid, obj, method, *args, **kw):
def execute_kw(db, uid, obj, method, args, kw=None):
return execute(db, uid, obj, method, *args, **kw or {})
-@contextmanager
-def closing_cr_and_commit(cr):
- try:
- yield cr
- cr.commit()
- except Exception:
- cr.rollback()
- raise
- finally:
- cr.close()
-
@check
def execute(db, uid, obj, method, *args, **kw):
threading.currentThread().dbname = db
- with closing_cr_and_commit(openerp.registry(db).db.cursor()) as cr:
+ with openerp.registry(db).cursor() as cr:
if method.startswith('_'):
raise except_orm('Access Denied', 'Private methods (such as %s) cannot be called remotely.' % (method,))
res = execute_cr(cr, uid, obj, method, *args, **kw)
@@ -190,7 +179,7 @@ def exec_workflow_cr(cr, uid, obj, signal, *args):
@check
def exec_workflow(db, uid, obj, signal, *args):
- with closing_cr_and_commit(openerp.registry(db).db.cursor()) as cr:
+ with openerp.registry(db).cursor() as cr:
return exec_workflow_cr(cr, uid, obj, signal, *args)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/service/report.py b/openerp/service/report.py
index 370b11fc199..0c2f720f4cb 100644
--- a/openerp/service/report.py
+++ b/openerp/service/report.py
@@ -49,7 +49,7 @@ def exp_render_report(db, uid, object, ids, datas=None, context=None):
self_reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
- cr = openerp.registry(db).db.cursor()
+ cr = openerp.registry(db).cursor()
try:
result, format = openerp.report.render_report(cr, uid, ids, object, datas, context)
if not result:
@@ -87,7 +87,7 @@ def exp_report(db, uid, object, ids, datas=None, context=None):
self_reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
def go(id, uid, ids, datas, context):
- cr = openerp.registry(db).db.cursor()
+ cr = openerp.registry(db).cursor()
try:
result, format = openerp.report.render_report(cr, uid, ids, object, datas, context)
if not result:
diff --git a/openerp/service/server.py b/openerp/service/server.py
index 5372459cd41..76b415eb01e 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
@@ -61,6 +62,14 @@ class BaseWSGIServerNoBind(werkzeug.serving.BaseWSGIServer):
# dont listen as we use PreforkServer#socket
pass
+
+class RequestHandler(werkzeug.serving.WSGIRequestHandler):
+ def setup(self):
+ # flag the current thread as handling a http request
+ super(RequestHandler, self).setup()
+ me = threading.currentThread()
+ me.name = 'openerp.service.http.request.%s' % (me.ident,)
+
# _reexec() should set LISTEN_* to avoid connection refused during reload time. It
# should also work with systemd socket activation. This is currently untested
# and not yet used.
@@ -70,6 +79,10 @@ class ThreadedWSGIServerReloadable(werkzeug.serving.ThreadedWSGIServer):
given by the environement, this is used by autoreload to keep the listen
socket open when a reload happens.
"""
+ def __init__(self, host, port, app):
+ super(ThreadedWSGIServerReloadable, self).__init__(host, port, app,
+ handler=RequestHandler)
+
def server_bind(self):
envfd = os.environ.get('LISTEN_FDS')
if envfd and os.environ.get('LISTEN_PID') == str(os.getpid()):
@@ -110,15 +123,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 +138,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 +158,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 +225,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 +239,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 +269,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 +290,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 +300,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 +331,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 +452,7 @@ class PreforkServer(CommonServer):
sys.exit(0)
def long_polling_spawn(self):
- nargs = stripped_sys_argv('--pidfile','--workers')
+ nargs = stripped_sys_argv()
cmd = nargs[0]
cmd = os.path.join(os.path.dirname(cmd), "openerp-gevent")
nargs[0] = cmd
@@ -447,10 +461,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 +480,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 +516,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 +532,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 +572,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 +596,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 +615,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 +647,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 +667,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 +715,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 +733,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 +764,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 +786,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 +795,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 +839,22 @@ 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', {}, 'init')
+ if config['test_commit']:
+ _logger.info('test %s has been commited', test_file)
+ cr.commit()
+ else:
+ _logger.info('test %s has been rollbacked', test_file)
+ 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 +865,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,30 +879,35 @@ 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.
"""
global server
load_server_wide_modules()
- if config['workers']:
- server = PreforkServer(openerp.service.wsgi_server.application)
- elif openerp.evented:
+ if openerp.evented:
server = GeventServer(openerp.service.wsgi_server.application)
+ elif config['workers']:
+ server = PreforkServer(openerp.service.wsgi_server.application)
else:
server = ThreadedServer(openerp.service.wsgi_server.application)
diff --git a/openerp/service/wsgi_server.py b/openerp/service/wsgi_server.py
index 506a07d0295..5e72ba570b5 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:
@@ -164,153 +164,6 @@ def wsgi_xmlrpc(environ, start_response):
params, method = xmlrpclib.loads(data)
return xmlrpc_return(start_response, service, method, params, string_faultcode)
-def wsgi_webdav(environ, start_response):
- pi = environ['PATH_INFO']
- if environ['REQUEST_METHOD'] == 'OPTIONS' and pi in ['*','/']:
- return return_options(environ, start_response)
- elif pi.startswith('/webdav'):
- http_dir = websrv_lib.find_http_service(pi)
- if http_dir:
- path = pi[len(http_dir.path):]
- if path.startswith('/'):
- environ['PATH_INFO'] = path
- else:
- environ['PATH_INFO'] = '/' + path
- return http_to_wsgi(http_dir)(environ, start_response)
-
-def return_options(environ, start_response):
- # Microsoft specific header, see
- # http://www.ibm.com/developerworks/rational/library/2089.html
- if 'Microsoft' in environ.get('User-Agent', ''):
- options = [('MS-Author-Via', 'DAV')]
- else:
- options = []
- options += [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')]
- start_response("200 OK", [('Content-Length', str(0))] + options)
- return []
-
-def http_to_wsgi(http_dir):
- """
- Turn a BaseHTTPRequestHandler into a WSGI entry point.
-
- Actually the argument is not a bare BaseHTTPRequestHandler but is wrapped
- (as a class, so it needs to be instanciated) in a HTTPDir.
-
- This code is adapted from wbsrv_lib.MultiHTTPHandler._handle_one_foreign().
- It is a temporary solution: the HTTP sub-handlers (in particular the
- document_webdav addon) have to be WSGIfied.
- """
- def wsgi_handler(environ, start_response):
-
- headers = {}
- for key, value in environ.items():
- if key.startswith('HTTP_'):
- key = key[5:].replace('_', '-').title()
- headers[key] = value
- if key == 'CONTENT_LENGTH':
- key = key.replace('_', '-').title()
- headers[key] = value
- if environ.get('Content-Type'):
- headers['Content-Type'] = environ['Content-Type']
-
- path = urllib.quote(environ.get('PATH_INFO', ''))
- if environ.get('QUERY_STRING'):
- path += '?' + environ['QUERY_STRING']
-
- request_version = 'HTTP/1.1' # TODO
- request_line = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version)
-
- class Dummy(object):
- pass
-
- # Let's pretend we have a server to hand to the handler.
- server = Dummy()
- server.server_name = environ['SERVER_NAME']
- server.server_port = int(environ['SERVER_PORT'])
-
- # Initialize the underlying handler and associated auth. provider.
- con = openerp.service.websrv_lib.noconnection(environ['wsgi.input'])
- handler = http_dir.instanciate_handler(con, environ['REMOTE_ADDR'], server)
-
- # Populate the handler as if it is called by a regular HTTP server
- # and the request is already parsed.
- handler.wfile = StringIO.StringIO()
- handler.rfile = environ['wsgi.input']
- handler.headers = headers
- handler.command = environ['REQUEST_METHOD']
- handler.path = path
- handler.request_version = request_version
- handler.close_connection = 1
- handler.raw_requestline = request_line
- handler.requestline = request_line
-
- # Handle authentication if there is an auth. provider associated to
- # the handler.
- if hasattr(handler, 'auth_provider'):
- try:
- handler.auth_provider.checkRequest(handler, path)
- except websrv_lib.AuthRequiredExc, ae:
- # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
- # authorisation features of HTTP/1.1
- if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')):
- start_response("403 Forbidden", [])
- return []
- start_response("401 Authorization required", [
- ('WWW-Authenticate', '%s realm="%s"' % (ae.atype,ae.realm)),
- # ('Connection', 'keep-alive'),
- ('Content-Type', 'text/html'),
- ('Content-Length', 4), # len(self.auth_required_msg)
- ])
- return ['Blah'] # self.auth_required_msg
- except websrv_lib.AuthRejectedExc,e:
- start_response("403 %s" % (e.args[0],), [])
- return []
-
- method_name = 'do_' + handler.command
-
- # Support the OPTIONS method even when not provided directly by the
- # handler. TODO I would prefer to remove it and fix the handler if
- # needed.
- if not hasattr(handler, method_name):
- if handler.command == 'OPTIONS':
- return return_options(environ, start_response)
- start_response("501 Unsupported method (%r)" % handler.command, [])
- return []
-
- # Finally, call the handler's method.
- try:
- method = getattr(handler, method_name)
- method()
- # The DAV handler buffers its output and provides a _flush()
- # method.
- getattr(handler, '_flush', lambda: None)()
- response = parse_http_response(handler.wfile.getvalue())
- response_headers = response.getheaders()
- body = response.read()
- start_response(str(response.status) + ' ' + response.reason, response_headers)
- return [body]
- except (websrv_lib.AuthRejectedExc, websrv_lib.AuthRequiredExc):
- raise
- except Exception, e:
- start_response("500 Internal error", [])
- return []
-
- return wsgi_handler
-
-def parse_http_response(s):
- """ Turn a HTTP response string into a httplib.HTTPResponse object."""
- class DummySocket(StringIO.StringIO):
- """
- This is used to provide a StringIO to httplib.HTTPResponse
- which, instead of taking a file object, expects a socket and
- uses its makefile() method.
- """
- def makefile(self, *args, **kw):
- return self
- response = httplib.HTTPResponse(DummySocket(s))
- response.begin()
- return response
-
# WSGI handlers registered through the register_wsgi_handler() function below.
module_handlers = []
# RPC endpoints registered through the register_rpc_endpoint() function below.
@@ -342,7 +195,7 @@ def application_unproxied(environ, start_response):
del threading.current_thread().dbname
# Try all handlers until one returns some result (i.e. not None).
- wsgi_handlers = [wsgi_xmlrpc, wsgi_webdav]
+ wsgi_handlers = [wsgi_xmlrpc]
wsgi_handlers += module_handlers
for handler in wsgi_handlers:
result = handler(environ, start_response)
diff --git a/openerp/sql_db.py b/openerp/sql_db.py
index de9d21dcd82..3e401347a18 100644
--- a/openerp/sql_db.py
+++ b/openerp/sql_db.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
@@ -27,8 +27,11 @@ the database, *not* a database abstraction toolkit. Database abstraction is what
the ORM does, in fact.
"""
+from contextlib import contextmanager
from functools import wraps
import logging
+import time
+import uuid
import psycopg2.extensions
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ
from psycopg2.pool import PoolError
@@ -145,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
@@ -162,9 +165,9 @@ 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.__pool = pool
self.dbname = dbname
# Whether to enable snapshot isolation level for this cursor.
@@ -177,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
@@ -186,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
@@ -299,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()
@@ -310,7 +313,7 @@ class Cursor(object):
chosen_template = tools.config['db_template']
templates_list = tuple(set(['template0', 'template1', 'postgres', chosen_template]))
keep_in_pool = self.dbname not in templates_list
- self._pool.give_back(self._cnx, keep_in_pool=keep_in_pool)
+ self.__pool.give_back(self._cnx, keep_in_pool=keep_in_pool)
@check
def autocommit(self, on):
@@ -344,10 +347,77 @@ class Cursor(object):
"""
return self._cnx.rollback()
+ def __enter__(self):
+ """ Using the cursor as a contextmanager automatically commits and
+ closes it::
+
+ with cr:
+ cr.execute(...)
+
+ # cr is committed if no failure occurred
+ # cr is closed in any case
+ """
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if exc_type is None:
+ self.commit()
+ self.close()
+
+ @contextmanager
+ @check
+ def savepoint(self):
+ """context manager entering in a new savepoint"""
+ name = uuid.uuid1().hex
+ self.execute('SAVEPOINT "%s"' % name)
+ try:
+ yield
+ self.execute('RELEASE SAVEPOINT "%s"' % name)
+ except:
+ self.execute('ROLLBACK TO SAVEPOINT "%s"' % name)
+ raise
+
@check
def __getattr__(self, name):
return getattr(self._obj, name)
+class TestCursor(Cursor):
+ """ A cursor to be used for tests. It keeps the transaction open across
+ several requests, and simulates committing, rolling back, and closing.
+ """
+ def __init__(self, *args, **kwargs):
+ super(TestCursor, self).__init__(*args, **kwargs)
+ # in order to simulate commit and rollback, the cursor maintains a
+ # savepoint at its last commit
+ self.execute("SAVEPOINT test_cursor")
+ # we use a lock to serialize concurrent requests
+ self._lock = threading.RLock()
+
+ def acquire(self):
+ self._lock.acquire()
+
+ def release(self):
+ self._lock.release()
+
+ def force_close(self):
+ super(TestCursor, self).close()
+
+ def close(self):
+ if not self._closed:
+ self.rollback() # for stuff that has not been committed
+ self.release()
+
+ def autocommit(self, on):
+ _logger.debug("TestCursor.autocommit(%r) does nothing", on)
+
+ def commit(self):
+ self.execute("RELEASE SAVEPOINT test_cursor")
+ self.execute("SAVEPOINT test_cursor")
+
+ def rollback(self):
+ self.execute("ROLLBACK TO SAVEPOINT test_cursor")
+ self.execute("SAVEPOINT test_cursor")
+
class PsycoConnection(psycopg2.extensions.connection):
pass
@@ -468,12 +538,17 @@ class Connection(object):
def __init__(self, pool, dbname):
self.dbname = dbname
- self._pool = pool
+ self.__pool = pool
def cursor(self, serialized=True):
cursor_type = serialized and 'serialized ' or ''
_logger.debug('create %scursor to %r', cursor_type, self.dbname)
- return Cursor(self._pool, self.dbname, serialized=serialized)
+ return Cursor(self.__pool, self.dbname, serialized=serialized)
+
+ def test_cursor(self, serialized=True):
+ cursor_type = serialized and 'serialized ' or ''
+ _logger.debug('create test %scursor to %r', cursor_type, self.dbname)
+ return TestCursor(self.__pool, self.dbname, serialized=serialized)
# serialized_cursor is deprecated - cursors are serialized by default
serialized_cursor = cursor
diff --git a/openerp/tests/common.py b/openerp/tests/common.py
index dd0350aa7ca..e881959a31b 100644
--- a/openerp/tests/common.py
+++ b/openerp/tests/common.py
@@ -4,19 +4,23 @@ The module :mod:`openerp.tests.common` provides unittest2 test cases and a few
helpers and classes to write tests.
"""
+import errno
import json
import logging
import os
import select
import subprocess
-import sys
import threading
import time
import unittest2
-import uuid
+import urllib2
import xmlrpclib
+from datetime import datetime, timedelta
+
+import werkzeug
import openerp
+from openerp.modules.registry import RegistryManager
_logger = logging.getLogger(__name__)
@@ -25,36 +29,50 @@ ADDONS_PATH = openerp.tools.config['addons_path']
HOST = '127.0.0.1'
PORT = openerp.tools.config['xmlrpc_port']
DB = openerp.tools.config['db_name']
-
# If the database name is not provided on the command-line,
# use the one on the thread (which means if it is provided on
# the command-line, this will break when installing another
# database from XML-RPC).
if not DB and hasattr(threading.current_thread(), 'dbname'):
DB = threading.current_thread().dbname
-
-ADMIN_USER = 'admin'
+# Useless constant, tests are aware of the content of demo data
ADMIN_USER_ID = openerp.SUPERUSER_ID
-ADMIN_PASSWORD = 'admin'
-HTTP_SESSION = {}
+def at_install(flag):
+ """ Sets the at-install state of a test, the flag is a boolean specifying
+ whether the test should (``True``) or should not (``False``) run during
+ module installation.
+
+ By default, tests are run at install.
+ """
+ def decorator(obj):
+ obj.at_install = flag
+ return obj
+ return decorator
+
+def post_install(flag):
+ """ Sets the post-install state of a test. The flag is a boolean
+ specifying whether the test should or should not run after a set of
+ module installations.
+
+ By default, tests are *not* run after installation.
+ """
+ def decorator(obj):
+ obj.post_install = flag
+ return obj
+ return decorator
class BaseCase(unittest2.TestCase):
"""
Subclass of TestCase for common OpenERP-specific code.
- This class is abstract and expects self.cr and self.uid to be initialized by subclasses.
+ This class is abstract and expects self.registry, self.cr and self.uid to be
+ initialized by subclasses.
"""
- @classmethod
def cursor(self):
- return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
+ return self.registry.cursor()
- @classmethod
- def registry(self, model):
- return openerp.modules.registry.RegistryManager.get(DB)[model]
-
- @classmethod
def ref(self, xid):
""" Returns database ID corresponding to a given identifier.
@@ -66,7 +84,6 @@ class BaseCase(unittest2.TestCase):
_, id = self.registry('ir.model.data').get_object_reference(self.cr, self.uid, module, xid)
return id
- @classmethod
def browse_ref(self, xid):
""" Returns a browsable record for the given identifier.
@@ -85,10 +102,9 @@ class TransactionCase(BaseCase):
"""
def setUp(self):
- # Store cr and uid in class variables, to allow ref() and browse_ref to be BaseCase @classmethods
- # and still access them
- TransactionCase.cr = self.cursor()
- TransactionCase.uid = openerp.SUPERUSER_ID
+ self.registry = RegistryManager.get(DB)
+ self.cr = self.cursor()
+ self.uid = openerp.SUPERUSER_ID
def tearDown(self):
self.cr.rollback()
@@ -103,7 +119,8 @@ class SingleTransactionCase(BaseCase):
@classmethod
def setUpClass(cls):
- cls.cr = cls.cursor()
+ cls.registry = RegistryManager.get(DB)
+ cls.cr = cls.registry.cursor()
cls.uid = openerp.SUPERUSER_ID
@classmethod
@@ -113,7 +130,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'):
@@ -126,13 +143,24 @@ class HttpCase(TransactionCase):
def setUp(self):
super(HttpCase, self).setUp()
- self.session_id = uuid.uuid4().hex
- HTTP_SESSION[self.session_id] = self.cr
+ self.registry.enter_test_mode()
+ # setup a magic session_id that will be rollbacked
+ self.session = openerp.http.root.session_store.new()
+ self.session_id = self.session.sid
+ self.session.db = DB
+ openerp.http.root.session_store.save(self.session)
def tearDown(self):
- del HTTP_SESSION[self.session_id]
+ self.registry.leave_test_mode()
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.
@@ -144,46 +172,82 @@ class HttpCase(TransactionCase):
Other lines are relayed to the test log.
"""
- t0 = time.time()
- buf = ''
- while 1:
+ t0 = datetime.now()
+ td = timedelta(seconds=timeout)
+ buf = bytearray()
+ while True:
# timeout
- if time.time() > t0 + timeout:
- raise Exception("phantomjs test timeout (%ss)" % timeout)
+ self.assertLess(datetime.now() - t0, td,
+ "PhantomJS tests should take less than %s seconds" % timeout)
# read a byte
- ready, _, _ = select.select([phantom.stdout], [], [], 0.5)
+ try:
+ ready, _, _ = select.select([phantom.stdout], [], [], 0.5)
+ except select.error, e:
+ # In Python 2, select.error has no relation to IOError or
+ # OSError, and no errno/strerror/filename, only a pair of
+ # unnamed arguments (matching errno and strerror)
+ err, _ = e.args
+ if err == errno.EINTR:
+ continue
+ raise
+
if ready:
s = phantom.stdout.read(1)
- if s:
- buf += s
- else:
+ if not s:
break
+ buf.append(s)
# 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":
- _logger.info("phantomjs test successful")
- return
- if line == "error":
- raise Exception("phantomjs test failed")
+ break
+ if line.startswith("error"):
+ line_ = line[6:]
+ # 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")
def phantom_run(self, cmd, timeout):
- _logger.info('executing %s', cmd)
+ _logger.info('phantom_run executing %s', ' '.join(cmd))
try:
phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except OSError:
- _logger.info("phantomjs not found, test %s skipped", jsfile)
+ raise unittest2.SkipTest("PhantomJS not found")
try:
self.phantom_poll(phantom, timeout)
finally:
# kill phantomjs if phantom.exit() wasn't called in the test
if phantom.poll() is None:
phantom.terminate()
+ self._wait_remaining_requests()
+ _logger.info("phantom_run execution finished")
- def phantom_jsfile(self, jsfile, timeout=30, **kw):
+ def _wait_remaining_requests(self):
+ t0 = int(time.time())
+ for thread in threading.enumerate():
+ if thread.name.startswith('openerp.service.http.request.'):
+ while thread.isAlive():
+ # Need a busyloop here as thread.join() masks signals
+ # and would prevent the forced shutdown.
+ thread.join(0.05)
+ time.sleep(0.05)
+ t1 = int(time.time())
+ if t0 != t1:
+ _logger.info('remaining requests')
+ openerp.tools.misc.dumpstacks()
+ t0 = t1
+
+ def phantom_jsfile(self, jsfile, timeout=60, **kw):
options = {
'timeout' : timeout,
'port': PORT,
@@ -197,8 +261,9 @@ class HttpCase(TransactionCase):
cmd = ['phantomjs', jsfile, phantomtest, json.dumps(options)]
self.phantom_run(cmd, timeout)
- def phantom_js(self, url_path, code, ready="window", timeout=30, **kw):
+ def phantom_js(self, url_path, code, ready="window", login=None, timeout=60, **kw):
""" Test js code running in the browser
+ - optionnally log as 'login'
- load page given by url_path
- wait for ready object to be available
- eval(code) inside the page
@@ -212,17 +277,17 @@ class HttpCase(TransactionCase):
If neither are done before timeout test fails.
"""
options = {
+ 'port': PORT,
+ 'db': DB,
'url_path': url_path,
'code': code,
'ready': ready,
'timeout' : timeout,
- 'port': PORT,
- 'db': DB,
- 'login': ADMIN_USER,
- 'password': ADMIN_PASSWORD,
+ 'login' : login,
'session_id': self.session_id,
}
options.update(kw)
+ options.setdefault('password', options.get('login'))
phantomtest = os.path.join(os.path.dirname(__file__), 'phantomtest.js')
cmd = ['phantomjs', phantomtest, json.dumps(options)]
self.phantom_run(cmd, timeout)
diff --git a/openerp/tests/phantomtest.js b/openerp/tests/phantomtest.js
index 6112df24107..5a6a9696cc0 100644
--- a/openerp/tests/phantomtest.js
+++ b/openerp/tests/phantomtest.js
@@ -2,82 +2,84 @@
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) {
+ console.log('error', timeoutMessageCallback ? timeoutMessageCallback() : "Timeout after "+timeout+" ms");
+ phantom.exit(1);
+ } else if (ready()) {
+ callback();
} else {
- if(!condition) {
- var message = timeoutMessageCallback ? timeoutMessageCallback() : "Timeout after "+timeout+" ms";
- console.log(message);
- console.log("Waiting for " + ready);
- console.log("error");
- phantom.exit(1);
- } else {
- clearInterval(interval);
- callback();
- }
+ setTimeout(waitLoop, 250);
}
- }, 250);
+ }());
}
function PhantomTest() {
var self = this;
- if(phantom.args.length === 1) {
- this.options = JSON.parse(phantom.args[0]);
- } else {
- this.options = JSON.parse(phantom.args[1]);
- }
- this.inject = [];
+ this.options = JSON.parse(phantom.args[phantom.args.length-1]);
+ this.inject = this.options.inject || [];
this.timeout = this.options.timeout ? Math.round(parseFloat(this.options.timeout)*1000 - 5000) : 10000;
this.origin = 'http://localhost';
this.origin += this.options.port ? ':' + this.options.port : '';
// ----------------------------------------------------
- // test reporting
+ // configure phantom and page
// ----------------------------------------------------
- this.error = function(message) {
- console.log(message);
- console.log("error");
- phantom.exit(1);
- };
-
- // ----------------------------------------------------
- // configure page
- // ----------------------------------------------------
- this.page = require('webpage').create();
- this.page.viewportSize = { width: 1366, height: 768 };
- this.page.addCookie({
+ phantom.addCookie({
'domain': 'localhost',
'name': 'session_id',
'value': this.options.session_id,
});
+ this.page = require('webpage').create();
+ this.page.viewportSize = { width: 1366, height: 768 };
this.page.onError = function(message, trace) {
- self.error(message + " " + trace);
+ var msg = [message];
+ if (trace && trace.length) {
+ msg.push.apply(msg, trace.map(function (frame) {
+ var result = [' at ', frame.file, ':', frame.line];
+ if (frame.function) {
+ result.push(' (in ', frame.function, ')');
+ }
+ return result.join('');
+ }));
+ msg.push('(leaf frame on top)')
+ }
+ console.log('error', JSON.stringify(msg.join('\n')));
+ phantom.exit(1);
};
this.page.onAlert = function(message) {
- self.error(message);
+ console.log('error', message);
+ phantom.exit(1);
};
this.page.onConsoleMessage = function(message) {
console.log(message);
};
this.page.onLoadFinished = function(status) {
if (status === "success") {
- var src, test;
for (var k in self.inject) {
- if (typeof self.inject[k] !== "string") {
- test = self.page.evaluate(function (variable) {
- try { return eval("("+variable+")") != null; }
- catch (e) { return false; }
- }, self.inject[k][0]);
- src = self.inject[k][1];
- } else {
- src = self.inject[k];
- test = true;
+ var found = false;
+ var v = self.inject[k];
+ var need = v;
+ var src = v;
+ if (v[0]) {
+ need = v[0];
+ src = v[1];
+ found = self.page.evaluate(function(code) {
+ try {
+ return !!eval(code);
+ } catch (e) {
+ return false;
+ }
+ }, need);
}
- if(test && !page.injectJs(src)) {
- self.error("Can't inject " + src);
+ if(!found) {
+ console.log('Injecting', src, 'needed for', need);
+ if(!self.page.injectJs(src)) {
+ console.log('error', "Cannot inject " + src);
+ phantom.exit(1);
+ }
}
}
}
@@ -86,8 +88,9 @@ function PhantomTest() {
self.page.evaluate(function () {
var message = ("Timeout\nhref: " + window.location.href
+ "\nreferrer: " + document.referrer
- + "\n\n" + document.body.innerHTML).replace(/[^a-z0-9\s~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "*");
- self.error(message);
+ + "\n\n" + (document.body && document.body.innerHTML)).replace(/[^a-z0-9\s~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "*");
+ console.log('error', message);
+ phantom.exit(1);
});
}, self.timeout);
@@ -96,33 +99,38 @@ 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 = "/web/login?" + qp.join('&');
+ url_path = "/login?" + qp.join('&');
}
var url = self.origin + url_path;
self.page.open(url, function(status) {
if (status !== 'success') {
- self.error("failed to load " + url)
+ console.log('error', "failed to load " + url);
+ phantom.exit(1);
} else {
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 page " + ready)
- };
+ }
+ 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 91fdbca86ad..6b21bda8b4f 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
@@ -141,19 +152,11 @@ class configmanager(object):
parser.add_option_group(group)
# WEB
- # TODO move to web addons after MetaOption merge
group = optparse.OptionGroup(parser, "Web interface Configuration")
group.add_option("--db-filter", dest="dbfilter", default='.*',
help="Filter listed database", metavar="REGEXP")
parser.add_option_group(group)
- # Static HTTP
- group = optparse.OptionGroup(parser, "Static HTTP service")
- group.add_option("--static-http-enable", dest="static_http_enable", action="store_true", my_default=False, help="enable static HTTP service for serving plain HTML files")
- group.add_option("--static-http-document-root", dest="static_http_document_root", help="specify the directory containing your static HTML files (e.g '/var/www/')")
- group.add_option("--static-http-url-prefix", dest="static_http_url_prefix", help="specify the URL root prefix where you want web browsers to access your static HTML files (e.g '/')")
- parser.add_option_group(group)
-
# Testing Group
group = optparse.OptionGroup(parser, "Testing Configuration")
group.add_option("--test-file", dest="test_file", my_default=False,
@@ -172,16 +175,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 +252,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 +275,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=640 * 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=768 * 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:
@@ -296,9 +305,10 @@ class configmanager(object):
self.options[option.dest] = option.my_default
self.casts[option.dest] = option
- self.parse_config(None, False)
+ # generate default config
+ self._parse_config()
- def parse_config(self, args=None, complete=True):
+ def parse_config(self, args=None):
""" Parse the configuration file (if any) and the command-line
arguments.
@@ -312,10 +322,12 @@ class configmanager(object):
Typical usage of this method:
openerp.tools.config.parse_config(sys.argv[1:])
-
- :param complete: this is a hack used in __init__(), leave it to True.
-
"""
+ self._parse_config(args)
+ openerp.netsvc.init_logger()
+ openerp.modules.module.initialize_sys_path()
+
+ def _parse_config(self, args=None):
if args is None:
args = []
opt, args = self.parser.parse_args(args)
@@ -348,6 +360,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 +371,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
@@ -374,8 +386,7 @@ class configmanager(object):
'db_maxconn', 'import_partial', 'addons_path',
'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 +398,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 +409,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.
@@ -476,8 +497,6 @@ class configmanager(object):
openerp.conf.server_wide_modules = map(lambda m: m.strip(), opt.server_wide_modules.split(','))
else:
openerp.conf.server_wide_modules = ['web','web_kanban']
- if complete:
- openerp.modules.module.initialize_sys_path()
def _generate_pgpassfile(self):
"""
@@ -617,6 +636,27 @@ 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
+
+ def filestore(self, dbname):
+ return os.path.join(self['data_dir'], 'filestore', dbname)
+
config = configmanager()
diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py
index 43ee96660bd..a4231529b8f 100644
--- a/openerp/tools/convert.py
+++ b/openerp/tools/convert.py
@@ -857,12 +857,12 @@ form: module.record_id""" % (xml_id,)
def _tag_template(self, cr, el, data_node=None):
# This helper transforms a element into a and forwards it
tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
- module = self.module
- if '.' in tpl_id:
- module, tpl_id = tpl_id.split('.', 1)
+ full_tpl_id = tpl_id
+ if '.' not in full_tpl_id:
+ full_tpl_id = '%s.%s' % (self.module, tpl_id)
# set the full template name for qweb .
if not (el.get('inherit_id') or el.get('inherit_option_id')):
- el.set('t-name', '%s.%s' % (module, tpl_id))
+ el.set('t-name', full_tpl_id)
el.tag = 't'
else:
el.tag = 'data'
@@ -952,8 +952,9 @@ form: module.record_id""" % (xml_id,)
'url': self._tag_url
}
-def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None):
- pathname = os.path.join(module, filename)
+def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None, pathname=None):
+ if pathname is None:
+ pathname = os.path.join(module, filename)
fp = misc.file_open(pathname)
ext = os.path.splitext(filename)[1].lower()
try:
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/func.py b/openerp/tools/func.py
index ca8bba6bd49..89cdf9fb622 100644
--- a/openerp/tools/func.py
+++ b/openerp/tools/func.py
@@ -3,7 +3,7 @@
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
-# Copyright (C) 2010 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
@@ -20,11 +20,39 @@
#
##############################################################################
-__all__ = ['synchronized']
+__all__ = ['synchronized', 'lazy_property']
from functools import wraps
from inspect import getsourcefile
+
+class lazy_property(object):
+ """ Decorator for a lazy property of an object, i.e., an object attribute
+ that is determined by the result of a method call evaluated once. To
+ reevaluate the property, simply delete the attribute on the object, and
+ get it again.
+ """
+ def __init__(self, fget):
+ self.fget = fget
+ self.name = fget.__name__
+
+ def __get__(self, obj, cls):
+ if obj is None:
+ return self
+ value = self.fget(obj)
+ setattr(obj, self.name, value)
+ return value
+
+ @staticmethod
+ def reset_all(obj):
+ """ Reset all lazy properties on the instance `obj`. """
+ cls = type(obj)
+ obj_dict = obj.__dict__
+ for name in obj_dict.keys():
+ if isinstance(getattr(cls, name, None), lazy_property):
+ obj_dict.pop(name)
+
+
def synchronized(lock_attr='_lock'):
def decorator(func):
@wraps(func)
diff --git a/openerp/tools/image.py b/openerp/tools/image.py
index 5e5055f252d..93458fb9408 100644
--- a/openerp/tools/image.py
+++ b/openerp/tools/image.py
@@ -25,14 +25,14 @@ except ImportError:
import StringIO
from PIL import Image
-from PIL import ImageOps
+from PIL import ImageEnhance
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,12 @@ 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).upper()
+
+ filetype = {
+ 'BMP': 'PNG',
+ }.get(filetype, filetype)
asked_width, asked_height = size
if asked_width is None:
@@ -81,8 +88,13 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
return base64_source
if image.size != size:
- # If you need faster thumbnails you may use use Image.NEAREST
- image = ImageOps.fit(image, size, Image.ANTIALIAS)
+ # create a thumbnail: will resize and keep ratios, then sharpen for better looking result
+ image.thumbnail(size, Image.ANTIALIAS)
+ sharpener = ImageEnhance.Sharpness(image.convert('RGBA'))
+ resized_image = sharpener.enhance(2.0)
+ # create a transparent image for background and paste the image on it
+ image = Image.new('RGBA', size, (255, 255, 255, 0))
+ image.paste(resized_image, ((size[0] - resized_image.size[0]) / 2, (size[1] - resized_image.size[1]) / 2))
if image.mode not in ["1", "L", "P", "RGB", "RGBA"]:
image = image.convert("RGB")
@@ -90,21 +102,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/mail.py b/openerp/tools/mail.py
index 85dcfd368fe..de6cd173521 100644
--- a/openerp/tools/mail.py
+++ b/openerp/tools/mail.py
@@ -625,7 +625,7 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
if not cr:
db_name = getattr(threading.currentThread(), 'dbname', None)
if db_name:
- local_cr = cr = openerp.registry(db_name).db.cursor()
+ local_cr = cr = openerp.registry(db_name).cursor()
else:
raise Exception("No database cursor found, please pass one explicitly")
diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py
index 251905137c0..d63e2a987e9 100644
--- a/openerp/tools/misc.py
+++ b/openerp/tools/misc.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
@@ -83,7 +83,8 @@ def exec_pg_command(name, *args):
raise Exception('Couldn\'t find %s' % name)
args2 = (prog,) + args
- return subprocess.call(args2)
+ with open(os.devnull) as dn:
+ return subprocess.call(args2, stdout=dn, stderr=subprocess.STDOUT)
def exec_pg_command_pipe(name, *args):
prog = find_pg_tool(name)
@@ -1170,7 +1171,7 @@ class ConstantMapping(Mapping):
return self._value
-def dumpstacks(sig, frame):
+def dumpstacks(sig=None, frame=None):
""" Signal handler: dump a stack trace for each existing thread."""
code = []
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..2f280685350 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
@@ -71,7 +71,8 @@ _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
'CONTINUE_LOOP', 'RAISE_VARARGS',
# New in Python 2.7 - http://bugs.python.org/issue4715 :
'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
- 'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY'
+ 'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY', 'LOAD_FAST',
+ 'LOAD_GLOBAL', # Only allows access to restricted globals
] if x in opmap))
_logger = logging.getLogger(__name__)
@@ -86,16 +87,65 @@ def _get_opcodes(codeobj):
[100, 100, 23, 100, 100, 102, 103, 83]
"""
i = 0
- opcodes = []
byte_codes = codeobj.co_code
while i < len(byte_codes):
code = ord(byte_codes[i])
- opcodes.append(code)
+ yield code
+
if code >= HAVE_ARGUMENT:
i += 3
else:
i += 1
- return opcodes
+
+def assert_no_dunder_name(code_obj, expr):
+ """ assert_no_dunder_name(code_obj, expr) -> None
+
+ Asserts that the code object does not refer to any "dunder name"
+ (__$name__), so that safe_eval prevents access to any internal-ish Python
+ attribute or method (both are loaded via LOAD_ATTR which uses a name, not a
+ const or a var).
+
+ Checks that no such name exists in the provided code object (co_names).
+
+ :param code_obj: code object to name-validate
+ :type code_obj: CodeType
+ :param str expr: expression corresponding to the code object, for debugging
+ purposes
+ :raises NameError: in case a forbidden name (containing two underscores)
+ is found in ``code_obj``
+
+ .. note:: actually forbids every name containing 2 underscores
+ """
+ for name in code_obj.co_names:
+ if "__" in name:
+ raise NameError('Access to forbidden name %r (%r)' % (name, expr))
+
+def assert_valid_codeobj(allowed_codes, code_obj, expr):
+ """ Asserts that the provided code object validates against the bytecode
+ and name constraints.
+
+ Recursively validates the code objects stored in its co_consts in case
+ lambdas are being created/used (lambdas generate their own separated code
+ objects and don't live in the root one)
+
+ :param allowed_codes: list of permissible bytecode instructions
+ :type allowed_codes: set(int)
+ :param code_obj: code object to name-validate
+ :type code_obj: CodeType
+ :param str expr: expression corresponding to the code object, for debugging
+ purposes
+ :raises ValueError: in case of forbidden bytecode in ``code_obj``
+ :raises NameError: in case a forbidden name (containing two underscores)
+ is found in ``code_obj``
+ """
+ assert_no_dunder_name(code_obj, expr)
+ for opcode in _get_opcodes(code_obj):
+ if opcode not in allowed_codes:
+ raise ValueError(
+ "opcode %s not allowed (%r)" % (opname[opcode], expr))
+ for const in code_obj.co_consts:
+ if isinstance(const, CodeType):
+ assert_valid_codeobj(allowed_codes, const, 'lambda')
def test_expr(expr, allowed_codes, mode="eval"):
"""test_expr(expression, allowed_codes[, mode]) -> code_object
@@ -110,15 +160,13 @@ def test_expr(expr, allowed_codes, mode="eval"):
# eval() does not like leading/trailing whitespace
expr = expr.strip()
code_obj = compile(expr, "", mode)
- except (SyntaxError, TypeError):
+ except (SyntaxError, TypeError, ValueError):
raise
except Exception, e:
import sys
exc_info = sys.exc_info()
raise ValueError, '"%s" while compiling\n%r' % (ustr(e), expr), exc_info[2]
- for code in _get_opcodes(code_obj):
- if code not in allowed_codes:
- raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
+ assert_valid_codeobj(allowed_codes, code_obj, expr)
return code_obj
@@ -187,19 +235,13 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal
This can be used to e.g. evaluate
an OpenERP domain expression from an untrusted source.
- Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
-
- >>> safe_eval("__import__('sys').modules")
- Traceback (most recent call last):
- ...
- ValueError: opcode LOAD_NAME not allowed
-
+ :throws TypeError: If the expression provided is a code object
+ :throws SyntaxError: If the expression provided is not valid Python
+ :throws NameError: If the expression provided accesses forbidden names
+ :throws ValueError: If the expression provided uses forbidden bytecode
"""
if isinstance(expr, CodeType):
- raise ValueError("safe_eval does not allow direct evaluation of code objects.")
-
- if '__subclasses__' in expr:
- raise ValueError('expression not allowed (__subclasses__)')
+ raise TypeError("safe_eval does not allow direct evaluation of code objects.")
if globals_dict is None:
globals_dict = {}
@@ -225,9 +267,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 +282,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/test_reports.py b/openerp/tools/test_reports.py
index 874a8cced61..c008c20ae29 100644
--- a/openerp/tools/test_reports.py
+++ b/openerp/tools/test_reports.py
@@ -38,7 +38,7 @@ _logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('openerp.tests')
-def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None):
+def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None, report_type=None):
""" Try to render a report with contents of ids
This function should also check for common pitfalls of reports.
@@ -114,7 +114,6 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
Eg. 'OK' or 'gtk-print'
:param our_module: the name of the calling module (string), like 'account'
"""
-
if not our_module and isinstance(action_id, basestring):
if '.' in action_id:
our_module = action_id.split('.', 1)[0]
@@ -161,7 +160,10 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
# Updating the context : Adding the context of action in order to use it on Views called from buttons
if datas.get('id',False):
context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)})
- context.update(safe_eval(action.get('context','{}'), context.copy()))
+ context1 = action.get('context', {})
+ if isinstance(context1, basestring):
+ context1 = safe_eval(context1, context.copy())
+ context.update(context1)
if action['type'] in ['ir.actions.act_window', 'ir.actions.submenu']:
for key in ('res_id', 'res_model', 'view_type', 'view_mode',
'limit', 'auto_refresh', 'search_view', 'auto_search', 'search_view_id'):
@@ -272,7 +274,9 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
if 'window' in datas:
del datas['window']
if not datas:
- datas = action.get('datas',{})
+ datas = action.get('datas')
+ if not datas:
+ datas = action.get('data')
datas = datas.copy()
ids = datas.get('ids')
if 'ids' in datas:
@@ -303,6 +307,4 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
return True
-#eof
-
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
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/openerpcommand/initialize.py b/openerpcommand/initialize.py
index 3723e9f5c01..cf4e54bfb82 100644
--- a/openerpcommand/initialize.py
+++ b/openerpcommand/initialize.py
@@ -9,26 +9,6 @@ import time
import common
-def install_openerp(database_name, create_database_flag, module_names, install_demo_data):
- import openerp
- config = openerp.tools.config
-
- if create_database_flag:
- with lock_file('/tmp/global_openerp_create_database.lock'):
- create_database(database_name)
-
- config['init'] = dict.fromkeys(module_names, 1)
-
- # Install the import hook, to import openerp.addons..
- openerp.modules.module.initialize_sys_path()
-
- print
-
- registry = openerp.modules.registry.RegistryManager.get(
- database_name, update_module=True, force_demo=install_demo_data)
-
- return registry
-
# From http://code.activestate.com/recipes/576572/
@contextlib.contextmanager
def lock_file(path, wait_delay=.1, max_try=600):
@@ -52,21 +32,6 @@ def lock_file(path, wait_delay=.1, max_try=600):
os.close(fd)
os.unlink(path)
-# TODO turn template1 in a parameter
-# This should be exposed from openerp (currently in
-# openerp/service/web_services.py).
-def create_database(database_name):
- import openerp
- db = openerp.sql_db.db_connect('template1')
- cr = db.cursor() # TODO `with db as cr:`
- try:
- cr.autocommit(True)
- cr.execute("""CREATE DATABASE "%s"
- ENCODING 'unicode' TEMPLATE "template1" """ \
- % (database_name,))
- finally:
- cr.close()
-
def run(args):
assert args.database
assert not (args.module and args.all_modules)
@@ -74,11 +39,14 @@ def run(args):
import openerp
config = openerp.tools.config
+ config['db_name'] = args.database
if args.tests:
config['log_handler'] = [':INFO']
config['test_enable'] = True
config['without_demo'] = False
+ if args.port:
+ config['xmlrpc_port'] = int(args.port)
else:
config['log_handler'] = [':CRITICAL']
config['test_enable'] = False
@@ -90,14 +58,13 @@ def run(args):
args.addons = []
config['addons_path'] = ','.join(args.addons)
- print "MY addons path is", config['addons_path']
-
if args.all_modules:
module_names = common.get_addons_from_paths(args.addons, args.exclude)
elif args.module:
module_names = args.module
else:
module_names = ['base']
+ config['init'] = dict.fromkeys(module_names, 1)
if args.coverage:
import coverage
@@ -106,17 +73,22 @@ def run(args):
cov = coverage.coverage(branch=True, include='*.py')
cov.start()
openerp.netsvc.init_logger()
- registry = install_openerp(args.database, not args.no_create, module_names, not config['without_demo'])
+
+ if not args.no_create:
+ with lock_file('/tmp/global_openerp_create_database.lock'):
+ openerp.service.db._create_empty_database(args.database)
+
+ config['workers'] = False
+
+ rc = openerp.service.server.start(preload=[args.database], stop=True)
+
if args.coverage:
cov.stop()
cov.html_report(directory='coverage')
# If we wanted the report on stdout:
# cov.report()
- # The `_assertion_report` attribute was added on the registry during the
- # OpenERP 7.0 development.
- if hasattr(registry, '_assertion_report'):
- sys.exit(1 if registry._assertion_report.failures else 0)
+ sys.exit(rc)
def add_parser(subparsers):
parser = subparsers.add_parser('initialize',
@@ -124,6 +96,8 @@ def add_parser(subparsers):
parser.add_argument('-d', '--database', metavar='DATABASE',
**common.required_or_default('DATABASE', 'the database to create'))
common.add_addons_argument(parser)
+ parser.add_argument('-P', '--port', metavar='PORT',
+ **common.required_or_default('PORT', 'the server port'))
parser.add_argument('--module', metavar='MODULE', action='append',
help='specify a module to install'
' (this option can be repeated)')
diff --git a/openerpcommand/module.py b/openerpcommand/module.py
index 550e0a2be28..284c0186785 100644
--- a/openerpcommand/module.py
+++ b/openerpcommand/module.py
@@ -33,12 +33,9 @@ def run(args):
xs = []
ir_module_module = registry.get('ir.module.module')
- cr = registry.db.cursor() # TODO context manager
- try:
+ with registry.cursor() as cr:
ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [], {})
xs = ir_module_module.read(cr, openerp.SUPERUSER_ID, ids, [], {})
- finally:
- cr.close()
if xs:
print "Modules (database `%s`):" % (args.database,)
diff --git a/openerpcommand/read.py b/openerpcommand/read.py
index 5b68fb26ef0..61c05117875 100644
--- a/openerpcommand/read.py
+++ b/openerpcommand/read.py
@@ -20,15 +20,12 @@ def run(args):
registry = openerp.modules.registry.RegistryManager.get(
args.database, update_module=False)
model = registry[args.model]
- cr = registry.db.cursor() # TODO context manager
field_names = [args.field] if args.field else []
if args.short:
# ignore --field
field_names = ['name']
- try:
+ with registry.cursor() as cr:
xs = model.read(cr, 1, args.id, field_names, {})
- finally:
- cr.close()
if xs:
print "Records (model `%s`, database `%s`):" % (args.model, args.database)
diff --git a/openerpcommand/uninstall.py b/openerpcommand/uninstall.py
index 27e4e04002a..244b0575e53 100644
--- a/openerpcommand/uninstall.py
+++ b/openerpcommand/uninstall.py
@@ -43,15 +43,12 @@ def run(args):
args.database, update_module=False)
ir_module_module = registry.get('ir.module.module')
- cr = registry.db.cursor() # TODO context manager
- try:
+ with registry.cursor() as cr:
ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [('name', 'in', args.module), ('state', '=', 'installed')], {})
if len(ids) == len(args.module):
ir_module_module.button_immediate_uninstall(cr, openerp.SUPERUSER_ID, ids, {})
else:
print "At least one module not found (database `%s`)." % (args.database,)
- finally:
- cr.close()
def add_parser(subparsers):
parser = subparsers.add_parser('uninstall',
diff --git a/setup.py b/setup.py
index 05178fb12ac..2d427786dbd 100644
--- a/setup.py
+++ b/setup.py
@@ -39,7 +39,7 @@ def data():
# but this will work fine (especially since we don't use the ZIP file
# approach).
r["babel/localedata"] = glob.glob(os.path.join(os.path.dirname(babel.__file__), "localedata", '*'))
- others = ['global.dat', 'numbers.py', 'support.py']
+ others = ['global.dat', 'numbers.py', 'support.py', 'plural.py']
r["babel"] = map(lambda f: os.path.join(os.path.dirname(babel.__file__), f), others)
others = ['frontend.py', 'mofile.py']
r["babel/messages"] = map(lambda f: os.path.join(os.path.dirname(babel.__file__), "messages", f), others)
@@ -75,7 +75,42 @@ 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",
+ "markupsafe", # dependence of jinja2 and mako
+ "mock",
+ "openerp",
+ "poplib",
+ "psutil",
+ "pychart",
+ "pydot",
+ "pyparsing",
+ "pytz",
+ "reportlab",
+ "requests",
+ "select",
+ "simplejson",
+ "smtplib",
+ "uuid",
+ "vatnumber",
+ "vobject",
+ "win32service", "win32serviceutil",
+ "xlwt",
+ "xml", "xml.dom",
+ "yaml",
+ ],
"excludes" : ["Tkconstants","Tkinter","tcl"],
}
}
@@ -115,7 +150,7 @@ setuptools.setup(
#include_package_data = True,
install_requires = [
'pychart', # not on pypi, use: pip install http://download.gna.org/pychart/PyChart-1.39.tar.gz
- 'babel',
+ 'babel >= 1.0',
'docutils',
'feedparser',
'gdata',
@@ -136,10 +171,11 @@ setuptools.setup(
'python-openid',
'pytz',
'pyusb >= 1.0.0b1',
- 'pywebdav',
+ 'pywebdav < 0.9.8',
'pyyaml',
'qrcode',
'reportlab', # windows binary pypi.python.org/pypi/reportlab
+ 'requests',
'simplejson',
'unittest2',
'vatnumber',
@@ -150,7 +186,7 @@ setuptools.setup(
extras_require = {
'SSL' : ['pyopenssl'],
},
- tests_require = ['unittest2'],
+ tests_require = ['unittest2', 'mock'],
**py2exe_options()
)