[tryton-debian-vcs] tryton-server branch upstream updated. upstream/3.0.4-1-g4f87ee7
Mathias Behrle
tryton-debian-vcs at alioth.debian.org
Tue Apr 22 13:12:24 UTC 2014
The following commit has been merged in the upstream branch:
https://alioth.debian.org/plugins/scmgit/cgi-bin/gitweb.cgi/?p=tryton/tryton-server.git;a=commitdiff;h=upstream/3.0.4-1-g4f87ee7
commit 4f87ee707bde692a01bd11d63328c69b58173f02
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Tue Apr 22 14:24:23 2014 +0200
Adding upstream version 3.2.0.
diff --git a/CHANGELOG b/CHANGELOG
index 186ace0..cf11373 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,14 +1,37 @@
-Version 3.0.4 - 2014-04-07
-* Bug fixes (see mercurial logs for details)
-
-Version 3.0.3 - 2014-03-22
-* Bug fixes (see mercurial logs for details)
-
-Version 3.0.2 - 2014-01-18
-* Bug fixes (see mercurial logs for details)
-
-Version 3.0.1 - 2013-12-04
+Version 3.2.0 - 2014-04-21
* Bug fixes (see mercurial logs for details)
+* Add restore_history to ModelSQL
+* Add history revisions
+* Add the multi selection widget
+* Add index to one2many's on_change
+* Remove auto-refresh on Action Window
+* Add support of domain for non-relation field
+* Manage microseconds in JSON-RPC and XML-RPC
+* Remove Sha field
+* Add password widget
+* Add Len to PYSON
+* Use bcrypt to hash password if possible
+* Use a sequence of ids, values to set fields
+* Client side actions on button and wizard
+* Add depends attribute to data tag
+* Add tree_invisible attribute to button in tree view
+* Drop support of Python 2.6
+* Deprecate on_change, on_change_with, selection_change_with and autocomplete
+ field arguments
+* Add fields.depends decorator
+* Add run-tests
+* Validate only modified and dependant fields on model write
+* Improve error messages by showing the failing value
+* Remove relation field actions:
+ - delete_all
+ - unlink_all
+ - set
+* Rename relation field action unlink into remove
+* Use a sequence of records, values in write
+* set_context of Transaction.set_user is restricted to root
+* Add a "copy" action to One2Many and Many2Many's set method
+* Force UTC as timezone (migration script available on tryton-tools)
+* Add relation_field for many2one
Version 3.0.0 - 2013-10-21
* Bug fixes (see mercurial logs for details)
diff --git a/PKG-INFO b/PKG-INFO
index 52c4611..e0f280b 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,12 +1,12 @@
Metadata-Version: 1.1
Name: trytond
-Version: 3.0.4
+Version: 3.2.0
Summary: Tryton server
Home-page: http://www.tryton.org/
Author: Tryton
-Author-email: UNKNOWN
+Author-email: issue_tracker at tryton.org
License: GPL-3
-Download-URL: http://downloads.tryton.org/3.0/
+Download-URL: http://downloads.tryton.org/3.2/
Description: trytond
=======
@@ -56,6 +56,8 @@ Description: trytond
* MySQL can not create indexes containing text or blob fields.
+ * Timestamp has a precision of second which is used for optimistic lock.
+
* Tryton uses a DECIMAL(65, 30) for Decimal fields and DOUBLE(255, 30) for
Float fields.
@@ -67,6 +69,8 @@ Description: trytond
* SQL constraints are validated by Tryton instead of database.
+ * Timestamp has a precision of second which is used for optimistic lock.
+
Support
-------
@@ -93,7 +97,8 @@ Description: trytond
http://www.tryton.org/
-Platform: UNKNOWN
+Keywords: business application platform ERP
+Platform: any
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Framework :: Tryton
@@ -110,6 +115,5 @@ Classifier: Natural Language :: Russian
Classifier: Natural Language :: Slovenian
Classifier: Natural Language :: Spanish
Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
diff --git a/README b/README
index b79cb86..91ae41b 100644
--- a/README
+++ b/README
@@ -47,6 +47,8 @@ backends available. Here are some warnings about using other backends:
* MySQL can not create indexes containing text or blob fields.
+ * Timestamp has a precision of second which is used for optimistic lock.
+
* Tryton uses a DECIMAL(65, 30) for Decimal fields and DOUBLE(255, 30) for
Float fields.
@@ -58,6 +60,8 @@ backends available. Here are some warnings about using other backends:
* SQL constraints are validated by Tryton instead of database.
+ * Timestamp has a precision of second which is used for optimistic lock.
+
Support
-------
diff --git a/bin/trytond b/bin/trytond
index a55d02c..d8b84ef 100755
--- a/bin/trytond
+++ b/bin/trytond
@@ -3,7 +3,7 @@
#this repository contains the full copyright notices and license terms.
import sys
import os
-import optparse
+import argparse
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
'..', '..', 'trytond')))
@@ -17,37 +17,39 @@ from trytond.version import VERSION
def parse_commandline():
options = {}
- parser = optparse.OptionParser(version=VERSION)
+ parser = argparse.ArgumentParser(prog='trytond')
- parser.add_option("-c", "--config", dest="config",
+ parser.add_argument('--version', action='version',
+ version='%(prog)s ' + VERSION)
+ parser.add_argument("-c", "--config", dest="config",
help="specify config file")
- parser.add_option('--debug', dest='debug_mode', action='store_true',
+ parser.add_argument('--debug', dest='debug_mode', action='store_true',
help='enable debug mode (start post-mortem debugger if exceptions'
' occur)')
- parser.add_option("-v", "--verbose", action="store_true",
+ parser.add_argument("-v", "--verbose", action="store_true",
dest="verbose", help="enable verbose mode")
- parser.add_option("-d", "--database", dest="db_name",
+ parser.add_argument("-d", "--database", dest="db_name",
help="specify the database name")
- parser.add_option("-i", "--init", dest="init",
+ parser.add_argument("-i", "--init", dest="init",
help="init a module (use \"all\" for all modules)")
- parser.add_option("-u", "--update", dest="update",
+ parser.add_argument("-u", "--update", dest="update",
help="update a module (use \"all\" for all modules)")
- parser.add_option("--pidfile", dest="pidfile",
+ parser.add_argument("--pidfile", dest="pidfile",
help="file where the server pid will be stored")
- parser.add_option("--logfile", dest="logfile",
+ parser.add_argument("--logfile", dest="logfile",
help="file where the server log will be stored")
- parser.add_option("--disable-cron", dest="cron",
+ parser.add_argument("--disable-cron", dest="cron",
action="store_false", help="disable cron")
- parser.epilog = 'The first time a database is initialized with "-i" admin'\
- ' password is read from file defined by TRYTONPASSFILE '\
- 'environment variable or interactively ask user. '\
- 'The config file can be specified in the TRYTOND_CONFIG '\
- 'environment variable.'
+ parser.epilog = ('The first time a database is initialized with "-i" admin'
+ ' password is read from file defined by TRYTONPASSFILE '
+ 'environment variable or interactively ask user. '
+ 'The config file can be specified in the TRYTOND_CONFIG '
+ 'environment variable.')
- (opt, _) = parser.parse_args()
+ opt = parser.parse_args()
if opt.config:
options['configfile'] = opt.config
diff --git a/doc/conf.py b/doc/conf.py
index a5b9b8d..71c121e 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -48,9 +48,9 @@ copyright = (u'2008-2011, Bertrand Chenal, Cédric Krier, Ian Wilson, '
# built documents.
#
# The short X.Y version.
-version = '3.0'
+version = '3.2'
# The full version, including alpha/beta/rc tags.
-release = '3.0'
+release = '3.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst
index 451392f..85fd0ca 100644
--- a/doc/ref/models/fields.rst
+++ b/doc/ref/models/fields.rst
@@ -49,12 +49,6 @@ If ``True``, the field is not editable in the client. Default is ``False``.
A :ref:`domain <topics-domain>` constraint that will be applied on the field
value.
-.. warning::
-
- For now it only works on relational fields like :class:`Many2One`,
- :class:`One2Many` and :class:`Many2Many`.
-..
-
``states``
----------
@@ -72,12 +66,14 @@ with the values of the record.
If true, the content of the field will be indexed.
+.. _ref-models-fields-on_change:
+
``on_change``
-------------
.. attribute:: Field.on_change
-A list of field names. If this attribute is set, the client will call the
+A set of field names. If this attribute is set, the client will call the
method ``on_change_<field name>`` of the model when the user changes the
current field value and will give the values of each fields in this list. The
method signature is::
@@ -88,15 +84,19 @@ This method must return a dictionary with the values of fields to be updated.
.. note::
- The on_change_<field name> methods are runnin in a rollbacked transaction.
+ The on_change_<field name> methods are running in a rollbacked transaction.
..
+The set of field names could be filled by using the decorator :meth:`depends`.
+
+.. _ref-models-fields-on_change_with:
+
``on_change_with``
------------------
.. attribute:: Field.on_change_with
-A list of field names. Same like :attr:`on_change`, but defined the other way
+A set of field names. Same like :attr:`on_change`, but defined the other way
around. If this attribute is set, the client will call the method
``on_change_with_<field name>`` of the model when the user changes one of the
fields defined in the list and will give the values of each fields in this
@@ -112,6 +112,8 @@ This method must return the new value of the field.
..
+The set of field names could be filled by using the decorator :meth:`depends`.
+
``depends``
-----------
@@ -173,6 +175,10 @@ could be updated to add new joins)::
Return the namedtuple('SQLType', 'base type') which defines the SQL type to
use for creation and casting.
+.. method:: Field.sql_column(table)
+
+ Return the Column instance based on table.
+
Default value
=============
@@ -189,6 +195,15 @@ The method signature is::
Where ``tables`` is a nested dictionary, see :meth:`~Field.convert_domain`.
+Depends
+=======
+
+.. method:: depends([\*fields[, methods]])
+
+A decorator to define the field names on which the decorated method depends.
+The `methods` argument can be used to duplicate the field names from other
+fields. This is usefull if the decorated method calls another method.
+
Field types
===========
@@ -234,7 +249,7 @@ A single line string field.
.. attribute:: Char.autocomplete
- A list of field names. If this attribute is set, the client will call the
+ A set of field names. If this attribute is set, the client will call the
method ``autocomplete_<field name>`` of the model when the user changes one
of those field value. The method signature is::
@@ -242,15 +257,7 @@ A single line string field.
This method must return a list of string that will populate the
ComboboxEntry in the client.
-
-Sha
----
-
-.. class:: Sha(string[, \**options])
-
-A string field which value will be stored with a `secure hash algorithm`_.
-
-.. _`secure hash algorithm`: http://en.wikipedia.org/wiki/Secure_Hash_Algorithm
+ The set of field names could be filled by using the decorator :meth:`depends`.
Text
----
@@ -395,10 +402,11 @@ A string field with limited values to choice.
.. attribute:: Selection.selection_change_with
- A list of field names. If this attribute is set, the client will call the
+ A set of field names. If this attribute is set, the client will call the
``selection`` method of the model when the user changes on of the fields
defined in the list and will give the values of each fields in the list.
The ``selection`` method should be an instance method.
+ The set of field names could be filled by using the decorator :meth:`depends`.
.. attribute:: Selection.translate_selection
@@ -495,21 +503,19 @@ This field accepts as written value a list of tuples like this:
- ``('create', [{<field name>: value, ...}, ...])``: it will create new
target records and link them to this one.
- - ``('write'[, ids, ...], {<field name>: value, ...})``: it will write
- values to target ids.
+ - ``('write'[[, ids, ...], {<field name>: value, ...}, ...])``: it will
+ write values to target ids.
- ``('delete'[, ids, ...])``: it will delete the target ids.
- - ``('delete_all')``: it will delete all the target records.
-
- ``('add'[, ids, ...])``: it will link the target ids to this record.
- - ``('unlink'[, ids, ...])``: it will unlink the target ids from this
+ - ``('remove'[, ids, ...])``: it will unlink the target ids from this
record.
- - ``('unlink_all')``: it will unlink all the target records.
-
- - ``('set'[, ids, ...])``: it will link only the target ids to this record.
+ - ``('copy', ids[, {<field name>: value, ...}])``: it will copy the target
+ ids to this record. Optional field names and values may be added to
+ override some of the fields of the copied records.
:class:`One2Many` has some extra required arguments:
@@ -549,7 +555,11 @@ Many2Many
.. class:: Many2Many(relation_name, origin, target, string[, order[, datetime_field[, size[, \**options]]]])
-A many-to-many relation field.
+A many-to-many relation field. It requires to have the opposite origin
+:class:`Many2One` field or a:class:`Reference` field defined on the relation
+model and a :class:`Many2One` field pointing to the target.
+
+This field accepts as written value a list of tuples like the :class:`One2Many`.
:class:`Many2Many` has some extra required arguments:
diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst
index 54b092c..5ac7918 100644
--- a/doc/ref/models/models.rst
+++ b/doc/ref/models/models.rst
@@ -76,12 +76,13 @@ Class methods:
warning states by users.
..
-.. classmethod:: Model.default_get(fields_names[, with_rec_name])
+.. classmethod:: Model.default_get(fields_names[, with_rec_name[, with_on_change]])
Return a dictionary with the default values for each field in
``fields_names``. Default values are defined by the returned value of each
instance method with the pattern ``default_`field_name`()``.
``with_rec_name`` allow to add `rec_name` value for each many2one field.
+ ``with_on_change`` allow to add ``on_change`` value for each default value.
.. classmethod:: Model.fields_get([fields_names])
@@ -238,7 +239,7 @@ CLass methods:
Return a list of values for the ids. If ``fields_names`` is set, there will
be only values for these fields otherwise it will be for all fields.
-.. classmethod:: ModelStorage.write(records, values)
+.. classmethod:: ModelStorage.write(records, values, [[records, values], ...])
Write ``values`` on the list of records. ``values`` is a dictionary with
fields names as key and writen values as value.
@@ -400,6 +401,20 @@ Class methods:
Could be overrided to use a custom SQL query instead of a table of the
database. It should return a SQL FromItem.
+.. classmethod:: ModelSQL.history_revisions(ids)
+
+ Return a sorted list of all revisions for ids. The list is composed of
+ the date, id and username of the revision.
+
+.. classmethod:: ModelSQL.restore_history(ids, datetime)
+
+ Restore the record ids from history at the specified date time.
+ Restoring a record will still generate an entry in the history table.
+
+ .. warning::
+ No access rights are verified and the records are not validated.
+ ..
+
.. classmethod:: ModelStorage.search(domain[, offset[, limit[, order[, count[, query]]]]])
Return a list of records that match the :ref:`domain <topics-domain>` or
diff --git a/doc/ref/pyson.rst b/doc/ref/pyson.rst
index 5f012b9..fd1640b 100644
--- a/doc/ref/pyson.rst
+++ b/doc/ref/pyson.rst
@@ -250,6 +250,11 @@ Arguments:
``delta_microseconds``
Contains a PYSON statement of type int or long.
+.. class:: Len(value)
+
+A :class:`Len` object represents the PYSON ``Len()`` statement for length of a
+dictionary, list or string. Returns the number of items in ``value``.
+
.. class:: Id(module, fs_id)
An :class:`Id` object represents the PYSON ``Id()`` statement for filesystem id
diff --git a/doc/ref/rpc.rst b/doc/ref/rpc.rst
index cb7f2e3..ba0e6f9 100644
--- a/doc/ref/rpc.rst
+++ b/doc/ref/rpc.rst
@@ -17,7 +17,7 @@ Instance attributes are:
.. attribute:: RPC.instantiate
- The position of the argument to be instanciated
+ The position or the slice of the argument to be instanciated
.. attribute:: RPC.result
diff --git a/doc/ref/transaction.rst b/doc/ref/transaction.rst
index 6c06292..2ddc292 100644
--- a/doc/ref/transaction.rst
+++ b/doc/ref/transaction.rst
@@ -17,6 +17,10 @@ database transaction.
The database cursor.
+.. attribute:: Transaction.database
+
+ The database.
+
.. attribute:: Transaction.user
The id of the user.
@@ -39,7 +43,7 @@ database transaction.
Count the number of modification made in this transaction.
-.. method:: Transaction.start(database_name, user[, context])
+.. method:: Transaction.start(database_name, user[, readonly[, context[, close[, autocommit]]]])
Start a new transaction and return a `context manager`_.
@@ -64,7 +68,7 @@ database transaction.
Modify the cursor of the transaction and return a `context manager`_. The
previous cursor will be restored when exiting the `with` statement.
-.. method:: Transaction.new_cursor()
+.. method:: Transaction.new_cursor([autocommit[, readonly]])
Change the cursor of the transaction with a new one on the same database
and return a `context manager`_. The previous cursor will be restored when
diff --git a/doc/ref/wizard.rst b/doc/ref/wizard.rst
index 6cc0674..62267e4 100644
--- a/doc/ref/wizard.rst
+++ b/doc/ref/wizard.rst
@@ -32,6 +32,9 @@ Class attributes are:
.. attribute:: Wizard.end_state
It contains the name of the ending state.
+ If an instance method with this name exists on the wizard, it will be
+ called on deletion of the wizard and it may return one of the :ref:`client
+ side action keywords <topics-views-client-actions>`.
.. attribute:: Wizard.__rpc__
@@ -70,8 +73,8 @@ Class methods are:
Execute the wizard for the state.
`session_id` is a session id.
`data` is a dictionary with the session data to update.
- `active_id`, `active_ids` and `active_model` must be set in the context
- according to the records on which the wizard is run.
+ `active_id`, `active_ids`, `active_model` and `action_id` must be set in
+ the context according to the records on which the wizard is run.
=====
State
diff --git a/doc/topics/index.rst b/doc/topics/index.rst
index ad1fcae..bae4107 100644
--- a/doc/topics/index.rst
+++ b/doc/topics/index.rst
@@ -12,6 +12,7 @@ Introduction to all the key parts of trytond:
install
models/index
models/fields_default_value
+ models/fields_on_change
domain
pyson
actions
diff --git a/doc/topics/install.rst b/doc/topics/install.rst
index 00d91ce..a5eb993 100644
--- a/doc/topics/install.rst
+++ b/doc/topics/install.rst
@@ -7,9 +7,9 @@ How to install Tryton
Prerequisites
=============
- * Python 2.6 or later (http://www.python.org/)
+ * Python 2.7 or later (http://www.python.org/)
* lxml 2.0 or later (http://codespeak.net/lxml/)
- * relatorio 0.2.0 or later (http://relatorio.openhex.org/)
+ * relatorio 0.2.0 or later (http://code.google.com/p/python-relatorio/)
* python-dateutil (http://labix.org/python-dateutil)
* polib (https://bitbucket.org/izi/polib/wiki/Home)
* python-sql 0.2 or later (http://code.google.com/p/python-sql/)
@@ -17,7 +17,6 @@ Prerequisites
* Optional: MySQL-python (http://sourceforge.net/projects/mysql-python/)
* Optional: pywebdav 0.9.8 or later (http://code.google.com/p/pywebdav/)
* Optional: pydot (http://code.google.com/p/pydot/)
- * Optional: pytz (http://pytz.sourceforge.net/)
* Optional: unoconv http://dag.wieers.com/home-made/unoconv/)
* Optional: sphinx (http://sphinx.pocoo.org/)
* Optional: simplejson (http://undefined.org/python/#simplejson)
diff --git a/doc/topics/models/fields_on_change.rst b/doc/topics/models/fields_on_change.rst
new file mode 100644
index 0000000..e47c787
--- /dev/null
+++ b/doc/topics/models/fields_on_change.rst
@@ -0,0 +1,65 @@
+.. _topcis-fields_on_change:
+
+===================
+on_change of fields
+===================
+
+Tryton allows developers to define methods that can be called once a field's
+value has changed by the user this is the :ref:`ref-models-fields-on_change`
+method. The method has the following name::
+
+ Model.on_change_<field name>
+
+This is an instance method, an instance of ``Model`` will be created by
+using the values from the form's fields specified by the ``on_change`` list
+defined on the field.
+
+There is also a way to define a method that must update a field whenever any
+field from a predefined list is modified. This list is defined by the
+:ref:`ref-models-fields-on_change_with` attribute of the field. The method
+that will be called has the following name::
+
+ Model.on_change_with_<field_name>
+
+Just like for the classic ``on_change``, an instance of ``Model`` will be
+created by using the values entered in the form's fields specified by the
+``on_change_with`` attribute.
+
+on_change & on_change_with return values
+----------------------------------------
+
+The return value of the method called will depend of the type of the call that
+occured. In case of an ``on_change`` the returned value will be a dictionary
+whose keys are field names to be modified and whose values will be the
+corresponding new value. In case of an ``on_change_with`` the returned value
+will be the new value of the field.
+
+Pay attention that the new value of a field differs according to its type.
+Simple fields require the value to be of the same type as the field.
+
+Relation fields require some more explanations:
+
+ - a ``field.One2Many`` or a ``field.Many2Many`` will accept either:
+
+ - a list of ``id`` denoting the new value that will replace all
+ previously ids.
+
+ - a dictionary composed of three keys: ``update``, ``add`` and
+ ``remove``.
+
+ The ``update`` key has as value a list of dictionary that denotes the
+ new value of the target's fields. The lines affected by the change
+ are found using the ``id`` key of the dictionary.
+
+ The ``add`` key have as value a list of tuple, the first element is
+ the index where a new line should be added, the second element is a
+ dictionary using the same convention as the dictionaries for the
+ ``update`` key.
+
+ The ``remove`` key will have as value a list of ids that will be
+ remove from the field.
+
+ Records that are not yet saved receive temporary ids (negative
+ integers) that can be used in with ``update`` or ``remove`` to
+ interact with them.
+
diff --git a/doc/topics/modules/index.rst b/doc/topics/modules/index.rst
index fe1a7e4..fa79017 100644
--- a/doc/topics/modules/index.rst
+++ b/doc/topics/modules/index.rst
@@ -110,9 +110,13 @@ Here is the list of the tags:
* ``tryton``: The main tag of the xml
* ``data``: Define a set of data inside the file. It can have the
- attributes ``noupdate`` to prevent the framework to update the records,
- ``skiptest`` to prevent import of data when running tests and ``grouped``
- to create records at the end with a grouped call.
+ attributes:
+
+ * ``noupdate`` to prevent the framework to update the records,
+ * ``skiptest`` to prevent import of data when running tests,
+ * ``depends`` to import data only if all modules in the comma separated
+ module list value are installed,
+ * ``grouped`` to create records at the end with a grouped call.
* ``record``: Create a record of the model defined by the attribute
``model`` in the database. The ``id`` attribute can be used to refer to
diff --git a/doc/topics/reports/index.rst b/doc/topics/reports/index.rst
index f732835..633820e 100644
--- a/doc/topics/reports/index.rst
+++ b/doc/topics/reports/index.rst
@@ -254,8 +254,8 @@ the default style.
.. _Genshi XML Templates: http://genshi.edgewall.org/wiki/Documentation/0.5.x/xml-templates.html
-.. _Quick Example: http://relatorio.openhex.org/wiki/QuickExample
+.. _Quick Example: http://code.google.com/p/python-relatorio/wiki/QuickExample
-.. _In Depth Introduction: http://relatorio.openhex.org/wiki/IndepthIntroduction
+.. _In Depth Introduction: http://code.google.com/p/python-relatorio/wiki/IndepthIntroduction
-.. _Example Documents: http://relatorio.openhex.org/browser/examples
+.. _Example Documents: http://code.google.com/p/python-relatorio/source/browse/#hg%2Fexamples
diff --git a/doc/topics/views/index.rst b/doc/topics/views/index.rst
index bf9da74..04203c5 100644
--- a/doc/topics/views/index.rst
+++ b/doc/topics/views/index.rst
@@ -296,6 +296,9 @@ newline
Force to use a new row.
+
+.. _form-button:
+
button
^^^^^^
@@ -308,6 +311,22 @@ Display a button.
``button(cls, records)``
+ The function may return an `ir.action` id or one of those client side
+ action keywords:
+
+.. _topics-views-client-actions:
+
+ * ``new``: to create a new record
+ * ``delete``: to delete the selected records
+ * ``remove``: to remove the record if it has a parent
+ * ``copy``: to copy the selected records
+ * ``next``: to go to the next record
+ * ``previous``: to go to the previous record
+ * ``close``: to close the current tab
+ * ``switch <view type>``: to switch the view to the defined type
+ * ``reload context``: to reload user context
+ * ``reload menu``: to reload menu
+
* ``icon``
* ``confirm``: A text that will be displayed in a confirmation popup when
@@ -517,6 +536,11 @@ diplayed in the same column.
* ``icon``: The name of the field that contains the name of the icon to
display or the name of the icon.
+button
+^^^^^^
+
+Same as in form-button_.
+
Example
-------
@@ -535,6 +559,22 @@ Example
<field name="sequence" tree_invisible="1"/>
</tree>
+button
+^^^^^^
+
+Display a button.
+
+ * ``string``: The string that will be displayed inside the button.
+
+ * ``name``: The name of the function that will be called. The function must
+ have this syntax:
+
+ ``button(cls, records)``
+
+ * ``confirm``: A text that will be displayed in a confirmation popup when
+ the button is clicked.
+
+ * ``help``: see in common-attributes-help_
Graph view
==========
diff --git a/etc/trytond.conf b/etc/trytond.conf
index 6208dd5..f70a6d6 100644
--- a/etc/trytond.conf
+++ b/etc/trytond.conf
@@ -86,6 +86,3 @@ jsonrpc = localhost:8000
# Default language code
# language = en_US
-
-# Timezone of the server
-# timezone = False
diff --git a/setup.py b/setup.py
index d13df20..793dac8 100644
--- a/setup.py
+++ b/setup.py
@@ -5,20 +5,33 @@
from setuptools import setup, find_packages
import os
+PACKAGE, VERSION, LICENSE, WEBSITE = None, None, None, None
execfile(os.path.join('trytond', 'version.py'))
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
+major_version, minor_version, _ = VERSION.split('.', 2)
+major_version = int(major_version)
+minor_version = int(minor_version)
+
+download_url = 'http://downloads.tryton.org/%s.%s/' % (
+ major_version, minor_version)
+if minor_version % 2:
+ VERSION = '%s.%s.dev0' % (major_version, minor_version)
+ download_url = 'hg+http://hg.tryton.org/%s#egg=%s-%s' % (
+ PACKAGE, PACKAGE, VERSION)
+
setup(name=PACKAGE,
version=VERSION,
description='Tryton server',
long_description=read('README'),
author='Tryton',
+ author_email='issue_tracker at tryton.org',
url=WEBSITE,
- download_url=("http://downloads.tryton.org/" +
- VERSION.rsplit('.', 1)[0] + '/'),
+ download_url=download_url,
+ keywords='business application platform ERP',
packages=find_packages(exclude=['*.modules.*', 'modules.*', 'modules',
'*.proteus.*', 'proteus.*', 'proteus']),
package_data={
@@ -51,10 +64,10 @@ setup(name=PACKAGE,
'Natural Language :: Slovenian',
'Natural Language :: Spanish',
'Operating System :: OS Independent',
- 'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Software Development :: Libraries :: Application Frameworks',
],
+ platforms='any',
license=LICENSE,
install_requires=[
'lxml >= 2.0',
@@ -70,12 +83,12 @@ setup(name=PACKAGE,
'WebDAV': ['PyWebDAV >= 0.9.8'],
'unoconv': ['unoconv'],
'graphviz': ['pydot'],
- 'timezone': ['pytz'],
'simplejson': ['simplejson'],
'cdecimal': ['cdecimal'],
'Levenshtein': ['python-Levenshtein'],
+ 'BCrypt': ['bcrypt'],
},
zip_safe=False,
test_suite='trytond.tests',
test_loader='trytond.test_loader:Loader',
-)
+ )
diff --git a/trytond.egg-info/PKG-INFO b/trytond.egg-info/PKG-INFO
index 52c4611..e0f280b 100644
--- a/trytond.egg-info/PKG-INFO
+++ b/trytond.egg-info/PKG-INFO
@@ -1,12 +1,12 @@
Metadata-Version: 1.1
Name: trytond
-Version: 3.0.4
+Version: 3.2.0
Summary: Tryton server
Home-page: http://www.tryton.org/
Author: Tryton
-Author-email: UNKNOWN
+Author-email: issue_tracker at tryton.org
License: GPL-3
-Download-URL: http://downloads.tryton.org/3.0/
+Download-URL: http://downloads.tryton.org/3.2/
Description: trytond
=======
@@ -56,6 +56,8 @@ Description: trytond
* MySQL can not create indexes containing text or blob fields.
+ * Timestamp has a precision of second which is used for optimistic lock.
+
* Tryton uses a DECIMAL(65, 30) for Decimal fields and DOUBLE(255, 30) for
Float fields.
@@ -67,6 +69,8 @@ Description: trytond
* SQL constraints are validated by Tryton instead of database.
+ * Timestamp has a precision of second which is used for optimistic lock.
+
Support
-------
@@ -93,7 +97,8 @@ Description: trytond
http://www.tryton.org/
-Platform: UNKNOWN
+Keywords: business application platform ERP
+Platform: any
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Framework :: Tryton
@@ -110,6 +115,5 @@ Classifier: Natural Language :: Russian
Classifier: Natural Language :: Slovenian
Classifier: Natural Language :: Spanish
Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
diff --git a/trytond.egg-info/SOURCES.txt b/trytond.egg-info/SOURCES.txt
index 6c96d96..d3ca803 100644
--- a/trytond.egg-info/SOURCES.txt
+++ b/trytond.egg-info/SOURCES.txt
@@ -28,6 +28,7 @@ doc/topics/install.rst
doc/topics/pyson.rst
doc/topics/wizard.rst
doc/topics/models/fields_default_value.rst
+doc/topics/models/fields_on_change.rst
doc/topics/models/index.rst
doc/topics/modules/index.rst
doc/topics/reports/index.rst
@@ -183,6 +184,7 @@ trytond/ir/view/model_field_list.xml
trytond/ir/view/model_form.xml
trytond/ir/view/model_list.xml
trytond/ir/view/model_print_model_graph_start_form.xml
+trytond/ir/view/module_config_wizard_done_form.xml
trytond/ir/view/module_config_wizard_first_form.xml
trytond/ir/view/module_config_wizard_item_list.xml
trytond/ir/view/module_config_wizard_other_form.xml
@@ -297,10 +299,12 @@ trytond/tests/__init__.py
trytond/tests/access.py
trytond/tests/copy_.py
trytond/tests/export_data.py
+trytond/tests/history.py
trytond/tests/import_data.py
trytond/tests/import_data.xml
trytond/tests/model.py
trytond/tests/mptt.py
+trytond/tests/run-tests.py
trytond/tests/sequence.xml
trytond/tests/test.py
trytond/tests/test_access.py
@@ -308,6 +312,7 @@ trytond/tests/test_cache.py
trytond/tests/test_copy.py
trytond/tests/test_exportdata.py
trytond/tests/test_fields.py
+trytond/tests/test_history.py
trytond/tests/test_importdata.py
trytond/tests/test_mixins.py
trytond/tests/test_modelsingleton.py
@@ -319,6 +324,7 @@ trytond/tests/test_tools.py
trytond/tests/test_transaction.py
trytond/tests/test_trigger.py
trytond/tests/test_tryton.py
+trytond/tests/test_user.py
trytond/tests/test_wizard.py
trytond/tests/test_workflow.py
trytond/tests/trigger.py
@@ -331,7 +337,6 @@ trytond/tools/StringMatcher.py
trytond/tools/__init__.py
trytond/tools/datetime_strftime.py
trytond/tools/misc.py
-trytond/tools/ordereddict.py
trytond/tools/singleton.py
trytond/webdav/__init__.py
trytond/webdav/tryton.cfg
diff --git a/trytond.egg-info/requires.txt b/trytond.egg-info/requires.txt
index efcab94..25bd7b5 100644
--- a/trytond.egg-info/requires.txt
+++ b/trytond.egg-info/requires.txt
@@ -23,8 +23,8 @@ simplejson
[MySQL]
MySQL-python
-[timezone]
-pytz
+[BCrypt]
+bcrypt
[Levenshtein]
python-Levenshtein
diff --git a/trytond/__init__.py b/trytond/__init__.py
index 6fb585c..80f6ae6 100644
--- a/trytond/__init__.py
+++ b/trytond/__init__.py
@@ -1,5 +1,12 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
+import os
+import time
+
from . import server
__all__ = ['server']
+
+os.environ['TZ'] = 'UTC'
+if hasattr(time, 'tzset'):
+ time.tzset()
diff --git a/trytond/backend/database.py b/trytond/backend/database.py
index c80e370..2979c66 100644
--- a/trytond/backend/database.py
+++ b/trytond/backend/database.py
@@ -41,7 +41,8 @@ class DatabaseInterface(object):
'''
raise NotImplementedError
- def create(self, cursor, database_name):
+ @staticmethod
+ def create(cursor, database_name):
'''
Create a database
@@ -49,7 +50,8 @@ class DatabaseInterface(object):
'''
raise NotImplementedError
- def drop(self, cursor, database_name):
+ @staticmethod
+ def drop(cursor, database_name):
'''
Drop a database
diff --git a/trytond/backend/mysql/database.py b/trytond/backend/mysql/database.py
index 1be7c0b..f61136f 100644
--- a/trytond/backend/mysql/database.py
+++ b/trytond/backend/mysql/database.py
@@ -100,19 +100,23 @@ class Database(DatabaseInterface):
if CONFIG['db_password']:
args['passwd'] = CONFIG['db_password']
conn = MySQLdb.connect(**args)
- return Cursor(conn, self.database_name)
+ cursor = Cursor(conn, self.database_name)
+ cursor.execute('SET time_zone = `UTC`')
+ return cursor
def close(self):
return
- def create(self, cursor, database_name):
+ @classmethod
+ def create(cls, cursor, database_name):
cursor.execute('CREATE DATABASE `' + database_name + '` '
'DEFAULT CHARACTER SET = \'utf8\'')
- Database._list_cache = None
+ cls._list_cache = None
- def drop(self, cursor, database_name):
+ @classmethod
+ def drop(cls, cursor, database_name):
cursor.execute('DROP DATABASE `' + database_name + '`')
- Database._list_cache = None
+ cls._list_cache = None
@staticmethod
def dump(database_name):
diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py
index 761a055..ca444cd 100644
--- a/trytond/backend/postgresql/database.py
+++ b/trytond/backend/postgresql/database.py
@@ -26,8 +26,6 @@ from sql import Flavor
__all__ = ['Database', 'DatabaseIntegrityError', 'DatabaseOperationalError',
'Cursor']
-RE_FROM = re.compile('.* from "?([a-zA-Z_0-9]+)"?.*$')
-RE_INTO = re.compile('.* into "?([a-zA-Z_0-9]+)"?.*$')
RE_VERSION = re.compile(r'\S+ (\d+)\.(\d+)')
os.environ['PGTZ'] = os.environ.get('TZ', '')
@@ -87,14 +85,16 @@ class Database(DatabaseInterface):
self._connpool.closeall()
self._connpool = None
- def create(self, cursor, database_name):
+ @classmethod
+ def create(cls, cursor, database_name):
cursor.execute('CREATE DATABASE "' + database_name + '" '
'TEMPLATE template0 ENCODING \'unicode\'')
- Database._list_cache = None
+ cls._list_cache = None
- def drop(self, cursor, database_name):
+ @classmethod
+ def drop(cls, cursor, database_name):
cursor.execute('DROP DATABASE "' + database_name + '"')
- Database._list_cache = None
+ cls._list_cache = None
def get_version(self, cursor):
if self.database_name not in self._version_cache:
@@ -353,7 +353,7 @@ class Cursor(CursorInterface):
return self.cursor.fetchone()[0]
def lock(self, table):
- self.cursor.execute('LOCK "%s"' % table)
+ self.cursor.execute('LOCK "%s" IN EXCLUSIVE MODE' % table)
def has_constraint(self):
return True
diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py
index d0e4b52..9f132a0 100644
--- a/trytond/backend/sqlite/database.py
+++ b/trytond/backend/sqlite/database.py
@@ -8,6 +8,7 @@ import datetime
import time
import sys
import threading
+import math
_FIX_ROWCOUNT = False
try:
@@ -132,6 +133,10 @@ class SQLiteOverlay(Function):
return string[:from_ - 1] + placing_string + string[from_ - 1 + for_:]
+def sign(value):
+ return math.copysign(1, value)
+
+
MAPPING = {
Extract: SQLiteExtract,
Position: SQLitePosition,
@@ -148,8 +153,7 @@ class Database(DatabaseInterface):
def __new__(cls, database_name=':memory:'):
if (database_name == ':memory:'
- and hasattr(cls._local, 'memory_database')
- and cls._local.memory_database):
+ and getattr(cls._local, 'memory_database', None)):
return cls._local.memory_database
return DatabaseInterface.__new__(cls, database_name=database_name)
@@ -178,6 +182,7 @@ class Database(DatabaseInterface):
if sqlite.sqlite_version_info < (3, 3, 14):
self._conn.create_function('replace', 3, replace)
self._conn.create_function('now', 0, now)
+ self._conn.create_function('sign', 1, sign)
self._conn.execute('PRAGMA foreign_keys = ON')
return self
@@ -197,7 +202,8 @@ class Database(DatabaseInterface):
return
self._conn = None
- def create(self, cursor, database_name):
+ @staticmethod
+ def create(cursor, database_name):
if database_name == ':memory:':
path = ':memory:'
else:
@@ -209,9 +215,10 @@ class Database(DatabaseInterface):
cursor = conn.cursor()
cursor.close()
- def drop(self, cursor, database_name):
+ @classmethod
+ def drop(cls, cursor, database_name):
if database_name == ':memory:':
- self._conn = None
+ cls._local.memory_database._conn = None
return
if os.sep in database_name:
return
@@ -356,21 +363,23 @@ class Cursor(CursorInterface):
self._conn.rollback()
def test(self):
+ sqlite_master = Table('sqlite_master')
+ select = sqlite_master.select(sqlite_master.name)
+ select.where = sqlite_master.type == 'table'
+ select.where &= sqlite_master.name.in_([
+ 'ir_model',
+ 'ir_model_field',
+ 'ir_ui_view',
+ 'ir_ui_menu',
+ 'res_user',
+ 'res_group',
+ 'ir_module_module',
+ 'ir_module_module_dependency',
+ 'ir_translation',
+ 'ir_lang',
+ ])
try:
- self.cursor.execute("SELECT name "
- "FROM sqlite_master "
- "WHERE type = 'table' AND name in ("
- "'ir_model', "
- "'ir_model_field', "
- "'ir_ui_view', "
- "'ir_ui_menu', "
- "'res_user', "
- "'res_group', "
- "'ir_module_module', "
- "'ir_module_module_dependency', "
- "'ir_translation', "
- "'ir_lang'"
- ")")
+ self.cursor.execute(*select)
except Exception:
return False
return len(self.cursor.fetchall()) != 0
diff --git a/trytond/cache.py b/trytond/cache.py
index e7ccbd2..c6d7adb 100644
--- a/trytond/cache.py
+++ b/trytond/cache.py
@@ -2,10 +2,11 @@
#this repository contains the full copyright notices and license terms.
import datetime
from threading import Lock
+from collections import OrderedDict
+
from trytond.transaction import Transaction
from trytond.config import CONFIG
from trytond import backend
-from trytond.tools import OrderedDict
__all__ = ['Cache', 'LRUDict']
diff --git a/trytond/config.py b/trytond/config.py
index 417ea58..ba67332 100644
--- a/trytond/config.py
+++ b/trytond/config.py
@@ -11,7 +11,6 @@ except ImportError:
sys.modules['cdecimal'] = decimal
import os
import ConfigParser
-import time
import getpass
import socket
@@ -84,15 +83,9 @@ class ConfigManager(object):
'unoconv': 'pipe,name=trytond;urp;StarOffice.ComponentContext',
'retry': 5,
'language': 'en_US',
- 'timezone': time.tzname[0] or time.tzname[1],
}
self.configfile = None
- def set_timezone(self):
- os.environ['TZ'] = self.get('timezone')
- if hasattr(time, 'tzset'):
- time.tzset()
-
def update_cmdline(self, cmdline_options):
self.options.update(cmdline_options)
diff --git a/trytond/convert.py b/trytond/convert.py
index 259db7f..378da55 100644
--- a/trytond/convert.py
+++ b/trytond/convert.py
@@ -137,6 +137,9 @@ class MenuitemTagHandler:
else:
values['name'] = action_name
+ if values.get('sequence'):
+ values['sequence'] = int(values['sequence'])
+
self.values = values
def characters(self, data):
@@ -402,7 +405,13 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
self.module_state = module_state
self.grouped = None
self.grouped_creations = defaultdict(dict)
+ self.grouped_write = defaultdict(list)
+ self.grouped_model_data = []
self.skip_data = False
+ Module = pool.get('ir.module.module')
+ self.installed_modules = [m.name for m in Module.search([
+ ('state', 'in', ['installed', 'to upgrade']),
+ ])]
# Tag handlders are used to delegate the processing
self.taghandlerlist = {
@@ -454,6 +463,11 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
self.skip_data = True
else:
self.skip_data = False
+ depends = attributes.get('depends', '').split(',')
+ depends = [m.strip() for m in depends if m]
+ if depends:
+ if not all((m in self.installed_modules for m in depends)):
+ self.skip_data = True
elif name == "tryton":
pass
@@ -475,6 +489,13 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
for model, values in self.grouped_creations.iteritems():
self.create_records(model, values.values(), values.keys())
self.grouped_creations.clear()
+ for key, actions in self.grouped_write.iteritems():
+ module, model = key
+ self.write_records(module, model, *actions)
+ self.grouped_write.clear()
+ if self.grouped_model_data:
+ self.ModelData.write(*self.grouped_model_data)
+ del self.grouped_model_data[:]
# Closing tag found, if we are in a delegation the handler
# know what to do:
@@ -664,42 +685,12 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
# and no user changed the value in the db:
to_update[key] = values[key]
- # if there is values to update:
- if to_update:
- # write the values in the db:
- with Transaction().set_context(
- module=module, language='en_US'):
- Model.write([record], to_update)
- self.fs2db.reset_browsercord(
- module, Model.__name__, [record.id])
-
- if to_update:
- # re-read it: this ensure that we store the real value
- # in the model_data table:
- record = self.fs2db.get_browserecord(
- module, Model.__name__, record.id)
- if not record:
- record = Model(record.id)
- for key in to_update:
- values[key] = self._clean_value(key, record)
-
- if module != self.module:
- temp_values = old_values.copy()
- temp_values.update(values)
- values = temp_values
-
- if values != old_values:
- self.ModelData.write([self.ModelData(mdata_id)], {
- 'fs_id': fs_id,
- 'model': model,
- 'module': module,
- 'db_id': record.id,
- 'values': str(values),
- 'date_update': datetime.datetime.now(),
- })
- # reset_browsercord to keep cache memory low
- self.fs2db.reset_browsercord(module, Model.__name__, [record.id])
-
+ if self.grouped:
+ self.grouped_write[(module, model)].extend(
+ (record, to_update, old_values, fs_id, mdata_id))
+ else:
+ self.write_records(module, model, record, to_update,
+ old_values, fs_id, mdata_id)
else:
if self.grouped:
self.grouped_creations[model][fs_id] = values
@@ -739,6 +730,57 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
self.fs2db.reset_browsercord(self.module, model,
[r.id for r in records])
+ def write_records(self, module, model,
+ record, values, old_values, fs_id, mdata_id, *args):
+ args = (record, values, old_values, fs_id, mdata_id) + args
+ Model = self.pool.get(model)
+
+ actions = iter(args)
+ to_update = []
+ for record, values, _, _, _ in zip(*((actions,) * 5)):
+ if values:
+ to_update += [[record], values]
+ # if there is values to update:
+ if to_update:
+ # write the values in the db:
+ with Transaction().set_context(
+ module=module, language='en_US'):
+ Model.write(*to_update)
+ self.fs2db.reset_browsercord(
+ module, Model.__name__, sum(to_update[::2], []))
+
+ actions = iter(to_update)
+ for records, values in zip(actions, actions):
+ record, = records
+ # re-read it: this ensure that we store the real value
+ # in the model_data table:
+ record = self.fs2db.get_browserecord(
+ module, Model.__name__, record.id)
+ if not record:
+ record = Model(record.id)
+ for key in values:
+ values[key] = self._clean_value(key, record)
+
+ actions = iter(args)
+ for record, values, old_values, fs_id, mdata_id in zip(
+ *((actions,) * 5)):
+ temp_values = old_values.copy()
+ temp_values.update(values)
+ values = temp_values
+
+ if values != old_values:
+ self.grouped_model_data.extend(([self.ModelData(mdata_id)], {
+ 'fs_id': fs_id,
+ 'model': model,
+ 'module': module,
+ 'db_id': record.id,
+ 'values': str(values),
+ 'date_update': datetime.datetime.now(),
+ }))
+
+ # reset_browsercord to keep cache memory low
+ self.fs2db.reset_browsercord(module, Model.__name__, args[::5])
+
def post_import(pool, module, to_delete):
"""
diff --git a/trytond/ir/__init__.py b/trytond/ir/__init__.py
index ef6e0c1..062897b 100644
--- a/trytond/ir/__init__.py
+++ b/trytond/ir/__init__.py
@@ -72,6 +72,7 @@ def register():
ModuleConfigWizardItem,
ModuleConfigWizardFirst,
ModuleConfigWizardOther,
+ ModuleConfigWizardDone,
ModuleInstallUpgradeStart,
ModuleInstallUpgradeDone,
Cache,
diff --git a/trytond/ir/action.py b/trytond/ir/action.py
index 25a5bfa..873a131 100644
--- a/trytond/ir/action.py
+++ b/trytond/ir/action.py
@@ -56,9 +56,9 @@ class Action(ModelSQL, ModelView):
return True
@classmethod
- def write(cls, actions, values):
+ def write(cls, actions, values, *args):
pool = Pool()
- super(Action, cls).write(actions, values)
+ super(Action, cls).write(actions, values, *args)
pool.get('ir.action.keyword')._get_keyword_cache.clear()
@classmethod
@@ -194,9 +194,12 @@ class ActionKeyword(ModelSQL, ModelView):
return super(ActionKeyword, cls).create(new_vlist)
@classmethod
- def write(cls, keywords, vals):
- vals = cls._convert_vals(vals)
- super(ActionKeyword, cls).write(keywords, vals)
+ def write(cls, keywords, values, *args):
+ actions = iter((keywords, values) + args)
+ args = []
+ for keywords, values in zip(actions, actions):
+ args.extend((keywords, cls._convert_vals(values)))
+ super(ActionKeyword, cls).write(*args)
ModelView._fields_view_get_cache.clear()
ModelView._view_toolbar_get_cache.clear()
cls._get_keyword_cache.clear()
@@ -223,6 +226,7 @@ class ActionKeyword(ModelSQL, ModelView):
('model', '=', model + ',' + str(model_id)),
],
]
+ clause = [clause, ('action.active', '=', True)]
action_keywords = cls.search(clause, order=[])
types = defaultdict(list)
for action_keyword in action_keywords:
@@ -328,10 +332,10 @@ class ActionMixin(ModelSQL):
return new_records
@classmethod
- def write(cls, records, values):
+ def write(cls, records, values, *args):
pool = Pool()
ActionKeyword = pool.get('ir.action.keyword')
- super(ActionMixin, cls).write(records, values)
+ super(ActionMixin, cls).write(records, values, *args)
ActionKeyword._get_keyword_cache.clear()
@classmethod
@@ -364,8 +368,12 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
report_name = fields.Char('Internal Name', required=True)
report = fields.Char('Path')
report_content_custom = fields.Binary('Content')
- report_content = fields.Function(fields.Binary('Content'),
- 'get_report_content', setter='set_report_content')
+ report_content = fields.Function(fields.Binary('Content',
+ filename='report_content_name'),
+ 'get_report_content', setter='set_report_content')
+ report_content_name = fields.Function(fields.Char('Content Name',
+ on_change_with=['name', 'template_extension']),
+ 'on_change_with_report_content_name')
action = fields.Many2One('ir.action', 'Action', required=True,
ondelete='CASCADE')
style = fields.Property(fields.Char('Style',
@@ -594,6 +602,11 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
def set_report_content(cls, records, name, value):
cls.write(records, {'%s_custom' % name: value})
+ def on_change_with_report_content_name(self, name=None):
+ if not self.name:
+ return
+ return ''.join([self.name, os.extsep, self.template_extension])
+
@classmethod
def get_style_content(cls, reports, name):
contents = {}
@@ -644,13 +657,18 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
return new_reports
@classmethod
- def write(cls, reports, vals):
+ def write(cls, reports, values, *args):
context = Transaction().context
if 'module' in context:
- vals = vals.copy()
- vals['module'] = context['module']
-
- super(ActionReport, cls).write(reports, vals)
+ actions = iter((reports, values) + args)
+ args = []
+ for reports, values in zip(actions, actions):
+ values = values.copy()
+ values['module'] = context['module']
+ args.extend((reports, values))
+ reports, values = args[:2]
+ args = args[2:]
+ super(ActionReport, cls).write(reports, values, *args)
class ActionActWindow(ActionMixin, ModelSQL, ModelView):
@@ -668,8 +686,6 @@ class ActionActWindow(ActionMixin, ModelSQL, ModelView):
domains = fields.Function(fields.Binary('Domains'), 'get_domains')
limit = fields.Integer('Limit', required=True,
help='Default limit for the list view')
- auto_refresh = fields.Integer('Auto-Refresh', required=True,
- help='Add an auto-refresh on the view')
action = fields.Many2One('ir.action', 'Action', required=True,
ondelete='CASCADE')
window_name = fields.Boolean('Window Name',
@@ -701,14 +717,20 @@ class ActionActWindow(ActionMixin, ModelSQL, ModelView):
@classmethod
def __register__(cls, module_name):
cursor = Transaction().cursor
+ TableHandler = backend.get('TableHandler')
act_window = cls.__table__()
super(ActionActWindow, cls).__register__(module_name)
+ table = TableHandler(cursor, cls, module_name)
+
# Migration from 2.0: new search_value format
cursor.execute(*act_window.update(
[act_window.search_value], ['[]'],
where=act_window.search_value == '{}'))
+ # Migration from 3.0: auto_refresh removed
+ table.drop_column('auto_refresh')
+
@staticmethod
def default_type():
return 'ir.action.act_window'
@@ -722,10 +744,6 @@ class ActionActWindow(ActionMixin, ModelSQL, ModelView):
return 0
@staticmethod
- def default_auto_refresh():
- return 0
-
- @staticmethod
def default_window_name():
return True
@@ -907,9 +925,9 @@ class ActionActWindowView(ModelSQL, ModelView):
return windows
@classmethod
- def write(cls, windows, values):
+ def write(cls, windows, values, *args):
pool = Pool()
- super(ActionActWindowView, cls).write(windows, values)
+ super(ActionActWindowView, cls).write(windows, values, *args)
pool.get('ir.action.keyword')._get_keyword_cache.clear()
@classmethod
diff --git a/trytond/ir/attachment.py b/trytond/ir/attachment.py
index 09e340e..d261509 100644
--- a/trytond/ir/attachment.py
+++ b/trytond/ir/attachment.py
@@ -1,11 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import os
-try:
- import hashlib
-except ImportError:
- hashlib = None
- import md5
+import hashlib
from sql.operators import Concat
from ..model import ModelView, ModelSQL, fields
@@ -39,8 +35,7 @@ class Attachment(ModelSQL, ModelView):
'invisible': Eval('type') != 'data',
}, depends=['type']), 'get_data', setter='set_data')
description = fields.Text('Description')
- summary = fields.Function(fields.Char('Summary',
- on_change_with=['description']), 'on_change_with_summary')
+ summary = fields.Function(fields.Char('Summary'), 'on_change_with_summary')
resource = fields.Reference('Resource', selection='models_get',
select=True)
link = fields.Char('Link', states={
@@ -148,10 +143,7 @@ class Attachment(ModelSQL, ModelView):
directory = os.path.join(CONFIG['data_path'], db_name)
if not os.path.isdir(directory):
os.makedirs(directory, 0770)
- if hashlib:
- digest = hashlib.md5(value).hexdigest()
- else:
- digest = md5.new(value).hexdigest()
+ digest = hashlib.md5(value).hexdigest()
directory = os.path.join(directory, digest[0:2], digest[2:4])
if not os.path.isdir(directory):
os.makedirs(directory, 0770)
@@ -191,6 +183,7 @@ class Attachment(ModelSQL, ModelView):
'collision': collision,
})
+ @fields.depends('description')
def on_change_with_summary(self, name=None):
return firstline(self.description or '')
@@ -231,10 +224,14 @@ class Attachment(ModelSQL, ModelView):
super(Attachment, cls).delete(attachments)
@classmethod
- def write(cls, attachments, vals):
- cls.check_access([a.id for a in attachments], mode='write')
- super(Attachment, cls).write(attachments, vals)
- cls.check_access(attachments, mode='write')
+ def write(cls, attachments, values, *args):
+ all_attachments = []
+ actions = iter((attachments, values) + args)
+ for records, _ in zip(actions, actions):
+ all_attachments += records
+ cls.check_access([a.id for a in all_attachments], mode='write')
+ super(Attachment, cls).write(attachments, values, *args)
+ cls.check_access(all_attachments, mode='write')
@classmethod
def create(cls, vlist):
diff --git a/trytond/ir/cron.py b/trytond/ir/cron.py
index 904a2fd..f255284 100644
--- a/trytond/ir/cron.py
+++ b/trytond/ir/cron.py
@@ -1,6 +1,5 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
-import contextlib
import datetime
from dateutil.relativedelta import relativedelta
import traceback
@@ -167,8 +166,8 @@ class Cron(ModelSQL, ModelView):
req_user = cron.request_user
language = (req_user.language.code if req_user.language
else Config.get_language())
- with contextlib.nested(Transaction().set_user(cron.user.id),
- Transaction().set_context(language=language)):
+ with Transaction().set_user(cron.user.id), \
+ Transaction().set_context(language=language):
cls.send_error_message(cron)
@classmethod
diff --git a/trytond/ir/date.py b/trytond/ir/date.py
index 2af7467..fabf04a 100644
--- a/trytond/ir/date.py
+++ b/trytond/ir/date.py
@@ -22,8 +22,8 @@ class Date(Model):
})
@staticmethod
- def today():
+ def today(timezone=None):
'''
Return the current date
'''
- return datetime.date.today()
+ return datetime.datetime.now(timezone).date()
diff --git a/trytond/ir/lang.py b/trytond/ir/lang.py
index d2270b9..a841274 100644
--- a/trytond/ir/lang.py
+++ b/trytond/ir/lang.py
@@ -213,12 +213,12 @@ class Lang(ModelSQL, ModelView):
return languages
@classmethod
- def write(cls, langs, vals):
+ def write(cls, langs, values, *args):
pool = Pool()
Translation = pool.get('ir.translation')
# Clear cache
cls._lang_cache.clear()
- super(Lang, cls).write(langs, vals)
+ super(Lang, cls).write(langs, values, *args)
Translation._get_language_cache.clear()
@classmethod
diff --git a/trytond/ir/locale/bg_BG.po b/trytond/ir/locale/bg_BG.po
index 8f008d2..56ebd1b 100644
--- a/trytond/ir/locale/bg_BG.po
+++ b/trytond/ir/locale/bg_BG.po
@@ -21,20 +21,26 @@ msgid "You are not allowed to delete this record."
msgstr "Нямате права да изтривате този запис."
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "Полето \"%s\" от \"%s\" има много десетични цифри"
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
+msgstr ""
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
-msgstr "Стойността на полето \"%s\" от \"%s\" не е валидно според домейна."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
+msgstr ""
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
-msgstr "Не може да изтривате записи \"%s\" защото се използват в поле \"%s\" от \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
+msgstr ""
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "Стойността на полето \"%s\" от \"%s\" не съществува."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr ""
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -242,10 +248,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr ""
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "Ключа %r не е намерен в полето за избор %r"
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -275,19 +277,21 @@ msgid "Relation not found: %r in %s"
msgstr "Не е намерена зависимост: %r в %s"
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Полето \"%s\" от \"%s\" е задължително."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr ""
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Полето \"%s\" от \"%s\" е задължително."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr ""
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
msgstr "Липсват функции за търсене върху поле \"%s\"."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
msgstr ""
msgctxt "error:selection_value_notfound:"
@@ -295,11 +299,11 @@ msgid "Value not in the selection for field \"%s\"."
msgstr ""
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "Полето \"%s\" от \"%s\" е много дълго."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr ""
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
msgstr ""
msgctxt "error:too_many_relations_found:"
@@ -401,10 +405,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Активен"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Самообновяване"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr "Стойност на котекст"
@@ -724,6 +724,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Съдържание"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr ""
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Вътрешно име"
@@ -1608,6 +1612,11 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr "Променено от"
+#, fuzzy
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr "ID"
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr "ID"
@@ -2669,10 +2678,6 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr "Променено от"
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Добавяне на самообновяване на изгледа"
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr "Ограничение по подразбиране за изглед със списък"
@@ -3114,6 +3119,10 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr "Модул"
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr ""
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
msgstr "Първи помощник на конфигуриране на модул"
@@ -3770,6 +3779,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Описание на модел"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr ""
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr ""
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "Добре дошли в помощника за конфигуриране на модула!"
@@ -4034,6 +4051,11 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr "Печат"
+#, fuzzy
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "Добре"
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr "Добре"
diff --git a/trytond/ir/locale/ca_ES.po b/trytond/ir/locale/ca_ES.po
index b07b6b7..1f2be61 100644
--- a/trytond/ir/locale/ca_ES.po
+++ b/trytond/ir/locale/ca_ES.po
@@ -23,20 +23,30 @@ msgid "You are not allowed to delete this record."
msgstr "No podeu eliminar aquest registre."
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "El camp \"%s\" de \"%s\" té massa decimals."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
+msgstr ""
+"El nombre de dígits \"%(digits)s\" del camp \"%(field)s\" de %\"(value)s\" "
+"és superior al límit."
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
msgstr "El valor del camp \"%s\" de \"%s\" no és correcte segons aquest domini."
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
-msgstr "No podeu eliminar \"%s\" perquè s'utilitzen en els camps \"%s\" de \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
+msgstr ""
+"No es poden eliminar els registres perquè es fan servir al camp "
+"\"%(field)s\" de \"%(models)\"."
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "El valor del camp \"%s\" de \"%s\" no existeix."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr "El valor \"%(value)s\" del camp \"%(field)s\" de \"%(model)s\" no existeix."
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -83,13 +93,13 @@ msgid ""
msgstr ""
"Ha fallat l'acció quan s'executava: \"%s\"\n"
"%s\n"
-" Traceback: \n"
+" Traça del programa: \n"
"\n"
"%s\n"
msgctxt "error:ir.lang:"
msgid "Default language can not be deleted."
-msgstr "El idioma per defecte no es pot eliminar."
+msgstr "L'idioma per defecte no es pot eliminar."
msgctxt "error:ir.lang:"
msgid "Invalid date format \"%(format)s\" on \"%(language)s\" language."
@@ -97,7 +107,7 @@ msgstr "El format de la data \"%(format)s\" de \"%(language)s\" no és correcte.
msgctxt "error:ir.lang:"
msgid "Invalid grouping \"%(grouping)s\" on \"%(language)s\" language."
-msgstr "La agrupació \"%(grouping)s\" de \"%(language)s\" no és correcte."
+msgstr "L'agrupació \"%(grouping)s\" de \"%(language)s\" no és correcte."
msgctxt "error:ir.lang:"
msgid "The default language must be translatable."
@@ -105,15 +115,15 @@ msgstr "L'idioma per defecte ha de ser traduïble."
msgctxt "error:ir.lang:"
msgid "decimal_point and thousands_sep must be different!"
-msgstr "decimal_point i thousands_sep han de ser diferents."
+msgstr "El punt decimal i el separador de milers han de ser diferents."
msgctxt "error:ir.model.access:"
msgid "You can not create this kind of document! (%s)"
-msgstr "No podeu crear aquest tipus de document. (%s)"
+msgstr "No podeu crear aquest tipus de document (%s)."
msgctxt "error:ir.model.access:"
msgid "You can not delete this document! (%s)"
-msgstr "No podeu eliminar aquest document (%s)"
+msgstr "No podeu eliminar aquest document (%s)."
msgctxt "error:ir.model.access:"
msgid "You can not read this document! (%s)"
@@ -125,7 +135,7 @@ msgstr "No podeu modificar aquest document (%s)."
msgctxt "error:ir.model.button:"
msgid "The button name in model must be unique!"
-msgstr "El nom del butó del model ha de ser únic."
+msgstr "El nom del botó del model ha de ser únic."
msgctxt "error:ir.model.data:"
msgid "The triple (fs_id, module, model) must be unique!"
@@ -141,7 +151,7 @@ msgstr "No podeu modificar el camp (%s.%s)."
msgctxt "error:ir.model.field:"
msgid "Model Field name \"%s\" is not a valid python identifier."
-msgstr "El nom del model \"%s\" no és un identificador de python correcte."
+msgstr "El nom del model \"%s\" no és un identificador de Python correcte."
msgctxt "error:ir.model.field:"
msgid "The field name in model must be unique!"
@@ -149,7 +159,7 @@ msgstr "El nom del camp ha de ser únic."
msgctxt "error:ir.model:"
msgid "Module name \"%s\" is not a valid python identifier."
-msgstr "El nom del model \"%s\" no és un identificador de python correcte."
+msgstr "El nom del model \"%s\" no és un identificador de Python correcte."
msgctxt "error:ir.model:"
msgid "The model must be unique!"
@@ -161,12 +171,13 @@ msgstr "La dependència ha de ser únic en el mòdul."
msgctxt "error:ir.module.module:"
msgid "Missing dependencies %s for module \"%s\""
-msgstr "No es troba la dependècia %s pel mòdul \"%s\"."
+msgstr "No es troba la dependència %s pel mòdul \"%s\"."
msgctxt "error:ir.module.module:"
msgid "The modules you are trying to uninstall depends on installed modules:"
msgstr ""
-"Els mòduls que està intentant desinstal·lar depenen de mòduls instal·lats:"
+"Els mòduls que esteu intentant desinstal·lar depenen d'altres mòduls "
+"instal·lats:"
msgctxt "error:ir.module.module:"
msgid "The name of the module must be unique!"
@@ -194,7 +205,7 @@ msgstr "Sufix \"%(sufix)s\" de la seqüència \"%(sequence)s\" no és correcte."
msgctxt "error:ir.sequence.strict:"
msgid "Last Timestamp cannot be in the future on sequence \"%s\"."
-msgstr "La darrera data-hora no pot ser del futur a la seqüència \"%s\"."
+msgstr "L'última data-hora no pot ser del futur a la seqüència \"%s\"."
msgctxt "error:ir.sequence.strict:"
msgid "Missing sequence."
@@ -202,7 +213,7 @@ msgstr "No es troba la seqüència."
msgctxt "error:ir.sequence.strict:"
msgid "Timestamp rounding should be greater than 0"
-msgstr "L'arrodiment Timestamp ha de ser mes gran que 0."
+msgstr "L'arrodoniment de data-hora ha de ser més gran que 0."
msgctxt "error:ir.sequence:"
msgid "Invalid prefix \"%(prefix)s\" on sequence \"%(sequence)s\"."
@@ -214,7 +225,7 @@ msgstr "Sufix \"%(sufix)s\" de la seqüència \"%(sequence)s\" no és correcte."
msgctxt "error:ir.sequence:"
msgid "Last Timestamp cannot be in the future on sequence \"%s\"."
-msgstr "La darrera data-hora no pot ser del futur a la seqüència \"%s\"."
+msgstr "L'última data-hora no pot ser del futur a la seqüència \"%s\"."
msgctxt "error:ir.sequence:"
msgid "Missing sequence."
@@ -222,7 +233,7 @@ msgstr "No es troba la seqüència."
msgctxt "error:ir.sequence:"
msgid "Timestamp rounding should be greater than 0"
-msgstr "L'arrodiment Timestamp ha de ser mes gran que 0."
+msgstr "L'arrodoniment de data-hora ha de ser més gran que 0."
msgctxt "error:ir.translation:"
msgid "Translation must be unique"
@@ -234,18 +245,18 @@ msgid ""
"translation by module %(overriding_module)s"
msgstr ""
"No podeu exportar la traducció %(name)s perquè s'utilitza en el mòdul "
-"%(overriding_module)s"
+"%(overriding_module)s."
msgctxt "error:ir.trigger:"
msgid "\"On Time\" and others are mutually exclusive!"
-msgstr "\"Al moment\" i altres son mutuament exclusius."
+msgstr "\"Al moment\" i altres són mútuament exclusius."
msgctxt "error:ir.trigger:"
msgid ""
"Condition \"%(condition)s\" is not a valid python expression on trigger "
"\"%(trigger)s\"."
msgstr ""
-"La condició \"%(condition)s\" no és una expressió python correcte en el "
+"La condició \"%(condition)s\" no és una expressió Python correcte en el "
"disparador \"%(trigger)s\"."
msgctxt "error:ir.ui.menu:"
@@ -256,10 +267,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr "XML en les vistes \"%s\" no és correcte."
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "No es troba la clau %r en la selecció del camp %r."
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -281,44 +288,50 @@ msgid ""
"Recursion error: Record \"%(rec_name)s\" with parent \"%(parent_rec_name)s\""
" was configured as ancestor of itself."
msgstr ""
-"Error de recurssió: El registre \"%(rec_name)s\" amb el pare "
+"Error de recursivitat: El registre \"%(rec_name)s\" amb el pare "
"\"%(parent_rec_name)s\" es va configurar com pare de si mateix."
msgctxt "error:reference_syntax_error:"
msgid "Syntax error for reference %r in %s"
-msgstr "Error de sintaxis en la referència %r a %s"
+msgstr "Error de sintaxi en la referència %r a %s."
msgctxt "error:relation_not_found:"
msgid "Relation not found: %r in %s"
-msgstr "No s'ha trobat la relació: %r a %s"
+msgstr "No s'ha trobat la relació: %r a %s."
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "El camp \"%s\" de \"%s\" es requerit."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "El camp \"%(field)s\" de \"%(model)s\" es obligatori."
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "El camp \"%s\" de \"%s\" es requerit."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "El camp \"%(field)s\" de \"%(model)s\" es obligatori."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
msgstr "No es troba la funció de cerca del camp \"%s\"."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
-msgstr "El camp \"%s\" de \"%s\" no es troba en la selecció."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
+msgstr ""
+"El valor \"%(value)s\" del camp \"%(field)s\" de \"%(model)s\" no està a la "
+"selecció."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
msgstr "El valor no es troba en la selecció en el camp \"%s\"."
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "El camp \"%s\" de \"%s\" és massa llarg."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr "La mida \"%(size)s\" del camp \"%(field)s\" de \"%(model)s\" es massa gran."
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
-msgstr "El valor d'hora del camp \"%s\" a \"%s\" no és correcte."
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
+msgstr ""
+"El valor de temps \"%(value)s\" del camp %(field)s\" de \"%(model)s\" no és "
+"vàlid."
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
@@ -346,7 +359,7 @@ msgstr "No podeu modificar aquest registre."
msgctxt "error:xml_id_syntax_error:"
msgid "Syntax error for XML id %r in %s"
-msgstr "Error de sintaxis XML id %r a %s"
+msgstr "Error de sintaxi XML id %r a %s."
msgctxt "error:xml_record_desc:"
msgid "This record is part of the base configuration."
@@ -420,10 +433,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Actiu"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Actualizació automatitzada"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr "Context"
@@ -478,7 +487,7 @@ msgstr "PySON Context"
msgctxt "field:ir.action.act_window,pyson_domain:"
msgid "PySON Domain"
-msgstr "PySON Domain"
+msgstr "Domini PySON"
msgctxt "field:ir.action.act_window,pyson_order:"
msgid "PySON Order"
@@ -486,7 +495,7 @@ msgstr "Ordre PySON"
msgctxt "field:ir.action.act_window,pyson_search_value:"
msgid "PySON Search Criteria"
-msgstr "PySON Search Criteria"
+msgstr "Criteri de cerca PySON"
msgctxt "field:ir.action.act_window,rec_name:"
msgid "Name"
@@ -666,7 +675,7 @@ msgstr "Direcció impressió"
msgctxt "field:ir.action.report,email:"
msgid "Email"
-msgstr "Email"
+msgstr "Correu electrònic"
msgctxt "field:ir.action.report,extension:"
msgid "Extension"
@@ -702,7 +711,7 @@ msgstr "Nom"
msgctxt "field:ir.action.report,pyson_email:"
msgid "PySON Email"
-msgstr "PySON Email"
+msgstr "Correu electrònic PySON"
msgctxt "field:ir.action.report,rec_name:"
msgid "Name"
@@ -720,6 +729,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Contingut"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr "Nom del contingut"
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Nom intern"
@@ -830,7 +843,7 @@ msgstr "Usuari creació"
msgctxt "field:ir.action.wizard,email:"
msgid "Email"
-msgstr "Email"
+msgstr "Correu electrònic"
msgctxt "field:ir.action.wizard,groups:"
msgid "Groups"
@@ -874,7 +887,7 @@ msgstr "Finestra"
msgctxt "field:ir.action.wizard,wiz_name:"
msgid "Wizard name"
-msgstr "Nom del assistent"
+msgstr "Nom de l'assistent"
msgctxt "field:ir.action.wizard,write_date:"
msgid "Write Date"
@@ -886,7 +899,7 @@ msgstr "Usuari modificació"
msgctxt "field:ir.attachment,collision:"
msgid "Collision"
-msgstr "Col·lissió"
+msgstr "Col·lisió"
msgctxt "field:ir.attachment,create_date:"
msgid "Create Date"
@@ -978,7 +991,7 @@ msgstr "Nom"
msgctxt "field:ir.cache,timestamp:"
msgid "Timestamp"
-msgstr "Timestamp"
+msgstr "Data-hora"
msgctxt "field:ir.cache,write_date:"
msgid "Write Date"
@@ -1042,7 +1055,7 @@ msgstr "ID"
msgctxt "field:ir.cron,interval_number:"
msgid "Interval Number"
-msgstr "Número d'intèrval"
+msgstr "Número d'interval"
msgctxt "field:ir.cron,interval_type:"
msgid "Interval Unit"
@@ -1058,7 +1071,7 @@ msgstr "Nom"
msgctxt "field:ir.cron,next_call:"
msgid "Next Call"
-msgstr "Següent execussió"
+msgstr "Següent execució"
msgctxt "field:ir.cron,number_calls:"
msgid "Number of Calls"
@@ -1074,11 +1087,11 @@ msgstr "Repeteix perduts"
msgctxt "field:ir.cron,request_user:"
msgid "Request User"
-msgstr "Sol·licitud usuari"
+msgstr "Usuari sol·licitant"
msgctxt "field:ir.cron,user:"
msgid "Execution User"
-msgstr "Usuari execussió"
+msgstr "Usuari execució"
msgctxt "field:ir.cron,write_date:"
msgid "Write Date"
@@ -1210,7 +1223,7 @@ msgstr "Separador de milers"
msgctxt "field:ir.lang,translatable:"
msgid "Translatable"
-msgstr "Traduible"
+msgstr "Traduïble"
msgctxt "field:ir.lang,write_date:"
msgid "Write Date"
@@ -1580,6 +1593,10 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr "Usuari modificació"
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr "ID"
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr "ID"
@@ -1834,7 +1851,7 @@ msgstr "ID"
msgctxt "field:ir.sequence,last_timestamp:"
msgid "Last Timestamp"
-msgstr "Últim Timestamp"
+msgstr "Última data-hora"
msgctxt "field:ir.sequence,name:"
msgid "Sequence Name"
@@ -1854,7 +1871,7 @@ msgstr "Següent número"
msgctxt "field:ir.sequence,padding:"
msgid "Number padding"
-msgstr "Omplenat del número"
+msgstr "Emplenat del número"
msgctxt "field:ir.sequence,prefix:"
msgid "Prefix"
@@ -1866,15 +1883,15 @@ msgstr "Nom"
msgctxt "field:ir.sequence,suffix:"
msgid "Suffix"
-msgstr "Suffix"
+msgstr "Sufix"
msgctxt "field:ir.sequence,timestamp_offset:"
msgid "Timestamp Offset"
-msgstr "Timestamp Offset"
+msgstr "Desfasament de data-hora"
msgctxt "field:ir.sequence,timestamp_rounding:"
msgid "Timestamp Rounding"
-msgstr "Arrondiment Tmestamp"
+msgstr "Arronodiment de data-hora"
msgctxt "field:ir.sequence,type:"
msgid "Type"
@@ -1910,7 +1927,7 @@ msgstr "ID"
msgctxt "field:ir.sequence.strict,last_timestamp:"
msgid "Last Timestamp"
-msgstr "Últim Timestamp"
+msgstr "Última data-hora"
msgctxt "field:ir.sequence.strict,name:"
msgid "Sequence Name"
@@ -1930,7 +1947,7 @@ msgstr "Següent número"
msgctxt "field:ir.sequence.strict,padding:"
msgid "Number padding"
-msgstr "Omplenat del número"
+msgstr "Emplenat del número"
msgctxt "field:ir.sequence.strict,prefix:"
msgid "Prefix"
@@ -1942,15 +1959,15 @@ msgstr "Nom"
msgctxt "field:ir.sequence.strict,suffix:"
msgid "Suffix"
-msgstr "Suffix"
+msgstr "Sufix"
msgctxt "field:ir.sequence.strict,timestamp_offset:"
msgid "Timestamp Offset"
-msgstr "Timestamp Offset"
+msgstr "Desfasament de data-hora"
msgctxt "field:ir.sequence.strict,timestamp_rounding:"
msgid "Timestamp Rounding"
-msgstr "Arrondiment Tmestamp"
+msgstr "Arronodiment de data-hora"
msgctxt "field:ir.sequence.strict,type:"
msgid "Type"
@@ -2082,7 +2099,7 @@ msgstr "Mòdul"
msgctxt "field:ir.translation,name:"
msgid "Field Name"
-msgstr "Nom del camp"
+msgstr "Nom"
msgctxt "field:ir.translation,overriding_module:"
msgid "Overriding Module"
@@ -2326,7 +2343,7 @@ msgstr "Fills"
msgctxt "field:ir.ui.menu,complete_name:"
msgid "Complete Name"
-msgstr "Nom compet"
+msgstr "Nom complet"
msgctxt "field:ir.ui.menu,create_date:"
msgid "Create Date"
@@ -2442,7 +2459,7 @@ msgstr "ID"
msgctxt "field:ir.ui.view,inherit:"
msgid "Inherited View"
-msgstr "Vista heredada"
+msgstr "Vista heretada"
msgctxt "field:ir.ui.view,model:"
msgid "Model"
@@ -2608,21 +2625,17 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr "Usuari modificació"
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Afegeix un actualizació a la vista automatitzada"
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
-msgstr "Limit per defecte en les vistes de llista"
+msgstr "Límit per defecte en les vistes de llista."
msgctxt "help:ir.action.act_window,search_value:"
msgid "Default search criteria for the list view"
-msgstr "Cerca per defecte en les vistes de llista"
+msgstr "Criteri de cerca per defecte en les vistes de llista."
msgctxt "help:ir.action.act_window,window_name:"
msgid "Use the action name as window name"
-msgstr "Utilitza el nom de l'acció pel nom de la finestra"
+msgstr "Utilitza el nom de l'acció pel nom de la finestra."
msgctxt "help:ir.action.report,email:"
msgid ""
@@ -2638,27 +2651,27 @@ msgid ""
"compatible format"
msgstr ""
"Deixeu-ho en blanc per mantenir el mateix que a la plantilla, veieu la "
-"documentació de l'unoconv per conèixer formats compatibles"
+"documentació de l'unoconv per conèixer formats compatibles."
msgctxt "help:ir.action.report,style:"
msgid "Define the style to apply on the report."
-msgstr "Defineix un esti per aplicar en els informes."
+msgstr "Defineix un estil per aplicar als informes."
msgctxt "help:ir.action.wizard,window:"
msgid "Run wizard in a new window"
-msgstr "Executa l'assistent en una nova finestra"
+msgstr "Executa l'assistent en una nova finestra."
msgctxt "help:ir.cron,number_calls:"
msgid ""
"Number of times the function is called, a negative number indicates that the"
" function will always be called"
msgstr ""
-"Número d'execussions de la funció, un número negatiu indica que aqusta "
-"funció sempre s'executarà."
+"Número d'execucions de la funció, un número negatiu indica que aquesta "
+"funció s'executarà sempre."
msgctxt "help:ir.cron,request_user:"
msgid "The user who will receive requests in case of failure"
-msgstr "L'usuari que rebrà solicituds en cas d'errors."
+msgstr "L'usuari que rebrà sol·licituds en cas d'errors."
msgctxt "help:ir.cron,user:"
msgid "The user used to execute this action"
@@ -2674,15 +2687,15 @@ msgstr "Mòdul en què es defineix aquest model."
msgctxt "help:ir.model.data,db_id:"
msgid "The id of the record in the database."
-msgstr "El id del registre de la base de dades."
+msgstr "L'id del registre de la base de dades."
msgctxt "help:ir.model.data,fs_id:"
msgid "The id of the record as known on the file system."
-msgstr "El id del registre que es conneix en el sistema de fitxers."
+msgstr "L'id del registre com es coneix al sistema de fitxers."
msgctxt "help:ir.model.field,module:"
msgid "Module in which this field is defined"
-msgstr "Mòdul en el qual es defineix aquest camp"
+msgstr "Mòdul en el qual es defineix aquest camp."
msgctxt "help:ir.model.print_model_graph.start,filter:"
msgid ""
@@ -2692,11 +2705,11 @@ msgstr "Una expressió regular de Python per excloure models de les gràfiques."
msgctxt "help:ir.rule,domain:"
msgid "Domain is evaluated with \"user\" as the current user"
-msgstr "El domini es evaluat amb el usuari \"user\" que es el usuari actual."
+msgstr "El domini s'avalua amb \"user\" com l'usuari actual."
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
-msgstr "Afegeix aquesta regla a tots els usuaris per defecte"
+msgstr "Afegeix aquesta regla a tots els usuaris per defecte."
msgctxt "help:ir.rule.group,global_p:"
msgid ""
@@ -2704,18 +2717,18 @@ msgid ""
"so every users must follow this rule"
msgstr ""
"Marcar la regla global \n"
-"per tant els usuari podràn seguir aquesta regla"
+"per tant tots els usuari hauran de seguir aquesta regla."
msgctxt "help:ir.rule.group,rules:"
msgid "The rule is satisfied if at least one test is True"
-msgstr "La regla es correcta si l'últim test és cert."
+msgstr "La regla es compleix si almenys un test és cert."
msgctxt "help:ir.trigger,condition:"
msgid ""
"A Python statement evaluated with record represented by \"self\"\n"
"It triggers the action if true."
msgstr ""
-"Una declaració de Python evaluat amb el registre representat per \"self\".\n"
+"Una declaració de Python avaluat amb el registre representat per \"self\".\n"
"Llença l'acció si la condició es certa."
msgctxt "help:ir.trigger,limit_number:"
@@ -2731,12 +2744,12 @@ msgid ""
"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
"0 for no delay."
msgstr ""
-"Intérval mínim en minuts entre les crides \"Acció de funció\" pel mateix registre.\n"
-"0 per no intérval."
+"Interval mínim en minuts entre les crides \"Acció de funció\" pel mateix registre.\n"
+"0 per no interval."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
-msgstr "El domini PYSON"
+msgstr "El domini PYSON."
msgctxt "model:ir.action,name:"
msgid "Action"
@@ -2800,7 +2813,7 @@ msgstr "Accés model"
msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
-msgstr "Butons"
+msgstr "Botons"
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
@@ -2972,7 +2985,7 @@ msgstr "Espanyol (Argentina)"
msgctxt "model:ir.lang,name:lang_bg"
msgid "Bulgarian"
-msgstr "Bulgaria"
+msgstr "Búlgar"
msgctxt "model:ir.lang,name:lang_ca"
msgid "Català"
@@ -2980,7 +2993,7 @@ msgstr "Català"
msgctxt "model:ir.lang,name:lang_cs"
msgid "Czech"
-msgstr "Check"
+msgstr "Txec"
msgctxt "model:ir.lang,name:lang_de"
msgid "German"
@@ -2996,7 +3009,7 @@ msgstr "Espanyol (Espanya)"
msgctxt "model:ir.lang,name:lang_es_CO"
msgid "Spanish (Colombia)"
-msgstr "Espanyol (Colombia)"
+msgstr "Espanyol (Colòmbia)"
msgctxt "model:ir.lang,name:lang_fr"
msgid "French"
@@ -3028,7 +3041,7 @@ msgstr "Accés model"
msgctxt "model:ir.model.button,name:"
msgid "Model Button"
-msgstr "Butó model"
+msgstr "Botó model"
msgctxt "model:ir.model.data,name:"
msgid "Model data"
@@ -3050,6 +3063,10 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr "Mòdul"
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr "Assistent de configuració del modul finalitzat"
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
msgstr "Inicialització de l'assistent de configuració del mòdul"
@@ -3060,7 +3077,7 @@ msgstr "Assistent configuració després d'instal·lar un mòdul"
msgctxt "model:ir.module.module.config_wizard.other,name:"
msgid "Module Config Wizard Other"
-msgstr "Un altre asistent de configuració del mòdul"
+msgstr "Un altre assistent de configuració del mòdul"
msgctxt "model:ir.module.module.dependency,name:"
msgid "Module dependency"
@@ -3076,7 +3093,7 @@ msgstr "Inici instal·lació/actualització mòdul"
msgctxt "model:ir.property,name:"
msgid "Property"
-msgstr "Property"
+msgstr "Propietat"
msgctxt "model:ir.rule,name:"
msgid "Rule"
@@ -3128,11 +3145,11 @@ msgstr "Exporta traducció"
msgctxt "model:ir.translation.set.start,name:"
msgid "Set Translation"
-msgstr "Estableix traduccions"
+msgstr "Defineix traduccions"
msgctxt "model:ir.translation.set.succeed,name:"
msgid "Set Translation"
-msgstr "Estableix traduccions"
+msgstr "Defineix traduccions"
msgctxt "model:ir.translation.update.start,name:"
msgid "Update translation"
@@ -3224,7 +3241,7 @@ msgstr "Accés model"
msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
-msgstr "Butons"
+msgstr "Botons"
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
@@ -3292,7 +3309,7 @@ msgstr "Traduccions"
msgctxt "model:ir.ui.menu,name:menu_translation_set"
msgid "Set Translations"
-msgstr "Estableix traduccions"
+msgstr "Defineix traduccions"
msgctxt "model:ir.ui.menu,name:menu_translation_update"
msgid "Synchronize Translations"
@@ -3436,7 +3453,7 @@ msgstr "Realitzat"
msgctxt "selection:ir.module.module.config_wizard.item,state:"
msgid "Open"
-msgstr "Per obrir"
+msgstr "Pendent"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "Installed"
@@ -3464,11 +3481,11 @@ msgstr "No es coneix"
msgctxt "selection:ir.sequence,type:"
msgid "Decimal Timestamp"
-msgstr "Decimal Tiemstamp"
+msgstr "Data-hora decimal"
msgctxt "selection:ir.sequence,type:"
msgid "Hexadecimal Timestamp"
-msgstr "Marca de temps hexadecimal"
+msgstr "Data-hora hexadecimal"
msgctxt "selection:ir.sequence,type:"
msgid "Incremental"
@@ -3476,11 +3493,11 @@ msgstr "Increment"
msgctxt "selection:ir.sequence.strict,type:"
msgid "Decimal Timestamp"
-msgstr "Decimal Tiemstamp"
+msgstr "Data-hora decimal"
msgctxt "selection:ir.sequence.strict,type:"
msgid "Hexadecimal Timestamp"
-msgstr "Marca de temps hexadecimal"
+msgstr "Data-hora hexadecimal"
msgctxt "selection:ir.sequence.strict,type:"
msgid "Incremental"
@@ -3516,7 +3533,7 @@ msgstr "Vista"
msgctxt "selection:ir.translation,type:"
msgid "Wizard Button"
-msgstr "Butó del assistent"
+msgstr "Botó de l'assistent"
msgctxt "selection:ir.ui.menu,action:"
msgid ""
@@ -3656,7 +3673,7 @@ msgstr "Exportacions"
msgctxt "view:ir.lang:"
msgid "Date Formatting"
-msgstr "Formateig data"
+msgstr "Format data"
msgctxt "view:ir.lang:"
msgid "Language"
@@ -3676,11 +3693,11 @@ msgstr "Control accés"
msgctxt "view:ir.model.button:"
msgid "Button"
-msgstr "Butons"
+msgstr "Botons"
msgctxt "view:ir.model.button:"
msgid "Buttons"
-msgstr "Butons"
+msgstr "Botons"
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
@@ -3702,6 +3719,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Descripció model"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr "Configuració del mòdul"
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr "S'ha realitzat la configuració correctament."
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "Benvingut a la configuració del mòdul."
@@ -3788,14 +3813,13 @@ msgstr "Propietats"
msgctxt "view:ir.property:"
msgid "Property"
-msgstr "Property"
+msgstr "Propietat"
msgctxt "view:ir.rule.group:"
msgid ""
"If there is no test defined, the rule is always satisfied if not global"
msgstr ""
-"Si no hi ha un test definit, la regla es sempre satisfactoria si no és "
-"global."
+"Si no hi ha un test definit, la regla es compleix sempre si no és global."
msgctxt "view:ir.rule.group:"
msgid "Record rules"
@@ -3803,7 +3827,7 @@ msgstr "Regles dels registres"
msgctxt "view:ir.rule.group:"
msgid "The rule is satisfied if at least one test is True"
-msgstr "La regla es correcta si l'últim test és cert."
+msgstr "La regla es compleix si almenys un test és cert."
msgctxt "view:ir.rule:"
msgid "Test"
@@ -3847,7 +3871,7 @@ msgstr "Seqüències"
msgctxt "view:ir.sequence:"
msgid "Timestamp"
-msgstr "Timestamp"
+msgstr "Data-hora"
msgctxt "view:ir.sequence:"
msgid "Year:"
@@ -3879,7 +3903,7 @@ msgstr "Exporta traducció"
msgctxt "view:ir.translation.set.start:"
msgid "Set Translations"
-msgstr "Estableix traduccions"
+msgstr "Defineix traduccions"
msgctxt "view:ir.translation.set.start:"
msgid "Synchronize Translations?"
@@ -3891,7 +3915,7 @@ msgstr "Les traduccions s'han realitzat correctament."
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Translations"
-msgstr "Estableix traduccions"
+msgstr "Defineix traduccions"
msgctxt "view:ir.translation.update.start:"
msgid "Synchronize Translations"
@@ -3969,9 +3993,13 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr "Imprimir"
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "D'acord"
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
-msgstr "Dacord"
+msgstr "D'acord"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
msgid "Cancel"
@@ -3987,7 +4015,7 @@ msgstr "Cancel·la"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
msgid "Ok"
-msgstr "Dacord"
+msgstr "D'acord"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
msgid "Cancel"
@@ -4007,7 +4035,7 @@ msgstr "Cancel·la"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
msgid "Ok"
-msgstr "Dacord"
+msgstr "D'acord"
msgctxt "wizard_button:ir.translation.export,result,end:"
msgid "Close"
@@ -4027,7 +4055,7 @@ msgstr "Cancel·la"
msgctxt "wizard_button:ir.translation.set,start,set_:"
msgid "Set"
-msgstr "Estableix"
+msgstr "Defineix"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
msgid "Ok"
diff --git a/trytond/ir/locale/cs_CZ.po b/trytond/ir/locale/cs_CZ.po
index 315e563..78ce3a0 100644
--- a/trytond/ir/locale/cs_CZ.po
+++ b/trytond/ir/locale/cs_CZ.po
@@ -19,19 +19,25 @@ msgid "You are not allowed to delete this record."
msgstr ""
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
msgstr ""
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
msgstr ""
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
msgstr ""
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
msgstr ""
msgctxt "error:ir.action.act_window:"
@@ -239,10 +245,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr ""
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr ""
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -270,11 +272,11 @@ msgid "Relation not found: %r in %s"
msgstr ""
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
msgstr ""
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
msgstr ""
msgctxt "error:search_function_missing:"
@@ -282,7 +284,9 @@ msgid "Missing search function on field \"%s\"."
msgstr ""
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
msgstr ""
msgctxt "error:selection_value_notfound:"
@@ -290,11 +294,11 @@ msgid "Value not in the selection for field \"%s\"."
msgstr ""
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
msgstr ""
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
msgstr ""
msgctxt "error:too_many_relations_found:"
@@ -393,10 +397,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr ""
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr ""
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr ""
@@ -693,6 +693,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr ""
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr ""
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr ""
@@ -1553,6 +1557,10 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr ""
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr ""
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr ""
@@ -2581,10 +2589,6 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr ""
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr ""
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr ""
@@ -3009,6 +3013,10 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr ""
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr ""
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
msgstr ""
@@ -3661,6 +3669,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr ""
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr ""
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr ""
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr ""
@@ -3924,6 +3940,10 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr ""
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr ""
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr ""
diff --git a/trytond/ir/locale/de_DE.po b/trytond/ir/locale/de_DE.po
index f6a900a..22986fe 100644
--- a/trytond/ir/locale/de_DE.po
+++ b/trytond/ir/locale/de_DE.po
@@ -23,30 +23,32 @@ msgid "You are not allowed to delete this record."
msgstr "Keine Löschberechtigung für diesen Datensatz"
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "Das Feld \"%s\" in \"%s\" weist zu viele Dezimalstellen auf."
-
-msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
msgstr ""
-"Der Wert von Feld \"%s\" in \"%s\" liegt nicht im gültigen Wertebereich "
-"(Domain)!"
+"Die Anzahl der Nachkommastellen \"%(digits)s\" in Feld \"%(field)s\" in "
+"\"%(value)s\" überschreitet die erlaubte Größe."
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid with his domain."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
msgstr ""
-"Der Wert von Feld \"%s\" in \"%s\" liegt nicht im gültigen Wertebereich "
-"(Domain)!"
+"Der Wert von Feld \"%(field)s\" in \"%(model)s\" liegt nicht im gültigen "
+"Wertebereich (Domain)."
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
msgstr ""
-"\"%s\" Datensätze konnten nicht gelöscht werden, weil sie in Feld \"%s\" in "
-"\"%s\" verwendet werden."
+"Die Datensätze konnten nicht gelöscht werden, da sie in Feld \"%(field)s\" "
+"von \"%(model)s\" verwendet werden."
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "Der Wert von Feld \"%s\" in \"%s\" existiert nicht."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr "Der Wert \"%(value)s\" für Feld \"%(field)s\" in \"%(model)s\" existiert nicht."
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -275,10 +277,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr "Ungültige XML-Daten für Sicht \"%s\"."
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "Schlüsselwert %r nicht gefunden in Auswahlfeld %r"
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -312,32 +310,38 @@ msgid "Relation not found: %r in %s"
msgstr "Beziehung nicht gefunden: %r in %s"
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Feld \"%s\" in \"%s\" ist erforderlich."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "Feld \"%(field)s\" in \"%(model)s\" ist erforderlich."
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Feld \"%s\" in \"%s\" ist erforderlich."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "Feld \"%(field)s\" in \"%(model)s\" ist erforderlich."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
msgstr "Fehlende Suchfunktion für Feld \"%s\"."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
-msgstr "Feld \"%s\" in \"%s\" ist nicht in der Auswahl."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
+msgstr ""
+"Der Wert \"%(value)s\" von Feld \"%(field)s\" in \"%(model)s\" ist nicht in "
+"der Auswahl enthalten."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
msgstr "Wert nicht in der Auswahl von Feld \"%s\"."
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "Der Wert von Feld \"%s\" in \"%s\" ist zu lang."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr "Die Länge \"%(size)s\" für Feld \"%(field)s\" in \"%(model)s\" ist zu lang."
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
-msgstr "Der Wert der Zeitangabe in Feld \"%s\" in \"%s\" ist nicht gültig"
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
+msgstr ""
+"Der Wert der Zeit \"%(value)s\" für Feld \"%(field)s\" in \"%(model)s\" ist "
+"ungültig."
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
@@ -439,10 +443,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Aktiv"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Automatisches Auffrischen"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr "Kontext"
@@ -739,6 +739,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Inhalt"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr "Inhalt Name"
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Interner Name"
@@ -1599,6 +1603,10 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr "Letzte Änderung durch"
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr "ID"
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr "ID"
@@ -2165,7 +2173,7 @@ msgstr "Sprache"
msgctxt "field:ir.translation.export.start,module:"
msgid "Module"
-msgstr "Module"
+msgstr "Modul"
msgctxt "field:ir.translation.set.start,id:"
msgid "ID"
@@ -2627,10 +2635,6 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr "Letzte Änderung durch"
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Die Sicht wird automatisch aufgefrischt"
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr "Standardobergrenze für die Listenanscht"
@@ -3072,7 +3076,11 @@ msgstr "Modellgraph drucken"
msgctxt "model:ir.module.module,name:"
msgid "Module"
-msgstr "Module"
+msgstr "Modul"
+
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr "Modulkonfigurationswizard Erledigt"
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
@@ -3726,6 +3734,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Modell Beschreibung"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr "Modulkonfiguration"
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr "Die Konfiguration ist beendet."
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "Willkommen bei der Modulkonfiguration!"
@@ -4025,6 +4041,10 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr "Drucken"
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "OK"
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr "OK"
diff --git a/trytond/ir/locale/es_AR.po b/trytond/ir/locale/es_AR.po
index 3054aad..d727c5e 100644
--- a/trytond/ir/locale/es_AR.po
+++ b/trytond/ir/locale/es_AR.po
@@ -23,22 +23,31 @@ msgid "You are not allowed to delete this record."
msgstr "No está autorizado a borrar este registro."
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "El campo «%s» en «%s» tiene demasiados dígitos decimales."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
+msgstr ""
+"El número de decimales «%(digits)s» del campo «%(field)s» en «%(value)s» "
+"excede su límite."
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
-msgstr "El valor del campo «%s» en «%s» no es válido según su dominio."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
+msgstr ""
+"El valor del campo «%(field)s» en «%(model)s» no es válido según su dominio."
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
msgstr ""
-"No se pudieron borrar «%s» registros porque se usan en el campo «%s» de "
-"«%s»."
+"No se pueden eliminar los registros porque son usados en el campo "
+"«%(field)s» de «%(model)s»."
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "El valor del campo «%s» en «%s» no existe."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr "El valor «%(value)s» del campo «%(field)s» en «%(model)s» no existe."
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -263,10 +272,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr "XML de la vista «%s» no es correcto."
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "Clave %r no encontrada en campo de selección %r"
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -300,32 +305,40 @@ msgid "Relation not found: %r in %s"
msgstr "Relación no encontrada: %r en %s"
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "El campo «%s» en «%s» es requerido."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "El campo «%(field)s» en «%(model)s» es requerido."
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "El campo «%s» en «%s» es requerido."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "El campo «%(field)s» en «%(model)s» es requerido."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
msgstr "Falta la función de búsqueda del campo «%s»."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
-msgstr "El campo «%s» en «%s» no se encuentra en la selección."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
+msgstr ""
+"El valor «%(value)s» del campo «%(field)s» en «%(model)s» no está en la "
+"selección."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
msgstr "El valor no se encuentra en la selección para el campo «%s»."
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "El campo «%s» en «%s» es muy largo."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr ""
+"El tamaño «%(size)s» del campo «%(field)s» en «%(model)s» es demasiado "
+"largo."
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
-msgstr "El valor de tiempo del campo «%s» en «%s» no es válido."
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
+msgstr ""
+"El valor de tiempo «%(value)s» del campo «%(field)s» en «%(model)s» no es "
+"válido."
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
@@ -427,10 +440,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Activo"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Auto-Recargar"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr "Valor del contexto"
@@ -473,7 +482,7 @@ msgstr "Límite"
msgctxt "field:ir.action.act_window,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.action.act_window,order:"
msgid "Order Value"
@@ -557,11 +566,11 @@ msgstr "ID"
msgctxt "field:ir.action.act_window.domain,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.action.act_window.domain,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.action.act_window.domain,sequence:"
msgid "Sequence"
@@ -705,7 +714,7 @@ msgstr "Módulo"
msgctxt "field:ir.action.report,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.action.report,pyson_email:"
msgid "PySON Email"
@@ -727,6 +736,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Contenido"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr "Nombre del contenido"
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Nombre interno"
@@ -793,7 +806,7 @@ msgstr "Palabras clave"
msgctxt "field:ir.action.url,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.action.url,rec_name:"
msgid "Name"
@@ -861,7 +874,7 @@ msgstr "Modelo"
msgctxt "field:ir.action.wizard,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.action.wizard,rec_name:"
msgid "Name"
@@ -1013,7 +1026,7 @@ msgstr "idioma"
msgctxt "field:ir.configuration,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.configuration,write_date:"
msgid "Write Date"
@@ -1349,11 +1362,11 @@ msgstr "Modelo"
msgctxt "field:ir.model.button,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.model.button,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.model.button,write_date:"
msgid "Write Date"
@@ -1517,7 +1530,7 @@ msgstr "Acceso para escribir"
msgctxt "field:ir.model.field.access,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.model.field.access,write_date:"
msgid "Write Date"
@@ -1587,6 +1600,10 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr "Usuario modificación"
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr "ID"
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr "ID"
@@ -2021,7 +2038,7 @@ msgstr "Clave"
msgctxt "field:ir.session,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.session,write_date:"
msgid "Write Date"
@@ -2049,7 +2066,7 @@ msgstr "ID"
msgctxt "field:ir.session.wizard,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.session.wizard,write_date:"
msgid "Write Date"
@@ -2213,7 +2230,7 @@ msgstr "Modelo"
msgctxt "field:ir.trigger,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.trigger,on_create:"
msgid "On Create"
@@ -2233,7 +2250,7 @@ msgstr "Al Escribir"
msgctxt "field:ir.trigger,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.trigger,write_date:"
msgid "Write Date"
@@ -2257,7 +2274,7 @@ msgstr "ID"
msgctxt "field:ir.trigger.log,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.trigger.log,record_id:"
msgid "Record ID"
@@ -2297,7 +2314,7 @@ msgstr "Módulo"
msgctxt "field:ir.ui.icon,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.icon,path:"
msgid "SVG Path"
@@ -2305,7 +2322,7 @@ msgstr "Ruta al SVG"
msgctxt "field:ir.ui.icon,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.icon,sequence:"
msgid "Sequence"
@@ -2401,7 +2418,7 @@ msgstr "Menú"
msgctxt "field:ir.ui.menu.favorite,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.menu.favorite,sequence:"
msgid "Sequence"
@@ -2461,7 +2478,7 @@ msgstr "Módulo"
msgctxt "field:ir.ui.view,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.view,priority:"
msgid "Priority"
@@ -2509,11 +2526,11 @@ msgstr "Modelo"
msgctxt "field:ir.ui.view_search,name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.view_search,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.view_search,user:"
msgid "User"
@@ -2615,10 +2632,6 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr "Usuario modificación"
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Añade un refresco automático a la vista"
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr "Límite predeterminado para la vista en lista"
@@ -3059,6 +3072,10 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr "Módulo"
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr "Asistente de configuración de módulo - Finalizado"
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
msgstr "Asistente de configuración inicial del módulo"
@@ -3712,6 +3729,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Descripción del modelo"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr "Configuración de módulo"
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr "La configuración ha finalizado."
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "¡Bienvenido al asistente de configuración del módulo!"
@@ -4010,6 +4035,10 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr "Imprimir"
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "Aceptar"
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr "Aceptar"
diff --git a/trytond/ir/locale/es_CO.po b/trytond/ir/locale/es_CO.po
index 299c7d8..01c59ac 100644
--- a/trytond/ir/locale/es_CO.po
+++ b/trytond/ir/locale/es_CO.po
@@ -7,7 +7,7 @@ msgid ""
"You try to bypass an access rule!\n"
"(Document type: %s)"
msgstr ""
-"Está intentando evitar una regla de acceso\n"
+"Está tratando de saltarse una regla de acceso!\n"
"(Tipo de documento: %s)"
msgctxt "error:access_error:"
@@ -21,22 +21,34 @@ msgid "You are not allowed to delete this record."
msgstr "No está autorizado a borrar este registro."
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "El campo \"%s\" en \"%s\" tiene demasiados dígitos decimales."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
+msgstr ""
+"El numero de decimales \"%(digits)s\" del campo \"%(field)s\" en el valor "
+"\"%(value)s\" excede el limite."
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
-msgstr "El valor del campo «%s» en «%s» no es válido según su dominio."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
+msgstr ""
+"El valor del campo \"%(field)s\" en \"%(model)s\" no es válido según su "
+"dominio."
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
msgstr ""
-"No se pudieron borrar «%s» registros porque se usan en el campo «%s» de "
-"«%s»."
+"No se pudieron borrar los registros porque son usados en el campo "
+"\"%(field)s\" de \"%(model)s\"."
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "El valor del campo «%s» en «%s» no existe."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr ""
+"El valor \"%(value)s\" en el campo \"%(field)s\" en el modelo \"%(model)s\" "
+"no existe."
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -50,15 +62,15 @@ msgstr ""
msgctxt "error:ir.action.act_window:"
msgid "Invalid view \"%(view)s\" for action \"%(action)s\"."
-msgstr "Vista inválida \"%(view)s\" para la acción \"%(action)s\""
+msgstr "Vista \"%(view)s\" inválida para la acción \"%(action)s\"."
msgctxt "error:ir.action.keyword:"
msgid "Wrong wizard model in keyword action \"%s\"."
-msgstr "Errado modelo de asistente en la acción clave \"%s\"."
+msgstr "Errado modelo asistente para acción la \"%s\"."
msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
-msgstr "Definición de correo electronico inválida en informe \"%s\""
+msgstr "El correo electronico definido en el informe \"%s\" es inválido."
msgctxt "error:ir.action.report:"
msgid "The internal name must be unique by module!"
@@ -92,7 +104,7 @@ msgstr "Los lenguajes por defecto no pueden ser borrados."
msgctxt "error:ir.lang:"
msgid "Invalid date format \"%(format)s\" on \"%(language)s\" language."
-msgstr "Formato de fecha inválido \"%(format)s\" en lenguaje \"%(languages)s\"."
+msgstr "Formato de fecha inválido \"%(format)s\" para el lenguaje \"%(languages)s\"."
msgctxt "error:ir.lang:"
msgid "Invalid grouping \"%(grouping)s\" on \"%(language)s\" language."
@@ -100,11 +112,11 @@ msgstr "Agrupamiento inválido \"%(grouping)s\" en lenguaje \"%(languages)s\"."
msgctxt "error:ir.lang:"
msgid "The default language must be translatable."
-msgstr "El lenguaje por defecto debe ser traducible."
+msgstr "El idioma por defecto debe ser traducible."
msgctxt "error:ir.lang:"
msgid "decimal_point and thousands_sep must be different!"
-msgstr "«decimal_point» y «thousands_sep» deben ser distintos"
+msgstr "decimal_point y «thousands_sep» deben ser distintos"
msgctxt "error:ir.model.access:"
msgid "You can not create this kind of document! (%s)"
@@ -124,15 +136,15 @@ msgstr "No puede escribir en este documento (%s)"
msgctxt "error:ir.model.button:"
msgid "The button name in model must be unique!"
-msgstr "¡El nombre del botón en el modelo debe ser único!"
+msgstr "El nombre del botón en el modelo debe ser único!"
msgctxt "error:ir.model.data:"
msgid "The triple (fs_id, module, model) must be unique!"
-msgstr "¡La terna (fs_id, module, model) debe ser única!"
+msgstr "La terna (fs_id, modulo, modelo) debe ser única!"
msgctxt "error:ir.model.field.access:"
msgid "You can not read the field! (%s.%s)"
-msgstr "¡No puede leer el campo! (%s.%s)"
+msgstr "No puede leer el campo! (%s.%s)"
msgctxt "error:ir.model.field.access:"
msgid "You can not write on the field! (%s.%s)"
@@ -140,8 +152,7 @@ msgstr "¡No puede escribir en el campo! (%s.%s)"
msgctxt "error:ir.model.field:"
msgid "Model Field name \"%s\" is not a valid python identifier."
-msgstr ""
-"El campo del Nombre del Modelo \"%s\" no es un identificador python válido."
+msgstr "El nombre del Modelo Campo \"%s\" no es un identificador python válido."
msgctxt "error:ir.model.field:"
msgid "The field name in model must be unique!"
@@ -161,7 +172,7 @@ msgstr "¡Las dependencias por módulo deben ser únicas!"
msgctxt "error:ir.module.module:"
msgid "Missing dependencies %s for module \"%s\""
-msgstr "Faltan las dependencias %s para el módulo «%s»"
+msgstr "Faltan las dependencias %s para el módulo \"%s\""
msgctxt "error:ir.module.module:"
msgid "The modules you are trying to uninstall depends on installed modules:"
@@ -179,7 +190,7 @@ msgstr ""
msgctxt "error:ir.rule.group:"
msgid "Global and Default are mutually exclusive!"
-msgstr "«Global» y «Predeterminado» son mutuamente excluyentes"
+msgstr "Global y Predeterminado son mutuamente excluyentes"
msgctxt "error:ir.rule:"
msgid "Invalid domain in rule \"%s\"."
@@ -196,11 +207,12 @@ msgstr "Sufijo inválido \"%(suffix)s\" en secuencia \"%(sequence)s\"."
msgctxt "error:ir.sequence.strict:"
msgid "Last Timestamp cannot be in the future on sequence \"%s\"."
msgstr ""
-"La ultima marca de tiempo no puede situarse en una secuencia futura \"%s\"."
+"La ultima marca de tiempo no puede estar en el futuro para la secuencia "
+"\"%s\"."
msgctxt "error:ir.sequence.strict:"
msgid "Missing sequence."
-msgstr "Falta la secuencia!"
+msgstr "Falta la secuencia."
msgctxt "error:ir.sequence.strict:"
msgid "Timestamp rounding should be greater than 0"
@@ -217,11 +229,12 @@ msgstr "Sufijo inválido \"%(suffix)s\" en secuencia \"%(sequence)s\"."
msgctxt "error:ir.sequence:"
msgid "Last Timestamp cannot be in the future on sequence \"%s\"."
msgstr ""
-"La ultima marca de tiempo no puede situarse en una secuencia futura \"%s\"."
+"La ultima marca de tiempo no puede estar en el futuro para la secuencia "
+"\"%s\"."
msgctxt "error:ir.sequence:"
msgid "Missing sequence."
-msgstr "Falta la secuencia!"
+msgstr "Falta la secuencia."
msgctxt "error:ir.sequence:"
msgid "Timestamp rounding should be greater than 0"
@@ -241,7 +254,7 @@ msgstr ""
msgctxt "error:ir.trigger:"
msgid "\"On Time\" and others are mutually exclusive!"
-msgstr "¡\"Al Tiempo\" y otros son mutuamente excluyentes!"
+msgstr "\"Al Tiempo\" y otros son mutuamente excluyentes!"
msgctxt "error:ir.trigger:"
msgid ""
@@ -259,10 +272,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr "XML inválido para la vista \"%s\"."
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "Clave %r no encontrada en campo de selección %r"
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -294,32 +303,40 @@ msgid "Relation not found: %r in %s"
msgstr "Relación no encontrada: %r en %s"
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "El campo «%s» en «%s» es necesario."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "El campo \"%(field)s\" en el modelo \"%(model)s\" es obligatorio."
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "El campo «%s» en «%s» es necesario."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "El campo \"%(field)s\" en el modelo \"%(model)s\" es obligatorio."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
-msgstr "¡Falta función de búsqueda en el campo \"%s\"."
+msgstr "Falta la función en el campo \"%s\"."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
-msgstr "El campo \"%s\" en \"%s\" no esta en la selección."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
+msgstr ""
+"El valor \"%(value)s\" en el campo \"%(field)s\" en el modelo \"%(model)s\" "
+"no esta en la selección."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
-msgstr "El valor no esta en la selección para el campo \"%s\"."
+msgstr "El valor no esta en la selección para el campo \"%s\"."
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "El campo «%s» en «%s» es muy largo."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr ""
+"El tamaño \"%(size)s\" del campo \"%(field)s\" en el modelo \"%(model)s\" is"
+" demasiado largo."
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
-msgstr "El valor del tiempo en el campo \"%s\" en \"%s\" no es válido."
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
+msgstr ""
+"El valor del tiempo \"%(value)s\" del campo \"%(field)s\" en \"%(model)s\" "
+"no es válido."
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
@@ -337,7 +354,9 @@ msgctxt "error:write_error:"
msgid ""
"You try to write on records that don't exist anymore.\n"
"(Document type: %s)"
-msgstr "Esta intentando modificar registros que ya no existen."
+msgstr ""
+"Esta intentando modificar registros que ya no existen.\n"
+"(Tipo de documento : %s)"
msgctxt "error:write_xml_record:"
msgid "You are not allowed to modify this record."
@@ -377,7 +396,7 @@ msgstr "ID"
msgctxt "field:ir.action,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Teclas"
msgctxt "field:ir.action,name:"
msgid "Name"
@@ -419,13 +438,9 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Activo"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Auto-Recargar"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
-msgstr "Valor del contexto"
+msgstr "Valor del Contexto"
msgctxt "field:ir.action.act_window,create_date:"
msgid "Create Date"
@@ -437,7 +452,7 @@ msgstr "Creado por Usuario"
msgctxt "field:ir.action.act_window,domain:"
msgid "Domain Value"
-msgstr "Valor del dominio"
+msgstr "Valor del Dominio"
msgctxt "field:ir.action.act_window,domains:"
msgid "Domains"
@@ -457,7 +472,7 @@ msgstr "ID"
msgctxt "field:ir.action.act_window,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Teclas"
msgctxt "field:ir.action.act_window,limit:"
msgid "Limit"
@@ -465,7 +480,7 @@ msgstr "Límite"
msgctxt "field:ir.action.act_window,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.action.act_window,order:"
msgid "Order Value"
@@ -497,7 +512,7 @@ msgstr "Modelo"
msgctxt "field:ir.action.act_window,search_value:"
msgid "Search Criteria"
-msgstr "Criterio de búsqueda"
+msgstr "Criterio de Búsqueda"
msgctxt "field:ir.action.act_window,type:"
msgid "Type"
@@ -549,11 +564,11 @@ msgstr "ID"
msgctxt "field:ir.action.act_window.domain,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.action.act_window.domain,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.action.act_window.domain,sequence:"
msgid "Sequence"
@@ -625,7 +640,7 @@ msgstr "ID"
msgctxt "field:ir.action.keyword,keyword:"
msgid "Keyword"
-msgstr "Palabra clave"
+msgstr "Tecla"
msgctxt "field:ir.action.keyword,model:"
msgid "Model"
@@ -661,7 +676,7 @@ msgstr "Creado por Usuario"
msgctxt "field:ir.action.report,direct_print:"
msgid "Direct Print"
-msgstr "Impresión directa"
+msgstr "Impresión Directa"
msgctxt "field:ir.action.report,email:"
msgid "Email"
@@ -685,7 +700,7 @@ msgstr "ID"
msgctxt "field:ir.action.report,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Teclas"
msgctxt "field:ir.action.report,model:"
msgid "Model"
@@ -697,7 +712,7 @@ msgstr "Módulo"
msgctxt "field:ir.action.report,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.action.report,pyson_email:"
msgid "PySON Email"
@@ -719,6 +734,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Contenido"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr "Nombre del Contenido"
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Nombre Interno"
@@ -781,11 +800,11 @@ msgstr "ID"
msgctxt "field:ir.action.url,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Teclas"
msgctxt "field:ir.action.url,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.action.url,rec_name:"
msgid "Name"
@@ -845,7 +864,7 @@ msgstr "ID"
msgctxt "field:ir.action.wizard,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Teclas"
msgctxt "field:ir.action.wizard,model:"
msgid "Model"
@@ -853,7 +872,7 @@ msgstr "Modelo"
msgctxt "field:ir.action.wizard,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.action.wizard,rec_name:"
msgid "Name"
@@ -929,7 +948,7 @@ msgstr "Enlace"
msgctxt "field:ir.attachment,name:"
msgid "Name"
-msgstr "Nombre del adjunto"
+msgstr "Nombre"
msgctxt "field:ir.attachment,rec_name:"
msgid "Name"
@@ -1005,7 +1024,7 @@ msgstr "Lenguaje"
msgctxt "field:ir.configuration,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.configuration,write_date:"
msgid "Write Date"
@@ -1041,11 +1060,11 @@ msgstr "ID"
msgctxt "field:ir.cron,interval_number:"
msgid "Interval Number"
-msgstr "Número intervalo"
+msgstr "Número de Intervalo"
msgctxt "field:ir.cron,interval_type:"
msgid "Interval Unit"
-msgstr "Unidad intervalo"
+msgstr "Unidad Intervalo"
msgctxt "field:ir.cron,model:"
msgid "Model"
@@ -1057,7 +1076,7 @@ msgstr "Nombre"
msgctxt "field:ir.cron,next_call:"
msgid "Next Call"
-msgstr "Próxima Llamada"
+msgstr "Siguiente Llamada"
msgctxt "field:ir.cron,number_calls:"
msgid "Number of Calls"
@@ -1293,19 +1312,19 @@ msgstr "Modelo"
msgctxt "field:ir.model.access,perm_create:"
msgid "Create Access"
-msgstr "Acceso para crear"
+msgstr "Permiso para Crear"
msgctxt "field:ir.model.access,perm_delete:"
msgid "Delete Access"
-msgstr "Acceso para borrar"
+msgstr "Permiso para Borrar"
msgctxt "field:ir.model.access,perm_read:"
msgid "Read Access"
-msgstr "Acceso para leer"
+msgstr "Permiso para Leer"
msgctxt "field:ir.model.access,perm_write:"
msgid "Write Access"
-msgstr "Acceso para escribir"
+msgstr "Permiso para Modificar"
msgctxt "field:ir.model.access,rec_name:"
msgid "Name"
@@ -1341,11 +1360,11 @@ msgstr "Modelo"
msgctxt "field:ir.model.button,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.model.button,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.model.button,write_date:"
msgid "Write Date"
@@ -1365,11 +1384,11 @@ msgstr "Creado por Usuario"
msgctxt "field:ir.model.data,date_init:"
msgid "Init Date"
-msgstr "Fecha de inicio"
+msgstr "Fecha Inicial"
msgctxt "field:ir.model.data,date_update:"
msgid "Update Date"
-msgstr "Fecha de actualización"
+msgstr "Fecha de Actualización"
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
@@ -1377,7 +1396,7 @@ msgstr "ID del recurso"
msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
-msgstr "Identificador en el sistema de archivos"
+msgstr "Identificador en Sistema de Archivos"
msgctxt "field:ir.model.data,id:"
msgid "ID"
@@ -1421,7 +1440,7 @@ msgstr "Creado por Usuario"
msgctxt "field:ir.model.field,field_description:"
msgid "Field Description"
-msgstr "Descripción del campo"
+msgstr "Descripción del Campo"
msgctxt "field:ir.model.field,groups:"
msgid "Groups"
@@ -1453,7 +1472,7 @@ msgstr "Nombre"
msgctxt "field:ir.model.field,relation:"
msgid "Model Relation"
-msgstr "Relación del modelo"
+msgstr "Modelo Relación"
msgctxt "field:ir.model.field,ttype:"
msgid "Field Type"
@@ -1493,23 +1512,23 @@ msgstr "ID"
msgctxt "field:ir.model.field.access,perm_create:"
msgid "Create Access"
-msgstr "Acceso para Crear"
+msgstr "Permiso para Crear"
msgctxt "field:ir.model.field.access,perm_delete:"
msgid "Delete Access"
-msgstr "Acceso para Borrar"
+msgstr "Permiso para Borrar"
msgctxt "field:ir.model.field.access,perm_read:"
msgid "Read Access"
-msgstr "Acceso para Leer"
+msgstr "Permiso para Leer"
msgctxt "field:ir.model.field.access,perm_write:"
msgid "Write Access"
-msgstr "Acceso para Modificar"
+msgstr "Permiso para Modificar"
msgctxt "field:ir.model.field.access,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.model.field.access,write_date:"
msgid "Write Date"
@@ -1579,6 +1598,10 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr "Modificado por Usuario"
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr "ID"
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr "ID"
@@ -1777,19 +1800,19 @@ msgstr "Nombre"
msgctxt "field:ir.rule.group,perm_create:"
msgid "Create Access"
-msgstr "Acceso para crear"
+msgstr "Permiso para Crear"
msgctxt "field:ir.rule.group,perm_delete:"
msgid "Delete Access"
-msgstr "Acceso para borrar"
+msgstr "Permiso para Borrar"
msgctxt "field:ir.rule.group,perm_read:"
msgid "Read Access"
-msgstr "Acceso para leer"
+msgstr "Permiso para Leer"
msgctxt "field:ir.rule.group,perm_write:"
msgid "Write Access"
-msgstr "Acceso para escribir"
+msgstr "Permiso para Modificar"
msgctxt "field:ir.rule.group,rec_name:"
msgid "Name"
@@ -1841,15 +1864,15 @@ msgstr "Nombre de Secuencia"
msgctxt "field:ir.sequence,number_increment:"
msgid "Increment Number"
-msgstr "Cantidad a incrementar"
+msgstr "Número de Incremento"
msgctxt "field:ir.sequence,number_next:"
msgid "Next Number"
-msgstr "Siguiente número"
+msgstr "Número Siguiente"
msgctxt "field:ir.sequence,number_next_internal:"
msgid "Next Number"
-msgstr "Siguiente número"
+msgstr "Número Siguiente"
msgctxt "field:ir.sequence,padding:"
msgid "Number padding"
@@ -1917,15 +1940,15 @@ msgstr "Nombre de Secuencia"
msgctxt "field:ir.sequence.strict,number_increment:"
msgid "Increment Number"
-msgstr "Cantidad a incrementar"
+msgstr "Número de Incremento"
msgctxt "field:ir.sequence.strict,number_next:"
msgid "Next Number"
-msgstr "Número siguiente"
+msgstr "Número Siguiente"
msgctxt "field:ir.sequence.strict,number_next_internal:"
msgid "Next Number"
-msgstr "Siguiente número"
+msgstr "Número Siguiente"
msgctxt "field:ir.sequence.strict,padding:"
msgid "Number padding"
@@ -1965,7 +1988,7 @@ msgstr "Modificado por Usuario"
msgctxt "field:ir.sequence.type,code:"
msgid "Sequence Code"
-msgstr "Código de la Secuencia"
+msgstr "Código de Secuencia"
msgctxt "field:ir.sequence.type,create_date:"
msgid "Create Date"
@@ -2013,7 +2036,7 @@ msgstr "Clave"
msgctxt "field:ir.session,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.session,write_date:"
msgid "Write Date"
@@ -2041,7 +2064,7 @@ msgstr "ID"
msgctxt "field:ir.session.wizard,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.session.wizard,write_date:"
msgid "Write Date"
@@ -2061,7 +2084,7 @@ msgstr "Creado por Usuario"
msgctxt "field:ir.translation,fuzzy:"
msgid "Fuzzy"
-msgstr "Necesita revisión"
+msgstr "Vago"
msgctxt "field:ir.translation,id:"
msgid "ID"
@@ -2165,11 +2188,11 @@ msgstr "Idioma"
msgctxt "field:ir.trigger,action_function:"
msgid "Action Function"
-msgstr "Función de Acción"
+msgstr "Acción Función"
msgctxt "field:ir.trigger,action_model:"
msgid "Action Model"
-msgstr "Modelo de Acción"
+msgstr "Acción en Modelo"
msgctxt "field:ir.trigger,active:"
msgid "Active"
@@ -2205,11 +2228,11 @@ msgstr "Modelo"
msgctxt "field:ir.trigger,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.trigger,on_create:"
msgid "On Create"
-msgstr "Al ser creado"
+msgstr "Al Crearse"
msgctxt "field:ir.trigger,on_delete:"
msgid "On Delete"
@@ -2221,11 +2244,11 @@ msgstr "Al Tiempo"
msgctxt "field:ir.trigger,on_write:"
msgid "On Write"
-msgstr "Al Escribir"
+msgstr "Al Modificar"
msgctxt "field:ir.trigger,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.trigger,write_date:"
msgid "Write Date"
@@ -2249,7 +2272,7 @@ msgstr "ID"
msgctxt "field:ir.trigger.log,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.trigger.log,record_id:"
msgid "Record ID"
@@ -2289,7 +2312,7 @@ msgstr "Módulo"
msgctxt "field:ir.ui.icon,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.icon,path:"
msgid "SVG Path"
@@ -2297,7 +2320,7 @@ msgstr "Ruta al SVG"
msgctxt "field:ir.ui.icon,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.icon,sequence:"
msgid "Sequence"
@@ -2441,7 +2464,7 @@ msgstr "ID"
msgctxt "field:ir.ui.view,inherit:"
msgid "Inherited View"
-msgstr "Vista heredada"
+msgstr "Vista Heredada"
msgctxt "field:ir.ui.view,model:"
msgid "Model"
@@ -2453,7 +2476,7 @@ msgstr "Módulo"
msgctxt "field:ir.ui.view,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.view,priority:"
msgid "Priority"
@@ -2465,7 +2488,7 @@ msgstr "Nombre"
msgctxt "field:ir.ui.view,type:"
msgid "View Type"
-msgstr "Tipo de vista"
+msgstr "Tipo de Vista"
msgctxt "field:ir.ui.view,write_date:"
msgid "Write Date"
@@ -2501,11 +2524,11 @@ msgstr "Modelo"
msgctxt "field:ir.ui.view_search,name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.view_search,rec_name:"
msgid "Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre"
msgctxt "field:ir.ui.view_search,user:"
msgid "User"
@@ -2607,17 +2630,13 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr "Modificado por Usuario"
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Añade un refresco automático a la vista"
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr "Límite predeterminado para la vista en lista"
msgctxt "help:ir.action.act_window,search_value:"
msgid "Default search criteria for the list view"
-msgstr "Criterio de búsqueda predeterminado para la vista en lista"
+msgstr "Criterio de búsqueda por defecto para la vista en lista"
msgctxt "help:ir.action.act_window,window_name:"
msgid "Use the action name as window name"
@@ -2637,11 +2656,11 @@ msgid ""
"compatible format"
msgstr ""
"Dejar vacío para el mismo como plantilla, consulte la documentación de "
-"unoconv por formato compatible"
+"unoconv sobre formatos compatibles."
msgctxt "help:ir.action.report,style:"
msgid "Define the style to apply on the report."
-msgstr "Definir el estilo a aplicar al informe."
+msgstr "Define el estilo a aplicar en el informe."
msgctxt "help:ir.action.wizard,window:"
msgid "Run wizard in a new window"
@@ -2688,8 +2707,8 @@ msgid ""
"Entering a Python Regular Expression will exclude matching models from the "
"graph."
msgstr ""
-"Introducir una Expresión Regular de Python excluirá modelos equivalentes de "
-"la gráfica."
+"Ingresando una Expresión Regular de Python se excluirán modelos que "
+"concuerden con el gráfico."
msgctxt "help:ir.rule,domain:"
msgid "Domain is evaluated with \"user\" as the current user"
@@ -2697,7 +2716,7 @@ msgstr "El dominio es evaluado con el \"usuario\" como el usuario actual"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
-msgstr "Añadir esta regla para todos los usuarios de forma predeterminada"
+msgstr "Añadir la regla a todos los usuarios por defecto"
msgctxt "help:ir.rule.group,global_p:"
msgid ""
@@ -2745,7 +2764,7 @@ msgstr "Acción"
msgctxt "model:ir.action,name:act_action_act_window_form"
msgid "Window Actions"
-msgstr "Acciones de la ventana"
+msgstr "Ventana de Acciones"
msgctxt "model:ir.action,name:act_action_form"
msgid "Actions"
@@ -2769,7 +2788,7 @@ msgstr "Adjuntos"
msgctxt "model:ir.action,name:act_config_wizard_item_form"
msgid "Config Wizard Items"
-msgstr "Configurar Elementos del Asistente"
+msgstr "Asistentes de Configuración"
msgctxt "model:ir.action,name:act_cron_form"
msgid "Scheduled Actions"
@@ -2797,7 +2816,7 @@ msgstr "Menú"
msgctxt "model:ir.action,name:act_model_access_form"
msgid "Models Access"
-msgstr "Acceso a los modelos"
+msgstr "Permisos de Modelos"
msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
@@ -2805,7 +2824,7 @@ msgstr "Botones"
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
-msgstr "Campos de Acceso"
+msgstr "Permisos de Campos"
msgctxt "model:ir.action,name:act_model_fields_form"
msgid "Fields"
@@ -2817,7 +2836,7 @@ msgstr "Modelos"
msgctxt "model:ir.action,name:act_module_config"
msgid "Configure Modules"
-msgstr "Configure Modulos"
+msgstr "Configuración de Modulos"
msgctxt "model:ir.action,name:act_module_config_wizard"
msgid "Module Configuration"
@@ -2841,7 +2860,7 @@ msgstr "Propiedades Predeterminadas"
msgctxt "model:ir.action,name:act_rule_group_form"
msgid "Record Rules"
-msgstr "Grabar reglas"
+msgstr "Reglas de Registros"
msgctxt "model:ir.action,name:act_sequence_form"
msgid "Sequences"
@@ -2869,7 +2888,7 @@ msgstr "Traducciones"
msgctxt "model:ir.action,name:act_translation_set"
msgid "Set Report Translations"
-msgstr "Establecer traducciones de los informes"
+msgstr "Establecer Traducciones de los Informes"
msgctxt "model:ir.action,name:act_translation_update"
msgid "Synchronize Translations"
@@ -2897,7 +2916,7 @@ msgstr "Estado de Arbol"
msgctxt "model:ir.action,name:act_view_tree_width_form"
msgid "View Tree Width"
-msgstr "Ancho de la vista en árbol"
+msgstr "Ancho de Columna"
msgctxt "model:ir.action,name:print_model_graph"
msgid "Graph"
@@ -2909,23 +2928,23 @@ msgstr "Gráfico"
msgctxt "model:ir.action.act_window,name:"
msgid "Action act window"
-msgstr "Ventana de acciones"
+msgstr "Acción ventana acción"
msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
-msgstr "Accion dominio de ventana de acción"
+msgstr "Accion en dominio de ventana de acción"
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
-msgstr "Vista de ventana de acción"
+msgstr "Acción en vista de ventana de acción"
msgctxt "model:ir.action.keyword,name:"
msgid "Action keyword"
-msgstr "Palabra clave de acción"
+msgstr "Teclas de acción "
msgctxt "model:ir.action.report,name:"
msgid "Action report"
-msgstr "Informe de acción"
+msgstr "Acción en Reporte"
msgctxt "model:ir.action.url,name:"
msgid "Action URL"
@@ -2933,7 +2952,7 @@ msgstr "URL de la acción"
msgctxt "model:ir.action.wizard,name:"
msgid "Action wizard"
-msgstr "Asistente de la acción"
+msgstr "Acción del Asistente"
msgctxt "model:ir.attachment,name:"
msgid "Attachment"
@@ -3025,7 +3044,7 @@ msgstr "Modelo"
msgctxt "model:ir.model.access,name:"
msgid "Model access"
-msgstr "Modelo de accesos"
+msgstr "Permisos de modelo"
msgctxt "model:ir.model.button,name:"
msgid "Model Button"
@@ -3041,7 +3060,7 @@ msgstr "Modelo de campo"
msgctxt "model:ir.model.field.access,name:"
msgid "Model Field Access"
-msgstr "Modelo de Acceso a Campo"
+msgstr "Permiso de Modelo Campo"
msgctxt "model:ir.model.print_model_graph.start,name:"
msgid "Print Model Graph"
@@ -3051,6 +3070,10 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr "Módulo"
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr "Configuración del Módulo Terminada"
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
msgstr "Primer Asistente de Configuración de Módulo"
@@ -3062,7 +3085,7 @@ msgstr ""
msgctxt "model:ir.module.module.config_wizard.other,name:"
msgid "Module Config Wizard Other"
-msgstr "Otro Asistente de Configuración de Módulo"
+msgstr "Siguiente Asistente de Configuración de Módulo"
msgctxt "model:ir.module.module.dependency,name:"
msgid "Module dependency"
@@ -3070,11 +3093,11 @@ msgstr "Dependencias del módulo"
msgctxt "model:ir.module.module.install_upgrade.done,name:"
msgid "Module Install Upgrade Done"
-msgstr "Instalación de Módulo de Mejora Terminada"
+msgstr "Actualización del Módulo Terminada"
msgctxt "model:ir.module.module.install_upgrade.start,name:"
msgid "Module Install Upgrade Start"
-msgstr "Iniciar la instalación de actualizaciones de módulos"
+msgstr "Inicio de Asistente de Instalación del Módulo"
msgctxt "model:ir.property,name:"
msgid "Property"
@@ -3166,7 +3189,7 @@ msgstr "Acciones"
msgctxt "model:ir.ui.menu,name:menu_action_act_window"
msgid "Window Actions"
-msgstr "Acciones de Ventana"
+msgstr "Ventana de Acciones"
msgctxt "model:ir.ui.menu,name:menu_action_report_form"
msgid "Reports"
@@ -3190,11 +3213,11 @@ msgstr "Adjuntos"
msgctxt "model:ir.ui.menu,name:menu_config_wizard_item_form"
msgid "Config Wizard Items"
-msgstr "Configurar Elementos del Asistente"
+msgstr "Asistentes de Configuración"
msgctxt "model:ir.ui.menu,name:menu_cron_form"
msgid "Scheduled Actions"
-msgstr "Acciones programadas"
+msgstr "Acciones Programadas"
msgctxt "model:ir.ui.menu,name:menu_export_form"
msgid "Exports"
@@ -3222,7 +3245,7 @@ msgstr "Menú"
msgctxt "model:ir.ui.menu,name:menu_model_access_form"
msgid "Models Access"
-msgstr "Acceso a los modelos"
+msgstr "Permisos de Modelos"
msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
@@ -3230,7 +3253,7 @@ msgstr "Botones"
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
-msgstr "Campos de Acceso"
+msgstr "Permisos de Campos"
msgctxt "model:ir.ui.menu,name:menu_model_form"
msgid "Models"
@@ -3262,7 +3285,7 @@ msgstr "Propiedades por Defecto"
msgctxt "model:ir.ui.menu,name:menu_rule_group_form"
msgid "Record Rules"
-msgstr "Grabar reglas"
+msgstr "Reglas de Registros"
msgctxt "model:ir.ui.menu,name:menu_scheduler"
msgid "Scheduler"
@@ -3322,7 +3345,7 @@ msgstr "Estado de Arbol"
msgctxt "model:ir.ui.menu,name:menu_view_tree_width"
msgid "View Tree Width"
-msgstr "Ancho de Vista Árbol"
+msgstr "Ancho de Columna"
msgctxt "model:ir.ui.menu,name:model_model_fields_form"
msgid "Fields"
@@ -3350,11 +3373,11 @@ msgstr "Estado de Vista de Arbol"
msgctxt "model:ir.ui.view_tree_width,name:"
msgid "View Tree Width"
-msgstr "Ancho de la vista en árbol"
+msgstr "Ancho de Columna"
msgctxt "selection:ir.action.keyword,keyword:"
msgid "Action form"
-msgstr "Formulario de acción"
+msgstr "Acción formulario"
msgctxt "selection:ir.action.keyword,keyword:"
msgid "Action tree"
@@ -3410,7 +3433,7 @@ msgstr "De izquierda a derecha"
msgctxt "selection:ir.lang,direction:"
msgid "Right-to-left"
-msgstr "De derecha a izquierda"
+msgstr "Derecha-a-Izquierda"
msgctxt "selection:ir.module.module,state:"
msgid "Installed"
@@ -3586,19 +3609,19 @@ msgstr "General"
msgctxt "view:ir.action.act_window:"
msgid "Open Window"
-msgstr "Abrir ventana"
+msgstr "Abrir Ventana"
msgctxt "view:ir.action.act_window:"
msgid "Open a Window"
-msgstr "Abrir una ventana"
+msgstr "Abrir una Ventana"
msgctxt "view:ir.action.keyword:"
msgid "Keyword"
-msgstr "Palabra clave"
+msgstr "Tecla"
msgctxt "view:ir.action.keyword:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Teclas"
msgctxt "view:ir.action.report:"
msgid "General"
@@ -3646,11 +3669,11 @@ msgstr "Acción a disparar"
msgctxt "view:ir.cron:"
msgid "Scheduled Action"
-msgstr "Acción programada"
+msgstr "Acción Programada"
msgctxt "view:ir.cron:"
msgid "Scheduled Actions"
-msgstr "Acciones programadas"
+msgstr "Acciones Programadas"
msgctxt "view:ir.export:"
msgid "Exports"
@@ -3686,7 +3709,7 @@ msgstr "Botones"
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
-msgstr "Acceso al Campo"
+msgstr "Permiso de Campo"
msgctxt "view:ir.model.field:"
msgid "Field"
@@ -3704,6 +3727,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Descripción del modelo"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr "Configuración del módulo"
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr "La configuracion ha terminado."
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "Bienvenido al asistente de configuración del módulo!"
@@ -3718,7 +3749,7 @@ msgstr ""
msgctxt "view:ir.module.module.config_wizard.item:"
msgid "Config Wizard Items"
-msgstr "Configurar Elementos del Asistente"
+msgstr "Asistentes de Configuración"
msgctxt "view:ir.module.module.config_wizard.other:"
msgid "Configuration Wizard Next Step!"
@@ -3734,7 +3765,7 @@ msgstr "Dependencia"
msgctxt "view:ir.module.module.install_upgrade.done:"
msgid "System Upgrade Done"
-msgstr "Actualización del Sistema finalizada"
+msgstr "Actualización del Sistema Finalizada"
msgctxt "view:ir.module.module.install_upgrade.done:"
msgid "The modules have been upgraded / installed !"
@@ -3746,7 +3777,7 @@ msgstr "Esta operación puede demorar varios minutos."
msgctxt "view:ir.module.module.install_upgrade.start:"
msgid "System Upgrade"
-msgstr "Mejora del Sistema"
+msgstr "Actualización del Sistema"
msgctxt "view:ir.module.module.install_upgrade.start:"
msgid "Your system will be upgraded."
@@ -3801,7 +3832,7 @@ msgstr ""
msgctxt "view:ir.rule.group:"
msgid "Record rules"
-msgstr "Grabar reglas"
+msgstr "Reglas de Registros"
msgctxt "view:ir.rule.group:"
msgid "The rule is satisfied if at least one test is True"
@@ -3837,7 +3868,7 @@ msgstr "Mes:"
msgctxt "view:ir.sequence.strict:"
msgid "Sequences Strict"
-msgstr "Secuencias estrictas"
+msgstr "Secuencias Estrictas"
msgctxt "view:ir.sequence.strict:"
msgid "Year:"
@@ -3845,7 +3876,7 @@ msgstr "Año:"
msgctxt "view:ir.sequence.type:"
msgid "Sequence Type"
-msgstr "Tipo de secuencia"
+msgstr "Tipo de Secuencia"
msgctxt "view:ir.sequence:"
msgid "${day}"
@@ -3893,7 +3924,7 @@ msgstr "Limpiar Traducciones"
msgctxt "view:ir.translation.clean.start:"
msgid "Clean Translations?"
-msgstr "¿Limpiar Traducciones?"
+msgstr "Limpiar Traducciones?"
msgctxt "view:ir.translation.clean.succeed:"
msgid "Clean Translations"
@@ -3979,22 +4010,21 @@ msgctxt "view:ir.ui.view_search:"
msgid "View Searches"
msgstr "Vista Busquedas"
-#, fuzzy
msgctxt "view:ir.ui.view_tree_state:"
msgid "View Tree State"
msgstr "Estado de Vista de Arbol"
msgctxt "view:ir.ui.view_tree_state:"
msgid "Views Tree State"
-msgstr ""
+msgstr "Estado de Vista de Arbol"
msgctxt "view:ir.ui.view_tree_width:"
msgid "View Tree Width"
-msgstr "Ancho de la vista en árbol"
+msgstr "Ancho de Columna"
msgctxt "view:ir.ui.view_tree_width:"
msgid "Views Tree Width"
-msgstr "Ancho de la vista en árbol"
+msgstr "Anchos de Columna"
msgctxt "wizard_button:ir.model.print_model_graph,start,end:"
msgid "Cancel"
@@ -4004,6 +4034,10 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr "Imprimir"
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "Aceptar"
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr "Aceptar"
diff --git a/trytond/ir/locale/es_ES.po b/trytond/ir/locale/es_ES.po
index 3592f99..bdb021d 100644
--- a/trytond/ir/locale/es_ES.po
+++ b/trytond/ir/locale/es_ES.po
@@ -23,20 +23,32 @@ msgid "You are not allowed to delete this record."
msgstr "No está autorizado para eliminar este registro."
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "El campo \"%s\" en \"%s\" contiene demasiados decimales."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
+msgstr ""
+"El número de dígitos \"%(digits)s\" del campo \"%(field)s\" de \"%(value)s\""
+" es superior a su límite."
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
-msgstr "El valor del campo \"%s\" de \"%s\" no es correcto según su dominio."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
+msgstr ""
+"El valor del campo \"%(field)s\" de \"%(model)s\" no es correcto según su "
+"dominio."
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
-msgstr "No puede eliminar \"%s\" registros porque se usan en el campo \"%s\" de \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
+msgstr ""
+"No se pueden eliminar los registros porqué se utilizan en el campo "
+"\"%(field)s\" del \"%(model)s\". "
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "El valor del campo \"%s\" de \"%s\" no existe."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr "El valor \"%(value)s\" del campo \"%(field)s\" de \"%(model)s\" no existe."
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -141,7 +153,7 @@ msgstr "No puede modificar el campo (%s.%s)."
msgctxt "error:ir.model.field:"
msgid "Model Field name \"%s\" is not a valid python identifier."
-msgstr "El nombre del modelo \"%s\" no es un identificador de python correcto."
+msgstr "El nombre del modelo \"%s\" no es un identificador de Python correcto."
msgctxt "error:ir.model.field:"
msgid "The field name in model must be unique!"
@@ -149,7 +161,7 @@ msgstr "El nombre del campo en el modelo debe ser único."
msgctxt "error:ir.model:"
msgid "Module name \"%s\" is not a valid python identifier."
-msgstr "El nombre del model \"%s\" no es un identificador de python correcto."
+msgstr "El nombre del model \"%s\" no es un identificador de Python correcto."
msgctxt "error:ir.model:"
msgid "The model must be unique!"
@@ -166,7 +178,7 @@ msgstr "Faltan las dependencias %s para el módulo \"%s\"."
msgctxt "error:ir.module.module:"
msgid "The modules you are trying to uninstall depends on installed modules:"
msgstr ""
-"Los módulos que esta intentando desintalar depende de otros módulos "
+"Los módulos que está intentando desinstalar depende de otros módulos "
"instalados:"
msgctxt "error:ir.module.module:"
@@ -246,8 +258,8 @@ msgid ""
"Condition \"%(condition)s\" is not a valid python expression on trigger "
"\"%(trigger)s\"."
msgstr ""
-"La condición \"%(condition)s\" no es una expresión python válida en el "
-"disparador \"%(trigger)s\"."
+"La condición \"%(condition)s\" no es una expresión Python válida en el "
+"disparador \"P%(trigger)s\"."
msgctxt "error:ir.ui.menu:"
msgid "\"%s\" is not a valid menu name because it is not allowed to contain \" / \"."
@@ -257,10 +269,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr "XML de la vista \"%s\" no es correcto."
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "No se ha encontrado la clave %r en el campo selección %r."
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -282,7 +290,7 @@ msgid ""
"Recursion error: Record \"%(rec_name)s\" with parent \"%(parent_rec_name)s\""
" was configured as ancestor of itself."
msgstr ""
-"Error de recursión: El registro \"%(rec_name)s\" con el padre "
+"Error de recursividad: El registro \"%(rec_name)s\" con el padre "
"\"%(parent_rec_name)s\" se configuró como padre de si mismo."
msgctxt "error:reference_syntax_error:"
@@ -294,32 +302,40 @@ msgid "Relation not found: %r in %s"
msgstr "No se ha encontrado la relación: %r en %s."
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "El campo \"%s\" en \"%s\" es requerido."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "El campo \"%(field)s\" de \"%(model)s\" es obligatorio."
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "El campo \"%s\" en \"%s\" es requerido."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "El campo \"%(field)s\" de \"%(model)s\" es obligatorio."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
msgstr "Falta la función de búsqueda del campo \"%s\"."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
-msgstr "El campo \"%s\" en \"%s\" no se encuentra en la selección."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
+msgstr ""
+"El valor \"%(value)s\" del campo \"%(field)s\" de \"%(model)s\" no está en "
+"la selección."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
msgstr "El valor no se encuentra en la selección en el campo \"%s\"."
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "El campo \"%s\" en \"%s\" es demasiado largo."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr ""
+"El tamaño \"%(size)s\" del campo \"%(field)s\" del \"%(model)s\" es "
+"demasiado grande."
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
-msgstr "El valor tiempo del campo \"%s\" en \"%s\" no es correcto."
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
+msgstr ""
+"El valor temporal \"%(value)s\" del campo \"%(field)s\" de \"%(model)s) no "
+"es valido."
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
@@ -421,10 +437,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Activo"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Auto-Refresco"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr "Valor del contexto"
@@ -721,6 +733,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Contenido"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr "Nombre del contenido"
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Nombre interno"
@@ -1047,7 +1063,7 @@ msgstr "Número de intervalos"
msgctxt "field:ir.cron,interval_type:"
msgid "Interval Unit"
-msgstr "Unidad de intérvalo"
+msgstr "Unidad de intervalo"
msgctxt "field:ir.cron,model:"
msgid "Model"
@@ -1581,6 +1597,10 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr "Usuario modificación"
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr "ID"
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr "ID"
@@ -2609,10 +2629,6 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr "Usuario modificación"
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Añade un auto-refresco a la vista."
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr "Límite por defecto en las vistas de lista."
@@ -2739,7 +2755,7 @@ msgstr ""
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
-msgstr "Dominio PYSON"
+msgstr "Dominio PYSON."
msgctxt "model:ir.action,name:"
msgid "Action"
@@ -2851,7 +2867,7 @@ msgstr "Secuencias"
msgctxt "model:ir.action,name:act_sequence_strict_form"
msgid "Sequences Strict"
-msgstr "Secuencia estricta"
+msgstr "Secuencias estrictas"
msgctxt "model:ir.action,name:act_sequence_type_form"
msgid "Sequence Types"
@@ -3053,6 +3069,10 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr "Módulo"
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr "Asitente de configuración de los módulos realizado"
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
msgstr "Inicio asistente de configuración del módulo"
@@ -3275,7 +3295,7 @@ msgstr "Secuencias"
msgctxt "model:ir.ui.menu,name:menu_sequence_strict_form"
msgid "Sequences Strict"
-msgstr "Secuencia estricta"
+msgstr "Secuencias estrictas"
msgctxt "model:ir.ui.menu,name:menu_sequences"
msgid "Sequences"
@@ -3307,7 +3327,7 @@ msgstr "Disparadores"
msgctxt "model:ir.ui.menu,name:menu_ui"
msgid "User Interface"
-msgstr "Interfície del usuario"
+msgstr "Interfaz de usuario"
msgctxt "model:ir.ui.menu,name:menu_view"
msgid "Views"
@@ -3439,7 +3459,7 @@ msgstr "Realizado"
msgctxt "selection:ir.module.module.config_wizard.item,state:"
msgid "Open"
-msgstr "Para abrir"
+msgstr "Pendiente"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "Installed"
@@ -3705,6 +3725,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Descripción modelo"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr "Configuración de los módulos"
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr "La configuración se ha realizado correctamente."
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "Bienvenido a la configuración del módulo."
@@ -3722,7 +3750,7 @@ msgstr "Asistente de configuración"
msgctxt "view:ir.module.module.config_wizard.other:"
msgid "Configuration Wizard Next Step!"
-msgstr "Siguiente paso asistente de configuracion."
+msgstr "Siguiente paso asistente de configuración."
msgctxt "view:ir.module.module.dependency:"
msgid "Dependencies"
@@ -3889,7 +3917,7 @@ msgstr "¿Sincronizar las traducciones?"
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Succeed!"
-msgstr "Las traducciones se han realitzado correctament."
+msgstr "Las traducciones se han realizado correctamente."
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Translations"
@@ -3971,6 +3999,10 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr "Imprimir"
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "Aceptar"
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr "Aceptar"
@@ -4029,7 +4061,7 @@ msgstr "Cancelar"
msgctxt "wizard_button:ir.translation.set,start,set_:"
msgid "Set"
-msgstr "Grupo"
+msgstr "Definir"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
msgid "Ok"
diff --git a/trytond/ir/locale/fr_FR.po b/trytond/ir/locale/fr_FR.po
index 989c2ba..8cc677d 100644
--- a/trytond/ir/locale/fr_FR.po
+++ b/trytond/ir/locale/fr_FR.po
@@ -20,25 +20,35 @@ msgstr ""
msgctxt "error:delete_xml_record:"
msgid "You are not allowed to delete this record."
-msgstr "Vous n'êtes pas authorisé à supprimer cet enregistrement"
+msgstr "Vous n'êtes pas autorisé à supprimer cet enregistrement"
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "Le champ \"%s\" de \"%s\" à trop de décimales."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
+msgstr ""
+"Le nombre de décimales \"%(digits)s\" du champ \"%(field)s\" dépasse sa "
+"limite."
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
-msgstr "La valeur du champ \"%s\" de \"%s\" n'est pas valide suivant son domaine."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
+msgstr ""
+"La valeur du champ \"%(field)s\" sur \"%(model)s\" n'est pas valide selon "
+"son domaine."
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
msgstr ""
-"Impossible de supprimer les enregistrements \"%s\" car ils sont utilisé dans"
-" le champ \"%s\" de \"%s\"."
+"Impossible de supprimer les enregistrements car ils sont utilisés dans le "
+"champ \"%(field)s\" de \"%(model)s\"."
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "La valeur du champ \"%s\" de \"%s\" est absente."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr "La valeur \"%(value)s\" du champ \"%(field)s\" sur \"%(model)s\" n'existe pas."
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -266,10 +276,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr "XML invalide sur la vue \"%s\"."
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "Clé %r non trouvée dans la sélection %r"
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -303,32 +309,36 @@ msgid "Relation not found: %r in %s"
msgstr "Relation non trouvée : %r dans %r"
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Le champ \"%s\" de \"%s\" est requis."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "Le champ \"%(field)s\" sur \"%(model)s\" est requis."
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Le champ \"%s\" de \"%s\" est requis."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "Le champ \"%(field)s\" sur \"%(model)s\" est requis."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
msgstr "Fonction de recherche absente pour le champ \"%s\"."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
-msgstr "Le champ \"%s\" sur \"%s\" n'est pas dans la sélection."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
+msgstr ""
+"La valeur \"%(value)s\" du champ \"%(field)s\" sur \"%(model)s\" n'est pas "
+"dans la sélection."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
msgstr "Valeur absente de la sélection pour le champ \"%s\"."
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "Le champ \"%s\" de \"%s\" est trop long"
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr "La taille \"%(size)s\" du champ \"%(field)s\" sur \"%(model)s\" est trop longue."
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
-msgstr "La valeur de type temps du champ \"%s\" de \"%s\" n'est pas valide"
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
+msgstr "L'heure \"%(value)s\" du champ \"%(field)s\" sur \"%(model)s\" n'est pas valide."
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
@@ -430,10 +440,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Actif"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Auto-rafraîchissement"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr "Valeur du contexte"
@@ -730,6 +736,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Contenu"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr "Nom du contenu"
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Nom interne"
@@ -1590,6 +1600,10 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr "Mis à jour par"
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr "ID"
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr "ID"
@@ -2618,10 +2632,6 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr "Mis à jour par"
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Ajoute un auto-rafraîchissement sur la vue"
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr "Limite par défaut pour la vue liste"
@@ -3061,6 +3071,10 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr "Module"
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr "Assistant de configuration de module - Fin"
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
msgstr "Assistant de configuration de module - Première"
@@ -3713,6 +3727,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Description de modèle"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr "Configuration de module"
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr "La configuration est faite."
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "Bienvenu dans l'assistant de configuration de module !"
@@ -4012,6 +4034,10 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr "Imprimer"
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "Ok"
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr "Ok"
diff --git a/trytond/ir/locale/nl_NL.po b/trytond/ir/locale/nl_NL.po
index c191cfe..daa70aa 100644
--- a/trytond/ir/locale/nl_NL.po
+++ b/trytond/ir/locale/nl_NL.po
@@ -21,22 +21,26 @@ msgid "You are not allowed to delete this record."
msgstr "Het is u niet toegestaan dit item te verwijderen."
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "Het veld \"%s\" in \"%s\" heeft te veel decimalen."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
+msgstr ""
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
-msgstr "De waarde van het veld \"%s\" in \"%s\" is niet geldig binnen zijn domein."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
+msgstr ""
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
msgstr ""
-"Kon items \"%s\" niet verwijderen omdat ze in gebruik waren in veld \"%s\" "
-"van \"%s\"."
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "De waarde van veld \"%s\" in \"%s\" bestaat niet."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr ""
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -244,10 +248,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr ""
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "Sleutel %r niet gevonden in selectie veld %r"
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -277,19 +277,21 @@ msgid "Relation not found: %r in %s"
msgstr "Relatie niet gevonden: %r in %s"
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Het veld \"%s\" in \"%s\" is vereist."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr ""
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Het veld \"%s\" in \"%s\" is vereist."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr ""
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
msgstr "Zoekfunctie ontbreekt voor veld \" %s\"."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
msgstr ""
msgctxt "error:selection_value_notfound:"
@@ -297,11 +299,11 @@ msgid "Value not in the selection for field \"%s\"."
msgstr ""
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "Veld \"%s\" op \"%s\" is te lang."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr ""
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
msgstr ""
msgctxt "error:too_many_relations_found:"
@@ -404,10 +406,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Actief"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Automatisch verversen"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr "Samenhang waarde"
@@ -723,6 +721,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Inhoud"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr ""
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Interne naam"
@@ -1614,6 +1616,10 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr ""
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr ""
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr ""
@@ -2671,10 +2677,6 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr ""
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Voegt automatisch verversen toe"
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr "Standaard begrenzing voor dit aanzicht"
@@ -3108,6 +3110,10 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr "Module"
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr ""
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
msgstr "Module EHBC"
@@ -3773,6 +3779,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Model omschrijving"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr ""
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr ""
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "Welkom bij de module configuratie assistent!"
@@ -4046,6 +4060,11 @@ msgid "Print"
msgstr ""
#, fuzzy
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "Oké"
+
+#, fuzzy
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr "Oké"
diff --git a/trytond/ir/locale/ru_RU.po b/trytond/ir/locale/ru_RU.po
index 5e5ffb1..6af0951 100644
--- a/trytond/ir/locale/ru_RU.po
+++ b/trytond/ir/locale/ru_RU.po
@@ -23,24 +23,26 @@ msgid "You are not allowed to delete this record."
msgstr "Вы не можете удалить эту запись."
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "Поле \"%s\" на \"%s\" содержит очень много десятичных знаков."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
+msgstr ""
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
msgstr ""
-"Значение поля \"%s\" в таблице \"%s\" не действует в соответствии с его "
-"домена."
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
msgstr ""
-"Не могу удалить \"%s\" записей, поскольку они используются в поле \"%s\" из "
-"таблицы \"%s\"."
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "Значение поля \"%s\" в таблице \"%s\" отсутствует."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr ""
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -252,10 +254,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr "Некорректный XML для вида \"%s\"."
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "Ключ %r не найден в поле выделения %r"
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -287,19 +285,21 @@ msgid "Relation not found: %r in %s"
msgstr "Связь не найдена: %r в %s"
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Поле \"%s\" на \"%s\" необходимо."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr ""
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Поле \"%s\" на \"%s\" необходимо."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr ""
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
msgstr "Отсутствует функция поиска для поля \"%s\"."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
msgstr ""
msgctxt "error:selection_value_notfound:"
@@ -307,12 +307,12 @@ msgid "Value not in the selection for field \"%s\"."
msgstr "Значение не выделено в поле \"%s\"."
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "Поле \"%s\" в таблице \"%s\" слишком длинное."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr ""
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
-msgstr "Значение времени поля \"%s\" в \"%s\" некорректное."
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
+msgstr ""
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
@@ -414,10 +414,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Действующий"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Авто-Обновление"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr "Значение контекста"
@@ -714,6 +710,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Содержание"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr ""
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Внутреннее наименование"
@@ -1574,6 +1574,11 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr "Изменено пользователем"
+#, fuzzy
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr "ID"
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr "ID"
@@ -2611,10 +2616,6 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr "Изменено пользователем"
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Добавить авто-обновление в просмотр"
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr "По умолчанию кол-во записей в списке"
@@ -3055,6 +3056,10 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr "Модуль"
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr ""
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
msgstr "Мастер конфигурации модуля"
@@ -3708,6 +3713,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Описание модели"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr ""
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr ""
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "Добро пожаловать в мастер конфигурации модуля!"
@@ -3976,6 +3989,11 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr "Печать"
+#, fuzzy
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "Ок"
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr "Ок"
diff --git a/trytond/ir/locale/sl_SI.po b/trytond/ir/locale/sl_SI.po
index ff857f9..59694de 100644
--- a/trytond/ir/locale/sl_SI.po
+++ b/trytond/ir/locale/sl_SI.po
@@ -23,20 +23,32 @@ msgid "You are not allowed to delete this record."
msgstr "Nimate dovoljenja za brisanje tega zapisa"
msgctxt "error:digits_validation_record:"
-msgid "The field \"%s\" on \"%s\" has too many decimal digits."
-msgstr "Polje \"%s\" pri \"%s\" ima preveč decimalk."
+msgid ""
+"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
+"exceeds it's limit."
+msgstr ""
+"Število decimalk \"%(digits)s\" polja \"%(field)s\" pri \"%(model)s\" je "
+"preseženo."
msgctxt "error:domain_validation_record:"
-msgid "The value of the field \"%s\" on \"%s\" is not valid according to its domain."
-msgstr "Vrednost polja \"%s\" pri \"%s\" ni veljavna glede na svojo domeno."
+msgid ""
+"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
+" to its domain."
+msgstr ""
+"Vrednost polja \"%(field)s\" pri \"%(model)s\" ni veljavna glede na svojo "
+"domeno."
msgctxt "error:foreign_model_exist:"
-msgid "Could not delete \"%s\" records because they are used on field \"%s\" of \"%s\"."
-msgstr "\"%s\" zapisov ni možno zbrisati, ker so uporabljeni v polju \"%s\" od \"%s\"."
+msgid ""
+"Could not delete the records because they are used on field \"%(field)s\" of"
+" \"%(model)s\"."
+msgstr ""
+"Zapisov ni možno zbrisati, ker so uporabljeni v polju \"%(field)s\" modela "
+"\"%(model)s\"."
msgctxt "error:foreign_model_missing:"
-msgid "The value of field \"%s\" on \"%s\" doesn't exist."
-msgstr "Vrednost polja \"%s\" pri \"%s\" ne obstaja."
+msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
+msgstr "Vrednost \"%(value)s\" polja \"%(field)s\" pri \"%(model)s\" ne obstaja."
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
@@ -255,10 +267,6 @@ msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
msgstr "Neveljaven XML za pogled \"%s\"."
-msgctxt "error:not_found_in_selection:"
-msgid "Key %r not found in selection field %r"
-msgstr "Ključa %r ni možno najti v izbirnem polju %r"
-
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
@@ -292,32 +300,36 @@ msgid "Relation not found: %r in %s"
msgstr "Veze ni možno najti: %r v %s"
msgctxt "error:required_field:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Polje \"%s\" v \"%s\" je obvezno."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "Polje \"%(field)s\" v \"%(model)s\" je obvezno."
msgctxt "error:required_validation_record:"
-msgid "The field \"%s\" on \"%s\" is required."
-msgstr "Polje \"%s\" v \"%s\" je obvezno."
+msgid "The field \"%(field)s\" on \"%(model)s\" is required."
+msgstr "Polje \"%(field)s\" v \"%(model)s\" je obvezno."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
msgstr "Manjka funkcija iskanja pri polju \"%s\"."
msgctxt "error:selection_validation_record:"
-msgid "The field \"%s\" on \"%s\" is not in the selection."
-msgstr "Polje \"%s\" pri \"%s\" ni v izboru."
+msgid ""
+"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
+"the selection."
+msgstr "Vrednost \"%(value)s\" polja \"%(field)s\" v \"%(model)s\" ni v izboru."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
msgstr "Vrednost za polje \"%s\" ni v izboru."
msgctxt "error:size_validation_record:"
-msgid "The field \"%s\" on \"%s\" is too long."
-msgstr "Polje \"%s\" v \"%s\" je predolgo."
+msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
+msgstr "Velikost \"%(size)s\" polja \"%(field)s\" v \"%(model)s\" je presežena."
msgctxt "error:time_format_validation_record:"
-msgid "The time value of field \"%s\" on \"%s\" is not valid."
-msgstr "Časovna vrednost v polju \"%s\" pri \"%s\" je neveljavna."
+msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
+msgstr ""
+"Časovna vrednost \"%(value)s\" v polju \"%(field)s\" pri \"%(model)s\" je "
+"neveljavna."
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
@@ -419,10 +431,6 @@ msgctxt "field:ir.action.act_window,active:"
msgid "Active"
msgstr "Aktivno"
-msgctxt "field:ir.action.act_window,auto_refresh:"
-msgid "Auto-Refresh"
-msgstr "Samodejna osvežitev"
-
msgctxt "field:ir.action.act_window,context:"
msgid "Context Value"
msgstr "Vrednost konteksta"
@@ -719,6 +727,10 @@ msgctxt "field:ir.action.report,report_content_custom:"
msgid "Content"
msgstr "Vsebina"
+msgctxt "field:ir.action.report,report_content_name:"
+msgid "Content Name"
+msgstr "Ime vsebine"
+
msgctxt "field:ir.action.report,report_name:"
msgid "Internal Name"
msgstr "Interni naziv"
@@ -1579,6 +1591,10 @@ msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
msgstr "Zapisal"
+msgctxt "field:ir.module.module.config_wizard.done,id:"
+msgid "ID"
+msgstr "ID"
+
msgctxt "field:ir.module.module.config_wizard.first,id:"
msgid "ID"
msgstr "ID"
@@ -2081,7 +2097,7 @@ msgstr "Modul"
msgctxt "field:ir.translation,name:"
msgid "Field Name"
-msgstr "Ime polja"
+msgstr "Polje"
msgctxt "field:ir.translation,overriding_module:"
msgid "Overriding Module"
@@ -2607,10 +2623,6 @@ msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
msgstr "Zapisal"
-msgctxt "help:ir.action.act_window,auto_refresh:"
-msgid "Add an auto-refresh on the view"
-msgstr "Dodaj samoosvežitev na pogledu"
-
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
msgstr "Privzeta meja za naštevni pogled"
@@ -3049,9 +3061,13 @@ msgctxt "model:ir.module.module,name:"
msgid "Module"
msgstr "Modul"
+msgctxt "model:ir.module.module.config_wizard.done,name:"
+msgid "Module Config Wizard Done"
+msgstr "Konfiguracija modula zaključek"
+
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
-msgstr "Nastavitveni čarovnik"
+msgstr "Konfiguracija modula zagon"
msgctxt "model:ir.module.module.config_wizard.item,name:"
msgid "Config wizard to run after installing module"
@@ -3059,7 +3075,7 @@ msgstr "Nastavitveni čarovnik, ki se zažene po namestitvi modula"
msgctxt "model:ir.module.module.config_wizard.other,name:"
msgid "Module Config Wizard Other"
-msgstr "Dodatna konfiguracija modula"
+msgstr "Konfiguracija modula drugo"
msgctxt "model:ir.module.module.dependency,name:"
msgid "Module dependency"
@@ -3067,11 +3083,11 @@ msgstr "Odvisnost modula"
msgctxt "model:ir.module.module.install_upgrade.done,name:"
msgid "Module Install Upgrade Done"
-msgstr "Namestitev/nadgradnja modula končana."
+msgstr "Namestitev modula zaključek"
msgctxt "model:ir.module.module.install_upgrade.start,name:"
msgid "Module Install Upgrade Start"
-msgstr "Start za namestitev/nadgradnjo modula"
+msgstr "Namestitev modula zagon"
msgctxt "model:ir.property,name:"
msgid "Property"
@@ -3701,6 +3717,14 @@ msgctxt "view:ir.model:"
msgid "Model Description"
msgstr "Opis modela"
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "Module configuration"
+msgstr "Konfiguracija modula"
+
+msgctxt "view:ir.module.module.config_wizard.done:"
+msgid "The configuration is done."
+msgstr "Konfiguracija je zaključena."
+
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
msgstr "Dobrodošli v čarovniku za nastavitev modulov"
@@ -3964,6 +3988,10 @@ msgctxt "wizard_button:ir.model.print_model_graph,start,print_:"
msgid "Print"
msgstr "Natisni"
+msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
+msgid "Ok"
+msgstr "V redu"
+
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
msgid "Ok"
msgstr "V redu"
diff --git a/trytond/ir/model.py b/trytond/ir/model.py
index 72e7a28..cb63303 100644
--- a/trytond/ir/model.py
+++ b/trytond/ir/model.py
@@ -68,6 +68,7 @@ class Model(ModelSQL, ModelView):
cls._order.insert(0, ('model', 'ASC'))
cls.__rpc__.update({
'list_models': RPC(),
+ 'list_history': RPC(),
'global_search': RPC(),
})
@@ -130,6 +131,12 @@ class Model(ModelSQL, ModelView):
return [m.model for m in models]
@classmethod
+ def list_history(cls):
+ 'Return a list of all models with history'
+ return [name for name, model in Pool().iterobject()
+ if getattr(model, '_history', False)]
+
+ @classmethod
def create(cls, vlist):
pool = Pool()
Property = pool.get('ir.property')
@@ -139,10 +146,10 @@ class Model(ModelSQL, ModelView):
return res
@classmethod
- def write(cls, models, vals):
+ def write(cls, models, values, *args):
pool = Pool()
Property = pool.get('ir.property')
- super(Model, cls).write(models, vals)
+ super(Model, cls).write(models, values, *args)
# Restart the cache of models_get
Property._models_get_cache.clear()
@@ -527,8 +534,40 @@ class ModelAccess(ModelSQL, ModelView):
return True
@classmethod
- def write(cls, accesses, vals):
- super(ModelAccess, cls).write(accesses, vals)
+ def check_relation(cls, model_name, field_name, mode='read'):
+ 'Check access to relation field for model_name and mode'
+ pool = Pool()
+ Model = pool.get(model_name)
+ field = getattr(Model, field_name)
+ if field._type in ('one2many', 'many2one'):
+ return cls.check(field.model_name, mode=mode,
+ raise_exception=False)
+ elif field._type in ('many2many', 'one2one'):
+ if (field.target
+ and not cls.check(field.target, mode=mode,
+ raise_exception=False)):
+ return False
+ elif (field.relation_name
+ and not cls.check(field.relation_name, mode=mode,
+ raise_exception=False)):
+ return False
+ else:
+ return True
+ elif field._type == 'reference':
+ selection = field.selection
+ if isinstance(selection, basestring):
+ selection = getattr(Model, field.selection)()
+ for model_name, _ in selection:
+ if not cls.check(model_name, mode=mode,
+ raise_exception=False):
+ return False
+ return True
+ else:
+ return True
+
+ @classmethod
+ def write(cls, accesses, values, *args):
+ super(ModelAccess, cls).write(accesses, values, *args)
# Restart the cache
cls._get_access_cache.clear()
ModelView._fields_view_get_cache.clear()
@@ -681,8 +720,8 @@ class ModelFieldAccess(ModelSQL, ModelView):
return True
@classmethod
- def write(cls, field_accesses, vals):
- super(ModelFieldAccess, cls).write(field_accesses, vals)
+ def write(cls, field_accesses, values, *args):
+ super(ModelFieldAccess, cls).write(field_accesses, values, *args)
# Restart the cache
cls._get_access_cache.clear()
ModelView._fields_view_get_cache.clear()
@@ -730,8 +769,8 @@ class ModelButton(ModelSQL, ModelView):
return result
@classmethod
- def write(cls, buttons, values):
- super(ModelButton, cls).write(buttons, values)
+ def write(cls, buttons, values, *args):
+ super(ModelButton, cls).write(buttons, values, *args)
# Restart the cache for get_groups
cls._groups_cache.clear()
@@ -813,8 +852,8 @@ class ModelData(ModelSQL, ModelView):
return False
@classmethod
- def write(cls, data, values):
- super(ModelData, cls).write(data, values)
+ def write(cls, data, values, *args):
+ super(ModelData, cls).write(data, values, *args)
# Restart the cache for get_id
cls._get_id_cache.clear()
diff --git a/trytond/ir/module/module.py b/trytond/ir/module/module.py
index 5322ca4..605a614 100644
--- a/trytond/ir/module/module.py
+++ b/trytond/ir/module/module.py
@@ -1,5 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
+from functools import wraps
+
from trytond.model import ModelView, ModelSQL, fields
from trytond.modules import create_graph, get_module_list, get_module_info
from trytond.wizard import Wizard, StateView, Button, StateTransition, \
@@ -12,12 +14,23 @@ from trytond.rpc import RPC
__all__ = [
'Module', 'ModuleDependency', 'ModuleConfigWizardItem',
- 'ModuleConfigWizardFirst', 'ModuleConfigWizardOther', 'ModuleConfigWizard',
+ 'ModuleConfigWizardFirst', 'ModuleConfigWizardOther',
+ 'ModuleConfigWizardDone', 'ModuleConfigWizard',
'ModuleInstallUpgradeStart', 'ModuleInstallUpgradeDone',
'ModuleInstallUpgrade', 'ModuleConfig',
]
+def filter_state(state):
+ def filter(func):
+ @wraps(func)
+ def wrapper(cls, modules):
+ modules = [m for m in modules if m.state == state]
+ return func(cls, modules)
+ return wrapper
+ return filter
+
+
class Module(ModelSQL, ModelView):
"Module"
__name__ = "ir.module.module"
@@ -97,14 +110,14 @@ class Module(ModelSQL, ModelView):
@classmethod
def get_childs(cls, modules, name):
child_ids = dict((m.id, []) for m in modules)
- names = [m.name for m in modules]
+ name2id = dict((m.name, m.id) for m in modules)
childs = cls.search([
- ('dependencies.name', 'in', names),
+ ('dependencies.name', 'in', name2id.keys()),
])
for child in childs:
for dep in child.dependencies:
- if dep.module.id in child_ids:
- child_ids[dep.module.id].append(child.id)
+ if dep.name in name2id:
+ child_ids[name2id[dep.name]].append(child.id)
return child_ids
@classmethod
@@ -121,34 +134,38 @@ class Module(ModelSQL, ModelView):
@classmethod
def on_write(cls, modules):
- ids = set()
- graph, packages, later = create_graph(get_module_list())
- for module in modules:
- if module.name not in graph:
- continue
+ dependencies = set()
+
+ def get_parents(module):
+ parents = set(p.id for p in module.parents)
+ for p in module.parents:
+ parents.update(get_parents(p))
+ return parents
+
+ def get_childs(module):
+ childs = set(c.id for c in module.childs)
+ for c in module.childs:
+ childs.update(get_childs(c))
+ return childs
- def get_parents(module):
- parents = set(p.name for p in module.parents)
- for p in module.parents:
- parents.update(get_parents(p))
- return parents
- dependencies = get_parents(module)
-
- def get_childs(module):
- childs = set(c.name for c in module.childs)
- for c in module.childs:
- childs.update(get_childs(c))
- return childs
+ for module in modules:
+ dependencies.update(get_parents(module))
dependencies.update(get_childs(module))
- ids |= set(x.id for x in cls.search([
- ('name', 'in', list(dependencies)),
- ]))
- return list(ids)
+ return list(dependencies)
@classmethod
@ModelView.button
+ @filter_state('uninstalled')
def install(cls, modules):
+ modules_install = set(modules)
graph, packages, later = create_graph(get_module_list())
+
+ def get_parents(module):
+ parents = set(p for p in module.parents)
+ for p in module.parents:
+ parents.update(get_parents(p))
+ return parents
+
for module in modules:
if module.name not in graph:
missings = []
@@ -157,24 +174,25 @@ class Module(ModelSQL, ModelView):
missings = [x for x in deps if x not in graph]
cls.raise_user_error('missing_dep', (missings, module.name))
- def get_parents(module):
- parents = set(p.name for p in module.parents)
- for p in module.parents:
- parents.update(get_parents(p))
- return parents
- dependencies = list(get_parents(module))
- modules_install = cls.search([
- ('name', 'in', dependencies),
- ('state', '=', 'uninstalled'),
- ])
- cls.write(modules_install + [module], {
- 'state': 'to install',
- })
+ modules_install.update((m for m in get_parents(module)
+ if m.state == 'uninstalled'))
+ cls.write(list(modules_install), {
+ 'state': 'to install',
+ })
@classmethod
@ModelView.button
+ @filter_state('installed')
def upgrade(cls, modules):
+ modules_installed = set(modules)
graph, packages, later = create_graph(get_module_list())
+
+ def get_childs(module):
+ childs = set(c for c in module.childs)
+ for c in module.childs:
+ childs.update(get_childs(c))
+ return childs
+
for module in modules:
if module.name not in graph:
missings = []
@@ -183,24 +201,15 @@ class Module(ModelSQL, ModelView):
missings = [x for x in deps if x not in graph]
cls.raise_user_error('missing_dep', (missings, module.name))
- def get_childs(name, graph):
- childs = set(x.name for x in graph[name].childs)
- childs2 = set()
- for child in childs:
- childs2.update(get_childs(child, graph))
- childs.update(childs2)
- return childs
- dependencies = list(get_childs(module.name, graph))
- modules_installed = cls.search([
- ('name', 'in', dependencies),
- ('state', '=', 'installed'),
- ])
- cls.write(modules_installed + [module], {
- 'state': 'to upgrade',
- })
+ modules_installed.update((m for m in get_childs(module)
+ if m.state == 'installed'))
+ cls.write(list(modules_installed), {
+ 'state': 'to upgrade',
+ })
@classmethod
@ModelView.button
+ @filter_state('to install')
def install_cancel(cls, modules):
cls.write(modules, {
'state': 'uninstalled',
@@ -208,6 +217,7 @@ class Module(ModelSQL, ModelView):
@classmethod
@ModelView.button
+ @filter_state('installed')
def uninstall(cls, modules):
cursor = Transaction().cursor
for module in modules:
@@ -227,11 +237,13 @@ class Module(ModelSQL, ModelView):
@classmethod
@ModelView.button
+ @filter_state('to remove')
def uninstall_cancel(cls, modules):
cls.write(modules, {'state': 'installed'})
@classmethod
@ModelView.button
+ @filter_state('to upgrade')
def upgrade_cancel(cls, modules):
cls.write(modules, {'state': 'installed'})
@@ -378,6 +390,11 @@ class ModuleConfigWizardOther(ModelView):
return 100.0 * done / all
+class ModuleConfigWizardDone(ModelView):
+ 'Module Config Wizard Done'
+ __name__ = 'ir.module.module.config_wizard.done'
+
+
class ModuleConfigWizard(Wizard):
'Run config wizards'
__name__ = 'ir.module.module.config_wizard'
@@ -414,6 +431,10 @@ class ModuleConfigWizard(Wizard):
Button('Next', 'action', 'tryton-go-next', default=True),
])
action = ConfigStateAction()
+ done = StateView('ir.module.module.config_wizard.done',
+ 'ir.module_config_wizard_done_view_form', [
+ Button('Ok', 'end', 'tryton-close', default=True),
+ ])
def transition_start(self):
res = self.transition_action()
@@ -429,7 +450,10 @@ class ModuleConfigWizard(Wizard):
])
if items:
return 'other'
- return 'end'
+ return 'done'
+
+ def end(self):
+ return 'reload menu'
class ModuleInstallUpgradeStart(ModelView):
diff --git a/trytond/ir/module/module.xml b/trytond/ir/module/module.xml
index 33a716a..8dc897f 100644
--- a/trytond/ir/module/module.xml
+++ b/trytond/ir/module/module.xml
@@ -89,6 +89,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">module_config_wizard_other_form</field>
</record>
+ <record model="ir.ui.view" id="module_config_wizard_done_view_form">
+ <field name="model">ir.module.module.config_wizard.done</field>
+ <field name="type">form</field>
+ <field name="name">module_config_wizard_done_form</field>
+ </record>
+
<record model="ir.action.wizard" id="act_module_install_upgrade">
<field name="name">Perform Pending Installation/Upgrade</field>
<field name="wiz_name">ir.module.module.install_upgrade</field>
diff --git a/trytond/ir/rule.py b/trytond/ir/rule.py
index 99c0f14..479f162 100644
--- a/trytond/ir/rule.py
+++ b/trytond/ir/rule.py
@@ -1,6 +1,5 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
-import contextlib
import time
import datetime
@@ -86,8 +85,8 @@ class RuleGroup(ModelSQL, ModelView):
return res
@classmethod
- def write(cls, groups, vals):
- super(RuleGroup, cls).write(groups, vals)
+ def write(cls, groups, vals, *args):
+ super(RuleGroup, cls).write(groups, vals, *args)
# Restart the cache on the domain_get method of ir.rule
Pool().get('ir.rule')._domain_get_cache.clear()
@@ -215,8 +214,8 @@ class Rule(ModelSQL, ModelView):
clause_global = {}
ctx = cls._get_context()
# Use root user without context to prevent recursion
- with contextlib.nested(Transaction().set_user(0),
- Transaction().set_context(user=0)):
+ with Transaction().set_user(0), \
+ Transaction().set_context(user=0):
for rule in cls.browse(ids):
assert rule.domain, ('Rule domain empty,'
'check if migration was done')
@@ -255,8 +254,8 @@ class Rule(ModelSQL, ModelView):
clause = ['AND', clause_global, clause]
# Use root to prevent infinite recursion
- with contextlib.nested(Transaction().set_user(0),
- Transaction().set_context(active_test=False, user=0)):
+ with Transaction().set_user(0), \
+ Transaction().set_context(active_test=False, user=0):
query = obj.search(clause, order=[], query=True)
cls._domain_get_cache.set(key, query)
@@ -276,7 +275,7 @@ class Rule(ModelSQL, ModelView):
return res
@classmethod
- def write(cls, rules, vals):
- super(Rule, cls).write(rules, vals)
+ def write(cls, rules, vals, *args):
+ super(Rule, cls).write(rules, vals, *args)
# Restart the cache on the domain_get method
cls._domain_get_cache.clear()
diff --git a/trytond/ir/sequence.py b/trytond/ir/sequence.py
index 8637f48..d1b3c73 100644
--- a/trytond/ir/sequence.py
+++ b/trytond/ir/sequence.py
@@ -184,12 +184,13 @@ class Sequence(ModelSQL, ModelView):
return sequences
@classmethod
- def write(cls, sequences, values):
- result = super(Sequence, cls).write(sequences, values)
+ def write(cls, sequences, values, *args):
+ super(Sequence, cls).write(sequences, values, *args)
if sql_sequence and not cls._strict:
- for sequence in sequences:
- sequence.update_sql_sequence(values.get('number_next'))
- return result
+ actions = iter((sequences, values) + args)
+ for sequences, values in zip(actions, actions):
+ for sequence in sequences:
+ sequence.update_sql_sequence(values.get('number_next'))
@classmethod
def delete(cls, sequences):
diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py
index e6a3f57..17e7079 100644
--- a/trytond/ir/translation.py
+++ b/trytond/ir/translation.py
@@ -1,6 +1,5 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
-import contextlib
try:
import cStringIO as StringIO
except ImportError:
@@ -10,10 +9,7 @@ import polib
import xml.dom.minidom
from difflib import SequenceMatcher
import os
-try:
- from hashlib import md5
-except ImportError:
- from md5 import md5
+from hashlib import md5
from lxml import etree
from itertools import izip
from sql import Column
@@ -731,13 +727,17 @@ class Translation(ModelSQL, ModelView):
return super(Translation, cls).create(vlist)
@classmethod
- def write(cls, translations, vals):
+ def write(cls, translations, values, *args):
cls._translation_cache.clear()
ModelView._fields_view_get_cache.clear()
- if 'src' in vals:
- vals = vals.copy()
- vals['src_md5'] = cls.get_src_md5(vals.get('src'))
- return super(Translation, cls).write(translations, vals)
+ actions = iter((translations, values) + args)
+ args = []
+ for translations, values in zip(actions, actions):
+ if 'src' in values:
+ values = values.copy()
+ values['src_md5'] = cls.get_src_md5(values.get('src'))
+ args.extend((translations, values))
+ return super(Translation, cls).write(*args)
@classmethod
def extra_model_data(cls, model_data):
@@ -750,6 +750,25 @@ class Translation(ModelSQL, ModelView):
):
yield 'ir.action'
+ @property
+ def unique_key(self):
+ if self.type in ('odt', 'view', 'wizard_button', 'selection', 'error'):
+ return (self.name, self.res_id, self.type, self.src)
+ elif self.type in ('field', 'model', 'help'):
+ return (self.name, self.res_id, self.type)
+
+ @classmethod
+ def from_poentry(cls, entry):
+ 'Returns a translation instance for a entry of pofile and its res_id'
+ ttype, name, res_id = entry.msgctxt.split(':')
+ src = entry.msgid
+ value = entry.msgstr
+ fuzzy = 'fuzzy' in entry.flags
+
+ translation = cls(name=name, type=ttype, src=src, fuzzy=fuzzy,
+ value=value)
+ return translation, res_id
+
@classmethod
def translation_import(cls, lang, module, po_path):
pool = Pool()
@@ -778,20 +797,15 @@ class Translation(ModelSQL, ModelView):
('module', '=', module),
], order=[])
for translation in module_translations:
- if translation.type in ('odt', 'view', 'wizard_button',
- 'selection', 'error'):
- key = (translation.name, translation.res_id, translation.type,
- translation.src)
- elif translation.type in ('field', 'model', 'help'):
- key = (translation.name, translation.res_id, translation.type)
- else:
- raise Exception('Unknow translation type: %s'
- % translation.type)
+ key = translation.unique_key
+ if not key:
+ raise ValueError('Unknow translation type: %s' %
+ translation.type)
key2ids.setdefault(key, []).append(translation.id)
if len(module_translations) <= RECORD_CACHE_SIZE:
id2translation[translation.id] = translation
- def override_translation(ressource_id, new_translation, src, fuzzy):
+ def override_translation(ressource_id, new_translation):
res_id_module, res_id = ressource_id.split('.')
if res_id:
model_data, = ModelData.search([
@@ -801,23 +815,23 @@ class Translation(ModelSQL, ModelView):
res_id = model_data.db_id
else:
res_id = -1
- with contextlib.nested(Transaction().set_user(0),
- Transaction().set_context(module=res_id_module)):
+ with Transaction().set_user(0), \
+ Transaction().set_context(module=res_id_module):
domain = [
- ('name', '=', name),
+ ('name', '=', new_translation.name),
('res_id', '=', res_id),
- ('lang', '=', lang),
- ('type', '=', ttype),
+ ('lang', '=', new_translation.lang),
+ ('type', '=', new_translation.type),
('module', '=', res_id_module),
]
- if ttype in ('odt', 'view', 'wizard_button', 'selection',
- 'error'):
- domain.append(('src', '=', src))
+ if new_translation.type in ('odt', 'view', 'wizard_button',
+ 'selection', 'error'):
+ domain.append(('src', '=', new_translation.src))
translation, = cls.search(domain)
- if translation.value != new_translation:
- translation.value = new_translation
+ if translation.value != new_translation.value:
+ translation.value = new_translation.value
translation.overriding_module = module
- translation.fuzzy = fuzzy
+ translation.fuzzy = new_translation.fuzzy
translation.save()
# Make a first loop to retreive translation ids in the right order to
@@ -832,17 +846,16 @@ class Translation(ModelSQL, ModelView):
id2translation = dict((t.id, t)
for t in cls.browse(translation_ids))
for entry in pofile:
- ttype, name, res_id = entry.msgctxt.split(':')
- src = entry.msgid
- value = entry.msgstr
- fuzzy = 'fuzzy' in entry.flags
+ translation, res_id = cls.from_poentry(entry)
+ translation.lang = lang
+ translation.module = module
noupdate = False
if '.' in res_id:
- override_translation(res_id, value, ttype, fuzzy)
+ override_translation(res_id, translation)
continue
- model = name.split(',')[0]
+ model = translation.name.split(',')[0]
if (model in fs_id2prop
and res_id in fs_id2prop[model]):
res_id, noupdate = fs_id2prop[model][res_id]
@@ -851,53 +864,44 @@ class Translation(ModelSQL, ModelView):
try:
res_id = int(res_id)
except ValueError:
- continue
+ res_id = None
if not res_id:
res_id = -1
- if ttype in ('odt', 'view', 'wizard_button', 'selection',
- 'error'):
- key = (name, res_id, ttype, src)
- elif ttype in('field', 'model', 'help'):
- key = (name, res_id, ttype)
- else:
- raise Exception('Unknow translation type: %s' % ttype)
+ translation.res_id = res_id
+ key = translation.unique_key
+ if not key:
+ raise ValueError('Unknow translation type: %s' %
+ translation.type)
ids = key2ids.get(key, [])
if not processing:
translation_ids.extend(ids)
continue
- with contextlib.nested(Transaction().set_user(0),
- Transaction().set_context(module=module)):
- if not ids:
- to_create.append({
- 'name': name,
- 'res_id': res_id,
- 'lang': lang,
- 'type': ttype,
- 'src': src,
- 'value': value,
- 'fuzzy': fuzzy,
- 'module': module,
- 'overriding_module': None,
- })
- else:
- translations2 = []
- for translation_id in ids:
- translation = id2translation[translation_id]
- if translation.value != value \
- or translation.fuzzy != fuzzy:
- translations2.append(translation)
- if translations2 and not noupdate:
- cls.write(translations2, {
- 'value': value,
- 'fuzzy': fuzzy,
- })
+ if not ids:
+ to_create.append(translation._save_values)
+ else:
+ to_write = []
+ for translation_id in ids:
+ old_translation = id2translation[translation_id]
+ if (old_translation.value != translation.value
+ or old_translation.fuzzy !=
+ translation.fuzzy):
+ to_write.append(old_translation)
+ with Transaction().set_user(0), \
+ Transaction().set_context(module=module):
+ if to_write and not noupdate:
+ cls.write(to_write, {
+ 'value': translation.value,
+ 'fuzzy': translation.fuzzy,
+ })
translations |= set(cls.browse(ids))
if to_create:
- translations |= set(cls.create(to_create))
+ with Transaction().set_user(0), \
+ Transaction().set_context(module=module):
+ translations |= set(cls.create(to_create))
if translations:
all_translations = set(cls.search([
@@ -1353,7 +1357,6 @@ class TranslationClean(Wizard):
'delete_xml_record',
'xml_record_desc',
'write_xml_record',
- 'not_found_in_selection',
'relation_not_found',
'too_many_relations_found',
'xml_id_syntax_error',
@@ -1377,10 +1380,11 @@ class TranslationClean(Wizard):
return False
if model_name in pool.object_name_list():
Model = pool.get(model_name)
- errors = (Model._error_messages.values()
- + Model._sql_error_messages.values())
- for _, _, error in Model._sql_constraints:
- errors.append(error)
+ errors = Model._error_messages.values()
+ if issubclass(Model, ModelSQL):
+ errors += Model._sql_error_messages.values()
+ for _, _, error in Model._sql_constraints:
+ errors.append(error)
if translation.src not in errors:
return True
elif model_name in pool.object_name_list(type='wizard'):
@@ -1448,6 +1452,10 @@ class TranslationUpdate(Wizard):
"Update translation"
__name__ = "ir.translation.update"
+ _source_types = ['odt', 'view', 'wizard_button', 'selection', 'error']
+ _ressource_types = ['field', 'model', 'help']
+ _updatable_types = ['field', 'model', 'selection', 'help']
+
start = StateView('ir.translation.update.start',
'ir.translation_update_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
@@ -1465,16 +1473,15 @@ class TranslationUpdate(Wizard):
cursor = Transaction().cursor
translation = Translation.__table__()
lang = self.start.language.code
- types = ['odt', 'view', 'wizard_button', 'selection', 'error']
columns = [translation.name.as_('name'),
translation.res_id.as_('res_id'), translation.type.as_('type'),
translation.src.as_('src'), translation.module.as_('module')]
cursor.execute(*(translation.select(*columns,
where=(translation.lang == 'en_US')
- & translation.type.in_(types))
+ & translation.type.in_(self._source_types))
- translation.select(*columns,
where=(translation.lang == lang)
- & translation.type.in_(types))))
+ & translation.type.in_(self._source_types))))
to_create = []
for row in cursor.dictfetchall():
to_create.append({
@@ -1488,16 +1495,15 @@ class TranslationUpdate(Wizard):
if to_create:
with Transaction().set_user(0):
Translation.create(to_create)
- types = ['field', 'model', 'help']
columns = [translation.name.as_('name'),
translation.res_id.as_('res_id'), translation.type.as_('type'),
translation.module.as_('module')]
cursor.execute(*(translation.select(*columns,
where=(translation.lang == 'en_US')
- & translation.type.in_(types))
+ & translation.type.in_(self._ressource_types))
- translation.select(*columns,
where=(translation.lang == lang)
- & translation.type.in_(types))))
+ & translation.type.in_(self._ressource_types))))
to_create = []
for row in cursor.dictfetchall():
to_create.append({
@@ -1510,16 +1516,15 @@ class TranslationUpdate(Wizard):
if to_create:
with Transaction().set_user(0):
Translation.create(to_create)
- types = ['field', 'model', 'selection', 'help']
columns = [translation.name.as_('name'),
translation.res_id.as_('res_id'), translation.type.as_('type'),
- translation.src.as_('src')]
+ translation.src.as_('src'), translation.module.as_('module')]
cursor.execute(*(translation.select(*columns,
where=(translation.lang == 'en_US')
- & translation.type.in_(types))
+ & translation.type.in_(self._updatable_types))
- translation.select(*columns,
where=(translation.lang == lang)
- & translation.type.in_(types))))
+ & translation.type.in_(self._updatable_types))))
for row in cursor.dictfetchall():
cursor.execute(*translation.update(
[translation.fuzzy, translation.src],
@@ -1527,7 +1532,8 @@ class TranslationUpdate(Wizard):
where=(translation.name == row['name'])
& (translation.type == row['type'])
& (translation.lang == lang)
- & (translation.res_id == (row['res_id'] or -1))))
+ & (translation.res_id == (row['res_id'] or -1))
+ & (translation.module == row['module'])))
cursor.execute(*translation.select(
translation.src.as_('src'),
diff --git a/trytond/ir/translation.xml b/trytond/ir/translation.xml
index 5c57717..d6de503 100644
--- a/trytond/ir/translation.xml
+++ b/trytond/ir/translation.xml
@@ -60,15 +60,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
- <record model="ir.translation" id="translation_not_found_in_selection">
- <field name="name">not_found_in_selection</field>
- <field name="lang">en_US</field>
- <field name="type">error</field>
- <field name="src">Key %r not found in selection field %r</field>
- <field name="value">Key %r not found in selection field %r</field>
- <field name="module">ir</field>
- <field name="fuzzy" eval="False"/>
- </record>
<record model="ir.translation" id="translation_relation_not_found">
<field name="name">relation_not_found</field>
<field name="lang">en_US</field>
@@ -109,8 +100,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">domain_validation_record</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">The value of the field "%s" on "%s" is not valid according to its domain.</field>
- <field name="value">The value of the field "%s" on "%s" is not valid according to its domain.</field>
+ <field name="src">The value of the field "%(field)s" on "%(model)s" is not valid according to its domain.</field>
+ <field name="value">The value of the field "%(field)s" on "%(model)s" is not valid according to its domain.</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
@@ -118,8 +109,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">required_validation_record</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">The field "%s" on "%s" is required.</field>
- <field name="value">The field "%s" on "%s" is required.</field>
+ <field name="src">The field "%(field)s" on "%(model)s" is required.</field>
+ <field name="value">The field "%(field)s" on "%(model)s" is required.</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
@@ -127,8 +118,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">size_validation_record</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">The field "%s" on "%s" is too long.</field>
- <field name="value">The field "%s" on "%s" is too long.</field>
+ <field name="src">The size "%(size)s" of the field "%(field)s" on "%(model)s" is too long.</field>
+ <field name="value">The size "%(size)s" of the field "%(field)s" on "(model)%s" is too long.</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
@@ -136,8 +127,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">digits_validation_record</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">The field "%s" on "%s" has too many decimal digits.</field>
- <field name="value">The field "%s" on "%s" has too many decimal digits.</field>
+ <field name="src">The number of digits "%(digits)s" of field "%(field)s" on "%(value)s" exceeds it's limit.</field>
+ <field name="value">The number of digits "%(digits)s" of field "%(field)s" on "%(value)s" exceeds it's limit.</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
@@ -146,8 +137,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">selection_validation_record</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">The field "%s" on "%s" is not in the selection.</field>
- <field name="value">The field "%s" on "%s" is not in the selection.</field>
+ <field name="src">The value "%(value)s" of field "%(field)s" on "%(model)s" is not in the selection.</field>
+ <field name="value">The value "%(value)s" of field "%(field)s" on "%(model)s" is not in the selection.</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
@@ -155,8 +146,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">time_format_validation_record</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">The time value of field "%s" on "%s" is not valid.</field>
- <field name="value">The time value of field "%s" on "%s" is not valid.</field>
+ <field name="src">The time value "%(value)s" of field "%(field)s" on "%(model)s" is not valid.</field>
+ <field name="value">The time value "%(value)s" of field "%(field)s" on "%(model)s" is not valid.</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
@@ -197,8 +188,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">required_field</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">The field "%s" on "%s" is required.</field>
- <field name="value">The field "%s" on "%s" is required.</field>
+ <field name="src">The field "%(field)s" on "%(model)s" is required.</field>
+ <field name="value">The field "%(field)s" on "%(model)s" is required.</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
@@ -206,8 +197,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">foreign_model_missing</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">The value of field "%s" on "%s" doesn't exist.</field>
- <field name="value">The value of field "%s" on "%s" doesn't exist.</field>
+ <field name="src">The value "%(value)s" of field "%(field)s" on "%(model)s" doesn't exist.</field>
+ <field name="value">The value "%(value)s" of field "%(field)s" on "%(model)s" doesn't exist.</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
@@ -215,8 +206,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">foreign_model_exist</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">Could not delete "%s" records because they are used on field "%s" of "%s".</field>
- <field name="value">Could not delete "%s" records because they are used on field "%s" of "%s".</field>
+ <field name="src">Could not delete the records because they are used on field "%(field)s" of "%(model)s".</field>
+ <field name="value">Could not delete the records because they are used on field "%(field)s" of "%(model)s".</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
diff --git a/trytond/ir/trigger.py b/trytond/ir/trigger.py
index c04842d..56dbddd 100644
--- a/trytond/ir/trigger.py
+++ b/trytond/ir/trigger.py
@@ -29,20 +29,16 @@ class Trigger(ModelSQL, ModelView):
'invisible': (Eval('on_create', False)
| Eval('on_write', False)
| Eval('on_delete', False)),
- }, depends=['on_create', 'on_write', 'on_delete'],
- on_change=['on_time'])
+ }, depends=['on_create', 'on_write', 'on_delete'])
on_create = fields.Boolean('On Create', select=True, states={
'invisible': Eval('on_time', False),
- }, depends=['on_time'],
- on_change=['on_create'])
+ }, depends=['on_time'])
on_write = fields.Boolean('On Write', select=True, states={
'invisible': Eval('on_time', False),
- }, depends=['on_time'],
- on_change=['on_write'])
+ }, depends=['on_time'])
on_delete = fields.Boolean('On Delete', select=True, states={
'invisible': Eval('on_time', False),
- }, depends=['on_time'],
- on_change=['on_delete'])
+ }, depends=['on_time'])
condition = fields.Char('Condition', required=True,
help='A Python statement evaluated with record represented by '
'"self"\nIt triggers the action if true.')
@@ -97,6 +93,7 @@ class Trigger(ModelSQL, ModelView):
def default_limit_number():
return 0
+ @fields.depends('on_time')
def on_change_on_time(self):
if self.on_time:
return {
@@ -106,6 +103,7 @@ class Trigger(ModelSQL, ModelView):
}
return {}
+ @fields.depends('on_create')
def on_change_on_create(self):
if self.on_create:
return {
@@ -113,6 +111,7 @@ class Trigger(ModelSQL, ModelView):
}
return {}
+ @fields.depends('on_write')
def on_change_on_write(self):
if self.on_write:
return {
@@ -120,6 +119,7 @@ class Trigger(ModelSQL, ModelView):
}
return {}
+ @fields.depends('on_delete')
def on_change_on_delete(self):
if self.on_delete:
return {
@@ -270,11 +270,10 @@ class Trigger(ModelSQL, ModelView):
return res
@classmethod
- def write(cls, ids, values):
- res = super(Trigger, cls).write(ids, values)
+ def write(cls, triggers, values, *args):
+ super(Trigger, cls).write(triggers, values, *args)
# Restart the cache on the get_triggers method of ir.trigger
cls._get_triggers_cache.clear()
- return res
@classmethod
def delete(cls, records):
diff --git a/trytond/ir/ui/form.rnc b/trytond/ir/ui/form.rnc
index f696e9e..5943d85 100644
--- a/trytond/ir/ui/form.rnc
+++ b/trytond/ir/ui/form.rnc
@@ -41,29 +41,32 @@ attlist.field &= [ a:defaultValue = "1" ] attribute colspan { text }?
attlist.field &=
attribute widget {
"date"
- | "char"
- | "many2one"
- | "date"
- | "one2many"
- | "selection"
+ | "datetime"
+ | "time"
| "float"
| "numeric"
- | "float_time"
| "integer"
- | "datetime"
+ | "selection"
+ | "char"
+ | "password"
+ | "float_time"
| "boolean"
+ | "reference"
+ | "binary"
| "text"
- | "url"
+ | "one2many"
+ | "many2many"
+ | "many2one"
| "email"
+ | "url"
| "callto"
| "sip"
- | "sha"
- | "reference"
- | "binary"
- | "many2many"
| "image"
| "progressbar"
+ | "one2one"
| "richtext"
+ | "dict"
+ | "multiselection"
}?
attlist.field &= attribute fill { "0" | "1" }?
attlist.field &= attribute yexpand { "0" | "1" }?
@@ -105,6 +108,7 @@ attlist.field &= attribute icon { text }?
attlist.field &= [a:defaultValue = "1"] attribute completion { "0" | "1" }?
attlist.field &= attribute string { text }?
attlist.field &= [a:defaultValue = "1"] attribute factor { text }?
+attlist.field &= attribute filename { text }?
image = element image { attlist.image, empty }
attlist.image &= attribute name { text }
attlist.image &= [ a:defaultValue = "1" ] attribute colspan { text }?
diff --git a/trytond/ir/ui/form.rng b/trytond/ir/ui/form.rng
index 5a68938..87b446c 100644
--- a/trytond/ir/ui/form.rng
+++ b/trytond/ir/ui/form.rng
@@ -140,29 +140,32 @@
<attribute name="widget">
<choice>
<value>date</value>
- <value>char</value>
- <value>many2one</value>
- <value>date</value>
- <value>one2many</value>
- <value>selection</value>
+ <value>datetime</value>
+ <value>time</value>
<value>float</value>
<value>numeric</value>
- <value>float_time</value>
<value>integer</value>
- <value>datetime</value>
+ <value>selection</value>
+ <value>char</value>
+ <value>password</value>
+ <value>float_time</value>
<value>boolean</value>
+ <value>reference</value>
+ <value>binary</value>
<value>text</value>
- <value>url</value>
+ <value>one2many</value>
+ <value>many2many</value>
+ <value>many2one</value>
<value>email</value>
+ <value>url</value>
<value>callto</value>
<value>sip</value>
- <value>sha</value>
- <value>reference</value>
- <value>binary</value>
- <value>many2many</value>
<value>image</value>
<value>progressbar</value>
+ <value>one2one</value>
<value>richtext</value>
+ <value>dict</value>
+ <value>multiselection</value>
</choice>
</attribute>
</optional>
@@ -384,6 +387,11 @@
<attribute name="factor" a:defaultValue="1"/>
</optional>
</define>
+ <define name="attlist.field" combine="interleave">
+ <optional>
+ <attribute name="filename"/>
+ </optional>
+ </define>
<define name="image">
<element name="image">
<ref name="attlist.image"/>
diff --git a/trytond/ir/ui/menu.py b/trytond/ir/ui/menu.py
index edb7039..2d3b45e 100644
--- a/trytond/ir/ui/menu.py
+++ b/trytond/ir/ui/menu.py
@@ -136,10 +136,12 @@ class UIMenu(ModelSQL, ModelView):
self.raise_user_error('wrong_name', (self.name,))
def get_rec_name(self, name):
- if self.parent:
- return self.parent.get_rec_name(name) + SEPARATOR + self.name
- else:
- return self.name
+ parent = self.parent
+ name = self.name
+ while parent:
+ name = parent.name + SEPARATOR + name
+ parent = parent.parent
+ return name
@classmethod
def search_rec_name(cls, name, clause):
diff --git a/trytond/ir/ui/tree.rnc b/trytond/ir/ui/tree.rnc
index e422d93..3500bc8 100644
--- a/trytond/ir/ui/tree.rnc
+++ b/trytond/ir/ui/tree.rnc
@@ -21,12 +21,14 @@ attlist.field &=
| "many2one"
| "date"
| "one2many"
+ | "many2many"
| "selection"
| "float"
| "numeric"
| "float_time"
| "integer"
| "datetime"
+ | "time"
| "boolean"
| "text"
| "url"
@@ -34,6 +36,9 @@ attlist.field &=
| "callto"
| "sip"
| "progressbar"
+ | "reference"
+ | "one2one"
+ | "binary"
}?
attlist.field &=
[ a:defaultValue = "0" ] attribute tree_invisible { "0" | "1" }?
@@ -53,6 +58,7 @@ attlist.field &= attribute float_time { text }?
attlist.field &= [a:defaultValue = "0"] attribute pre_validate { "0" | "1" }?
attlist.field &= [a:defaultValue = "1"] attribute completion { "0" | "1" }?
attlist.field &= [a:defaultValue = "1"] attribute factor { text }?
+attlist.field &= attribute filename { text }?
prefix = element prefix { attlist.affix, empty }
suffix = element suffix { attlist.affix, empty }
attlist.affix &= attribute string { text }?
@@ -64,6 +70,8 @@ attlist.button &=
[ a:defaultValue = "Unknown" ] attribute string { text }?
attlist.button &= attribute confirm { text }?
attlist.button &= attribute name { text }
+attlist.button &=
+ [ a:defaultValue = "0" ] attribute tree_invisible { "0" | "1" }?
data = element data { attlist.data, xpath+ }
attlist.data &= empty
xpath = element xpath { attlist.xpath,
diff --git a/trytond/ir/ui/tree.rng b/trytond/ir/ui/tree.rng
index 12e9ef3..85ecc64 100644
--- a/trytond/ir/ui/tree.rng
+++ b/trytond/ir/ui/tree.rng
@@ -83,12 +83,14 @@
<value>many2one</value>
<value>date</value>
<value>one2many</value>
+ <value>many2many</value>
<value>selection</value>
<value>float</value>
<value>numeric</value>
<value>float_time</value>
<value>integer</value>
<value>datetime</value>
+ <value>time</value>
<value>boolean</value>
<value>text</value>
<value>url</value>
@@ -96,6 +98,9 @@
<value>callto</value>
<value>sip</value>
<value>progressbar</value>
+ <value>reference</value>
+ <value>one2one</value>
+ <value>binary</value>
</choice>
</attribute>
</optional>
@@ -177,6 +182,11 @@
<attribute name="factor" a:defaultValue="1"/>
</optional>
</define>
+ <define name="attlist.field" combine="interleave">
+ <optional>
+ <attribute name="filename"/>
+ </optional>
+ </define>
<define name="prefix">
<element name="prefix">
<ref name="attlist.affix"/>
@@ -228,6 +238,16 @@
<define name="attlist.button" combine="interleave">
<attribute name="name"/>
</define>
+ <define name="attlist.button" combine="interleave">
+ <optional>
+ <attribute name="tree_invisible" a:defaultValue="0">
+ <choice>
+ <value>0</value>
+ <value>1</value>
+ </choice>
+ </attribute>
+ </optional>
+ </define>
<define name="data">
<element name="data">
<ref name="attlist.data"/>
diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py
index 614d48f..5e16398 100644
--- a/trytond/ir/ui/view.py
+++ b/trytond/ir/ui/view.py
@@ -196,8 +196,8 @@ class View(ModelSQL, ModelView):
return views
@classmethod
- def write(cls, views, vals):
- super(View, cls).write(views, vals)
+ def write(cls, views, values, *args):
+ super(View, cls).write(views, values, *args)
# Restart the cache
ModelView._fields_view_get_cache.clear()
@@ -260,8 +260,8 @@ class ViewTreeWidth(ModelSQL, ModelView):
return res
@classmethod
- def write(cls, records, vals):
- super(ViewTreeWidth, cls).write(records, vals)
+ def write(cls, records, values, *args):
+ super(ViewTreeWidth, cls).write(records, values, *args)
ModelView._fields_view_get_cache.clear()
@classmethod
diff --git a/trytond/ir/view/action_act_window_form.xml b/trytond/ir/view/action_act_window_form.xml
index 1b14858..2cef84f 100644
--- a/trytond/ir/view/action_act_window_form.xml
+++ b/trytond/ir/view/action_act_window_form.xml
@@ -28,8 +28,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="search_value" colspan="5"/>
<label name="limit"/>
<field name="limit"/>
- <label name="auto_refresh"/>
- <field name="auto_refresh"/>
<label name="window_name"/>
<field name="window_name"/>
</page>
diff --git a/trytond/ir/view/module_config_wizard_done_form.xml b/trytond/ir/view/module_config_wizard_done_form.xml
new file mode 100644
index 0000000..9163601
--- /dev/null
+++ b/trytond/ir/view/module_config_wizard_done_form.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form string="Module configuration" col="2">
+ <image name="tryton-dialog-information" xexpand="0"
+ xfill="0"/>
+ <label string="The configuration is done." id="done"
+ yalign="0.0" xalign="0.0" xexpand="1"/>
+</form>
diff --git a/trytond/model/fields/char.py b/trytond/model/fields/char.py
index 64f1c1f..1168cac 100644
--- a/trytond/model/fields/char.py
+++ b/trytond/model/fields/char.py
@@ -1,16 +1,13 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
+import warnings
+
from sql import Query, Expression
from ...config import CONFIG
from .field import Field, FieldTranslate, size_validate, SQLType
-def autocomplete_validate(value):
- if value:
- assert isinstance(value, list), 'autocomplete must be a list'
-
-
class Char(FieldTranslate):
'''
Define a char field (``unicode``).
@@ -31,22 +28,16 @@ class Char(FieldTranslate):
readonly=readonly, domain=domain, states=states, select=select,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
- self.__autocomplete = None
- self.autocomplete = autocomplete if autocomplete else None
+ self.autocomplete = set()
+ if autocomplete:
+ warnings.warn('autocomplete argument is deprecated, use the '
+ 'depends decorator', DeprecationWarning, stacklevel=2)
+ self.autocomplete |= set(autocomplete)
self.translate = translate
self.__size = None
self.size = size
__init__.__doc__ += Field.__init__.__doc__
- def _get_autocomplete(self):
- return self.__autocomplete
-
- def _set_autocomplete(self, value):
- autocomplete_validate(value)
- self.__autocomplete = value
-
- autocomplete = property(_get_autocomplete, _set_autocomplete)
-
def _get_size(self):
return self.__size
diff --git a/trytond/model/fields/field.py b/trytond/model/fields/field.py
index 4a47616..b20a204 100644
--- a/trytond/model/fields/field.py
+++ b/trytond/model/fields/field.py
@@ -1,6 +1,9 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from collections import namedtuple
+import warnings
+from functools import wraps
+
from sql import operators, Column, Literal, Select, CombiningQuery
from sql.conditionals import Coalesce, NullIf
from sql.operators import Concat
@@ -45,16 +48,6 @@ def states_validate(value):
'values of states must return boolean'
-def on_change_validate(value):
- if value:
- assert isinstance(value, list), 'on_change must be a list'
-
-
-def on_change_with_validate(value):
- if value:
- assert isinstance(value, list), 'on_change_with must be a list'
-
-
def depends_validate(value):
assert isinstance(value, list), 'depends must be a list'
@@ -70,6 +63,31 @@ def size_validate(value):
assert value.types() == set([int]), \
'size must return integer'
+
+def depends(*fields, **kwargs):
+ methods = kwargs.pop('methods', None)
+ assert not kwargs
+
+ def decorator(func):
+ depends = getattr(func, 'depends', set())
+ depends |= set(fields)
+ setattr(func, 'depends', depends)
+
+ if methods:
+ depend_methods = getattr(func, 'depend_methods', set())
+ depend_methods |= set(methods)
+ setattr(func, 'depend_methods', depend_methods)
+
+ @wraps(func)
+ def wrapper(self, *args, **kwargs):
+ for field in fields:
+ if not hasattr(self, field):
+ setattr(self, field, None)
+ return func(self, *args, **kwargs)
+ return wrapper
+ return decorator
+
+
SQL_OPERATORS = {
'=': operators.Equal,
'!=': operators.NotEqual,
@@ -128,10 +146,18 @@ class Field(object):
self.__states = None
self.states = states or {}
self.select = bool(select)
- self.__on_change = None
- self.on_change = on_change
- self.__on_change_with = None
- self.on_change_with = on_change_with
+ self.on_change = set()
+ if on_change:
+ warnings.warn('on_change argument is deprecated, '
+ 'use the depends decorator',
+ DeprecationWarning, stacklevel=3)
+ self.on_change |= set(on_change)
+ self.on_change_with = set()
+ if on_change_with:
+ warnings.warn('on_change_with argument is deprecated, '
+ 'use the depends decorator',
+ DeprecationWarning, stacklevel=3)
+ self.on_change_with |= set(on_change_with)
self.__depends = None
self.depends = depends or []
self.__context = None
@@ -159,23 +185,6 @@ class Field(object):
states = property(_get_states, _set_states)
- def _get_on_change(self):
- return self.__on_change
-
- def _set_on_change(self, value):
- on_change_validate(value)
- self.__on_change = value
- on_change = property(_get_on_change, _set_on_change)
-
- def _get_on_change_with(self):
- return self.__on_change_with
-
- def _set_on_change_with(self, value):
- on_change_with_validate(value)
- self.__on_change_with = value
-
- on_change_with = property(_get_on_change_with, _set_on_change_with)
-
def _get_depends(self):
return self.__depends
@@ -213,6 +222,9 @@ class Field(object):
def sql_type(self):
raise NotImplementedError
+ def sql_column(self, table):
+ return Column(table, self.name)
+
def _domain_value(self, operator, value):
if isinstance(value, (Select, CombiningQuery)):
return value
@@ -235,8 +247,9 @@ class Field(object):
"Return a SQL expression for the domain using tables"
table, _ = tables[None]
name, operator, value = domain
+ assert name == self.name
Operator = SQL_OPERATORS[operator]
- column = Column(table, name)
+ column = self.sql_column(table)
expression = Operator(column, self._domain_value(operator, value))
if isinstance(expression, operators.In) and not expression.right:
expression = Literal(False)
@@ -247,17 +260,18 @@ class Field(object):
def convert_order(self, name, tables, Model):
"Return a SQL expression to order"
+ assert name == self.name
table, _ = tables[None]
method = getattr(Model, 'order_%s' % name, None)
if method:
return method(tables)
else:
- return [Column(table, name)]
+ return [self.sql_column(table)]
class FieldTranslate(Field):
- def __get_translation_join(self, Model, name,
+ def _get_translation_join(self, Model, name,
translation, model, table):
language = Transaction().language
if Model.__name__ == 'ir.model':
@@ -302,11 +316,12 @@ class FieldTranslate(Field):
translation = Translation.__table__()
model = IrModel.__table__()
name, operator, value = domain
- join = self.__get_translation_join(Model, name,
+ join = self._get_translation_join(Model, name,
translation, model, table)
Operator = SQL_OPERATORS[operator]
+ assert name == self.name
column = Coalesce(NullIf(translation.value, ''),
- Column(table, name))
+ self.sql_column(table))
where = Operator(column, self._domain_value(operator, value))
if isinstance(where, operators.In) and not where.right:
where = Literal(False)
@@ -322,13 +337,14 @@ class FieldTranslate(Field):
if not self.translate:
return super(FieldTranslate, self).convert_order(name, tables,
Model)
+ assert name == self.name
table, _ = tables[None]
key = name + '.translation'
if key not in tables:
translation = Translation.__table__()
model = IrModel.__table__()
- join = self.__get_translation_join(Model, name,
+ join = self._get_translation_join(Model, name,
translation, model, table)
tables[key] = {
None: (join.right, join.condition),
@@ -336,6 +352,7 @@ class FieldTranslate(Field):
else:
translation, _ = tables[key][None]
- return [Coalesce(NullIf(translation.value, ''), Column(table, name))]
+ return [Coalesce(NullIf(translation.value, ''),
+ self.sql_column(table))]
SQLType = namedtuple('SQLType', 'base type')
diff --git a/trytond/model/fields/function.py b/trytond/model/fields/function.py
index 2ebc511..d5489b7 100644
--- a/trytond/model/fields/function.py
+++ b/trytond/model/fields/function.py
@@ -91,12 +91,16 @@ class Function(Field):
name = [name]
return call(name)
- def set(self, ids, Model, name, value):
+ def set(self, Model, name, ids, value, *args):
'''
Call the setter.
'''
if self.setter:
- getattr(Model, self.setter)(Model.browse(ids), name, value)
+ # TODO change setter API to use sequence of records, value
+ setter = getattr(Model, self.setter)
+ args = iter((ids, value) + args)
+ for ids, value in zip(args, args):
+ setter(Model.browse(ids), name, value)
def __set__(self, inst, value):
self._field.__set__(inst, value)
diff --git a/trytond/model/fields/many2many.py b/trytond/model/fields/many2many.py
index 8a94044..03e6b09 100644
--- a/trytond/model/fields/many2many.py
+++ b/trytond/model/fields/many2many.py
@@ -1,7 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from itertools import chain
-from sql import Cast, Literal, Column
+from sql import Cast, Literal
from sql.functions import Substring, Position
from .field import Field, size_validate
@@ -95,128 +95,129 @@ class Many2Many(Field):
res[origin_id].append(getattr(relation, self.target).id)
return dict((key, tuple(value)) for key, value in res.iteritems())
- def set(self, ids, model, name, values):
+ def set(self, Model, name, ids, values, *args):
'''
Set the values.
values: A list of tuples:
(``create``, ``[{<field name>: value}, ...]``),
- (``write``, ``<ids>``, ``{<field name>: value}``),
+ (``write``, [``<ids>``, ``{<field name>: value}``, ...]),
(``delete``, ``<ids>``),
- (``delete_all``),
- (``unlink``, ``<ids>``),
+ (``remove``, ``<ids>``),
(``add``, ``<ids>``),
- (``unlink_all``),
- (``set``, ``<ids>``)
+ (``copy``, ``<ids>``, ``[{<field name>: value}, ...]``)
'''
pool = Pool()
- if not values:
- return
Relation = pool.get(self.relation_name)
Target = self.get_target()
origin_field = Relation._fields[self.origin]
+ relation_to_create = []
+ relation_to_delete = []
+ target_to_write = []
+ target_to_delete = []
def search_clause(ids):
if origin_field._type == 'reference':
- references = ['%s,%s' % (model.__name__, x) for x in ids]
+ references = ['%s,%s' % (Model.__name__, x) for x in ids]
return (self.origin, 'in', references)
else:
return (self.origin, 'in', ids)
def field_value(record_id):
if origin_field._type == 'reference':
- return '%s,%s' % (model.__name__, record_id)
+ return '%s,%s' % (Model.__name__, record_id)
else:
return record_id
- for act in values:
- if act[0] == 'create':
- to_create = []
- for record_id in ids:
- for new in Target.create(act[1]):
- to_create.append({
- self.origin: field_value(record_id),
- self.target: new.id,
- })
- if to_create:
- Relation.create(to_create)
- elif act[0] == 'write':
- Target.write(Target.browse(act[1]), act[2])
- elif act[0] == 'delete':
- Target.delete(Target.browse(act[1]))
- elif act[0] == 'delete_all':
+ def create(ids, vlist):
+ for record_id in ids:
+ for new in Target.create(vlist):
+ relation_to_create.append({
+ self.origin: field_value(record_id),
+ self.target: new.id,
+ })
+
+ def write(_, *args):
+ actions = iter(args)
+ target_to_write.extend(sum(((Target.browse(ids), values)
+ for ids, values in zip(actions, actions)), ()))
+
+ def delete(_, target_ids):
+ target_to_delete.extend(Target.browse(target_ids))
+
+ def add(ids, target_ids):
+ target_ids = map(int, target_ids)
+ if not target_ids:
+ return
+ existing_ids = set()
+ in_max = Transaction().cursor.IN_MAX
+ for i in range(0, len(target_ids), in_max):
+ sub_ids = target_ids[i:i + in_max]
relations = Relation.search([
search_clause(ids),
+ (self.target, 'in', sub_ids),
])
- Target.delete([getattr(r, self.target) for r in relations
- if getattr(r, self.target)])
- elif act[0] == 'unlink':
- if isinstance(act[1], (int, long)):
- target_ids = [act[1]]
- else:
- target_ids = list(act[1])
- if not target_ids:
+ for relation in relations:
+ existing_ids.add(getattr(relation, self.target).id)
+ for new_id in target_ids:
+ if new_id in existing_ids:
continue
- relations = []
- for i in range(0, len(target_ids),
- Transaction().cursor.IN_MAX):
- sub_ids = target_ids[i:i + Transaction().cursor.IN_MAX]
- relations += Relation.search([
- search_clause(ids),
- (self.target, 'in', sub_ids),
- ])
- Relation.delete(relations)
- elif act[0] == 'add':
- target_ids = list(act[1])
- if not target_ids:
- continue
- existing_ids = []
- for i in range(0, len(target_ids),
- Transaction().cursor.IN_MAX):
- sub_ids = target_ids[i:i + Transaction().cursor.IN_MAX]
- relations = Relation.search([
+ for record_id in ids:
+ relation_to_create.append({
+ self.origin: field_value(record_id),
+ self.target: new_id,
+ })
+
+ def remove(ids, target_ids):
+ target_ids = map(int, target_ids)
+ if not target_ids:
+ return
+ in_max = Transaction().cursor.IN_MAX
+ for i in range(0, len(target_ids), in_max):
+ sub_ids = target_ids[i:i + in_max]
+ relation_to_delete.extend(Relation.search([
search_clause(ids),
(self.target, 'in', sub_ids),
- ])
- for relation in relations:
- existing_ids.append(getattr(relation, self.target).id)
- to_create = []
- for new_id in (x for x in target_ids if x not in existing_ids):
- for record_id in ids:
- to_create.append({
- self.origin: field_value(record_id),
- self.target: new_id,
- })
- if to_create:
- Relation.create(to_create)
- elif act[0] == 'unlink_all':
- targets = Relation.search([
- search_clause(ids),
- (self.target + '.id', '!=', None),
- ])
- Relation.delete(targets)
- elif act[0] == 'set':
- if not act[1]:
- target_ids = []
- else:
- target_ids = list(act[1])
- targets2 = Relation.search([
- search_clause(ids),
- (self.target + '.id', '!=', None),
- ])
- Relation.delete(targets2)
-
- to_create = []
- for new_id in target_ids:
- for record_id in ids:
- to_create.append({
- self.origin: field_value(record_id),
- self.target: new_id,
- })
- if to_create:
- Relation.create(to_create)
- else:
- raise Exception('Bad arguments')
+ ]))
+
+ def copy(ids, copy_ids, default=None):
+ copy_ids = map(int, copy_ids)
+
+ if default is None:
+ default = {}
+ default = default.copy()
+ copies = Target.browse(copy_ids)
+ for new in Target.copy(copies, default=default):
+ for record_id in ids:
+ relation_to_create.append({
+ self.origin: field_value(record_id),
+ self.target: new.id,
+ })
+
+ actions = {
+ 'create': create,
+ 'write': write,
+ 'delete': delete,
+ 'add': add,
+ 'remove': remove,
+ 'copy': copy,
+ }
+ args = iter((ids, values) + args)
+ for ids, values in zip(args, args):
+ if not values:
+ continue
+ for value in values:
+ action = value[0]
+ args = value[1:]
+ actions[action](ids, *args)
+ if relation_to_create:
+ Relation.create(relation_to_create)
+ if relation_to_delete:
+ Relation.delete(relation_to_delete)
+ if target_to_write:
+ Target.write(*target_to_write)
+ if target_to_delete:
+ Target.delete(target_to_delete)
def get_target(self):
'Return the target model'
@@ -266,7 +267,7 @@ class Many2Many(Field):
name, operator, value = domain[:3]
origin_field = Relation._fields[self.origin]
- origin = Column(relation, self.origin)
+ origin = getattr(Relation, self.origin).sql_column(relation)
origin_where = None
if origin_field._type == 'reference':
origin_where = origin.like(Model.__name__ + ',%')
@@ -274,13 +275,13 @@ class Many2Many(Field):
Position(',', origin) + Literal(1)),
Relation.id.sql_type().base)
+ target = getattr(Relation, self.target).sql_column(relation)
if '.' not in name:
if operator in ('child_of', 'not child_of'):
if Target != Model:
query = Target.search([(domain[3], 'child_of', value)],
order=[], query=True)
- where = (Column(relation, self.target).in_(query)
- & (Column(relation, self.origin) != None))
+ where = (target.in_(query) & (origin != None))
if origin_where:
where &= origin_where
query = relation.select(origin, where=where)
@@ -323,7 +324,7 @@ class Many2Many(Field):
_, target_name = name.split('.', 1)
target_domain = [(target_name,) + tuple(domain[1:])]
query = Target.search(target_domain, order=[], query=True)
- where = Column(relation, self.target).in_(query)
+ where = target.in_(query)
if origin_where:
where &= origin_where
query = relation.select(origin, where=where)
diff --git a/trytond/model/fields/many2one.py b/trytond/model/fields/many2one.py
index 84f5621..e180475 100644
--- a/trytond/model/fields/many2one.py
+++ b/trytond/model/fields/many2one.py
@@ -1,7 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from types import NoneType
-from sql import Column, Query, Expression
+from sql import Query, Expression
from sql.operators import Or
from .field import Field, SQLType
@@ -99,8 +99,9 @@ class Many2One(Field):
table, _ = tables[None]
name, operator, ids = domain
red_sql = reduce_ids(table.id, ids)
- left = Column(table, self.left)
- right = Column(table, self.right)
+ Target = self.get_target()
+ left = getattr(Target, self.left).sql_column(table)
+ right = getattr(Target, self.right).sql_column(table)
cursor.execute(*table.select(left, right, where=red_sql))
where = Or()
for l, r in cursor.fetchall():
@@ -133,7 +134,7 @@ class Many2One(Field):
Target = self.get_target()
table, _ = tables[None]
name, operator, value = domain[:3]
- column = Column(table, name.split('.', 1)[0])
+ column = self.sql_column(table)
if '.' not in name:
if operator in ('child_of', 'not child_of'):
if Target != Model:
@@ -180,6 +181,7 @@ class Many2One(Field):
def convert_order(self, name, tables, Model):
if getattr(Model, 'order_%s' % name, None):
return super(Many2One, self).convert_order(name, tables, Model)
+ assert name == self.name
Target = self.get_target()
@@ -195,7 +197,7 @@ class Many2One(Field):
if target_tables is None:
target = Target.__table__()
target_tables = {
- None: (target, target.id == Column(table, name)),
+ None: (target, target.id == self.sql_column(table)),
}
tables[name] = target_tables
return ofield.convert_order(oname, target_tables, Target)
diff --git a/trytond/model/fields/numeric.py b/trytond/model/fields/numeric.py
index 38b87d7..359a6a0 100644
--- a/trytond/model/fields/numeric.py
+++ b/trytond/model/fields/numeric.py
@@ -1,7 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from decimal import Decimal
-from sql import Query, Expression
+from sql import Query, Expression, Cast, Literal, Select, CombiningQuery
from ...config import CONFIG
from .field import SQLType
@@ -30,3 +30,25 @@ class Numeric(Float):
if db_type == 'mysql':
return SQLType('DECIMAL', 'DECIMAL(65, 30)')
return SQLType('NUMERIC', 'NUMERIC')
+
+ def sql_column(self, table):
+ column = super(Numeric, self).sql_column(table)
+ db_type = CONFIG['db_type']
+ if db_type == 'sqlite':
+ # Must be casted as Decimal is stored as bytes
+ column = Cast(column, self.sql_type().base)
+ return column
+
+ def _domain_value(self, operator, value):
+ value = super(Numeric, self)._domain_value(operator, value)
+ db_type = CONFIG['db_type']
+ if db_type == 'sqlite':
+ if isinstance(value, (Select, CombiningQuery)):
+ return value
+ # Must be casted as Decimal is adapted to bytes
+ type_ = self.sql_type().base
+ if operator in ('in', 'not in'):
+ return [Cast(Literal(v), type_) for v in value]
+ else:
+ return Cast(Literal(value), type_)
+ return value
diff --git a/trytond/model/fields/one2many.py b/trytond/model/fields/one2many.py
index bb9f451..f9c4f2a 100644
--- a/trytond/model/fields/one2many.py
+++ b/trytond/model/fields/one2many.py
@@ -1,7 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from itertools import chain
-from sql import Cast, Literal, Column
+from sql import Cast, Literal
from sql.functions import Substring, Position
from .field import Field, size_validate
@@ -102,102 +102,110 @@ class One2Many(Field):
res[origin_id].append(target.id)
return dict((key, tuple(value)) for key, value in res.iteritems())
- def set(self, ids, model, name, values):
+ def set(self, Model, name, ids, values, *args):
'''
Set the values.
values: A list of tuples:
(``create``, ``[{<field name>: value}, ...]``),
- (``write``, ``<ids>``, ``{<field name>: value}``),
+ (``write``, [``<ids>``, ``{<field name>: value}``, ...]),
(``delete``, ``<ids>``),
- (``delete_all``),
- (``unlink``, ``<ids>``),
(``add``, ``<ids>``),
- (``unlink_all``),
- (``set``, ``<ids>``)
+ (``remove``, ``<ids>``),
+ (``copy``, ``<ids>``, ``[{<field name>: value}, ...]``)
'''
- if not values:
- return
Target = self.get_target()
field = Target._fields[self.field]
+ to_create = []
+ to_write = []
+ to_delete = []
def search_clause(ids):
if field._type == 'reference':
- references = ['%s,%s' % (model.__name__, x) for x in ids]
+ references = ['%s,%s' % (Model.__name__, x) for x in ids]
return (self.field, 'in', references)
else:
return (self.field, 'in', ids)
def field_value(record_id):
if field._type == 'reference':
- return '%s,%s' % (model.__name__, record_id)
+ return '%s,%s' % (Model.__name__, record_id)
else:
return record_id
- for act in values:
- if act[0] == 'create':
- to_create = []
- for record_id in ids:
- value = field_value(record_id)
- for vals in act[1]:
- vals = vals.copy()
- vals[self.field] = value
- to_create.append(vals)
- if to_create:
- Target.create(to_create)
- elif act[0] == 'write':
- Target.write(Target.browse(act[1]), act[2])
- elif act[0] == 'delete':
- Target.delete(Target.browse(act[1]))
- elif act[0] == 'delete_all':
- targets = Target.search([
- search_clause(ids),
- ])
- Target.delete(targets)
- elif act[0] == 'unlink':
- target_ids = map(int, act[1])
- if not target_ids:
- continue
- targets = Target.search([
- search_clause(ids),
- ('id', 'in', target_ids),
- ])
- Target.write(targets, {
- self.field: None,
- })
- elif act[0] == 'add':
- target_ids = map(int, act[1])
- if not target_ids:
- continue
- for record_id in ids:
- Target.write(Target.browse(target_ids), {
+ def create(ids, vlist):
+ for record_id in ids:
+ value = field_value(record_id)
+ for values in vlist:
+ values = values.copy()
+ values[self.field] = value
+ to_create.append(values)
+
+ def write(_, *args):
+ actions = iter(args)
+ to_write.extend(sum(((Target.browse(ids), values)
+ for ids, values in zip(actions, actions)), ()))
+
+ def delete(_, target_ids):
+ to_delete.extend(Target.browse(target_ids))
+
+ def add(ids, target_ids):
+ target_ids = map(int, target_ids)
+ if not target_ids:
+ return
+ targets = Target.browse(target_ids)
+ for record_id in ids:
+ to_write.extend((targets, {
self.field: field_value(record_id),
- })
- elif act[0] == 'unlink_all':
+ }))
+
+ def remove(ids, target_ids):
+ target_ids = map(int, target_ids)
+ if not target_ids:
+ return
+ in_max = Transaction().cursor.IN_MAX
+ for i in range(0, len(target_ids), in_max):
+ sub_ids = target_ids[i:i + in_max]
targets = Target.search([
search_clause(ids),
+ ('id', 'in', sub_ids),
])
- Target.write(targets, {
- self.field: None,
- })
- elif act[0] == 'set':
- if not act[1]:
- target_ids = [-1]
- else:
- target_ids = map(int, act[1])
- for record_id in ids:
- targets = Target.search([
- search_clause([record_id]),
- ('id', 'not in', target_ids),
- ])
- Target.write(targets, {
+ to_write.extend((targets, {
self.field: None,
- })
- if act[1]:
- Target.write(Target.browse(target_ids), {
- self.field: field_value(record_id),
- })
- else:
- raise Exception('Bad arguments')
+ }))
+
+ def copy(ids, copy_ids, default=None):
+ copy_ids = map(int, copy_ids)
+
+ if default is None:
+ default = {}
+ default = default.copy()
+ copies = Target.browse(copy_ids)
+ for record_id in ids:
+ default[self.field] = field_value(record_id)
+ Target.copy(copies, default=default)
+
+ actions = {
+ 'create': create,
+ 'write': write,
+ 'delete': delete,
+ 'add': add,
+ 'remove': remove,
+ 'copy': copy,
+ }
+ args = iter((ids, values) + args)
+ for ids, values in zip(args, args):
+ if not values:
+ continue
+ for value in values:
+ action = value[0]
+ args = value[1:]
+ actions[action](ids, *args)
+ if to_create:
+ Target.create(to_create)
+ if to_write:
+ Target.write(*to_write)
+ if to_delete:
+ Target.delete(to_delete)
def get_target(self):
'Return the target Model'
@@ -223,7 +231,7 @@ class One2Many(Field):
name, operator, value = domain[:3]
origin_field = Target._fields[self.field]
- origin = Column(target, self.field)
+ origin = getattr(Target, self.field).sql_column(target)
origin_where = None
if origin_field._type == 'reference':
origin_where = origin.like(Model.__name__ + ',%')
diff --git a/trytond/model/fields/one2one.py b/trytond/model/fields/one2one.py
index eea5ac8..7e29afd 100644
--- a/trytond/model/fields/one2one.py
+++ b/trytond/model/fields/one2one.py
@@ -28,30 +28,31 @@ class One2One(Many2Many):
res[i] = vals[0] if vals else False
return res
- def set(self, ids, model, name, value):
+ def set(self, Model, name, ids, value, *args):
'''
Set the values.
-
- :param ids: A list of ids
- :param model: A string with the name of the model
- :param name: A string with the name of the field
- :param value: The id to link
'''
pool = Pool()
Relation = pool.get(self.relation_name)
- relations = Relation.search([
- (self.origin, 'in', ids),
- ])
- Relation.delete(relations)
- if value:
- to_create = []
- for record_id in ids:
- to_create.append({
- self.origin: record_id,
- self.target: value,
- })
- if to_create:
- Relation.create(to_create)
+ to_delete = []
+ to_create = []
+ args = iter((ids, value) + args)
+ for ids, value in zip(args, args):
+ relations = Relation.search([
+ (self.origin, 'in', ids),
+ ])
+ to_delete.extend(relations)
+ if value:
+ to_create = []
+ for record_id in ids:
+ to_create.append({
+ self.origin: record_id,
+ self.target: value,
+ })
+ if to_delete:
+ Relation.delete(to_delete)
+ if to_create:
+ Relation.create(to_create)
def __set__(self, inst, value):
Target = self.get_target()
diff --git a/trytond/model/fields/property.py b/trytond/model/fields/property.py
index 568b845..13737dd 100644
--- a/trytond/model/fields/property.py
+++ b/trytond/model/fields/property.py
@@ -46,23 +46,21 @@ class Property(Function):
Property = pool.get('ir.property')
return Property.get(name, model.__name__, ids)
- def set(self, ids, model, name, value):
+ def set(self, Model, name, ids, value, *args):
'''
Set the property.
-
- :param ids: A list of ids.
- :param model: The model.
- :param name: The name of the field.
- :param value: The value to set.
'''
pool = Pool()
Property = pool.get('ir.property')
- if value is not None:
- prop_value = '%s,%s' % (getattr(self, 'model_name', ''),
- str(value))
- else:
- prop_value = None
- Property.set(name, model.__name__, ids, prop_value)
+ args = iter((ids, value) + args)
+ for ids, value in zip(args, args):
+ if value is not None:
+ prop_value = '%s,%s' % (getattr(self, 'model_name', ''),
+ str(value))
+ else:
+ prop_value = None
+ # TODO change set API to use sequence of records, value
+ Property.set(name, Model.__name__, ids, prop_value)
def convert_domain(self, domain, tables, Model):
pool = Pool()
diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py
index 62c4bad..1e3b378 100644
--- a/trytond/model/fields/reference.py
+++ b/trytond/model/fields/reference.py
@@ -1,8 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
-import contextlib
from types import NoneType
-from sql import Cast, Literal, Column, Query, Expression
+from sql import Cast, Literal, Query, Expression
from sql.functions import Substring, Position
from .field import Field, SQLType
@@ -68,8 +67,8 @@ class Reference(Field):
ref_to_check[ref_model][1].append(i)
# Check if reference ids still exist
- with contextlib.nested(Transaction().set_context(active_test=False),
- Transaction().set_user(0)):
+ with Transaction().set_context(active_test=False), \
+ Transaction().set_user(0):
for ref_model, (ref_ids, ids) in ref_to_check.iteritems():
try:
pool.get(ref_model)
@@ -120,7 +119,8 @@ class Reference(Field):
Target = pool.get(target)
table, _ = tables[None]
name, target_name = name.split('.', 1)
- column = Column(table, name)
+ assert name == self.name
+ column = self.sql_column(table)
target_domain = [(target_name,) + tuple(domain[1:3])
+ tuple(domain[4:])]
if 'active' in Target._fields:
diff --git a/trytond/model/fields/selection.py b/trytond/model/fields/selection.py
index ba44c00..230e153 100644
--- a/trytond/model/fields/selection.py
+++ b/trytond/model/fields/selection.py
@@ -1,6 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
-from sql import Column
+import warnings
+
from sql.conditionals import Case
from ...config import CONFIG
@@ -32,7 +33,12 @@ class Selection(Field):
self.selection = selection.copy()
else:
self.selection = selection
- self.selection_change_with = selection_change_with
+ self.selection_change_with = set()
+ if selection_change_with:
+ warnings.warn('selection_change_with argument is deprecated, '
+ 'use the depends decorator',
+ DeprecationWarning, stacklevel=2)
+ self.selection_change_with |= set(selection_change_with)
self.sort = sort
self.translate_selection = translate
__init__.__doc__ += Field.__init__.__doc__
@@ -47,11 +53,12 @@ class Selection(Field):
if getattr(Model, 'order_%s' % name, None):
return super(Selection, self).convert_order(name, tables, Model)
+ assert name == self.name
table, _ = tables[None]
selections = Model.fields_get([name])[name]['selection']
if not isinstance(selections, (tuple, list)):
selections = getattr(Model, selections)()
- column = Column(table, name)
+ column = self.sql_column(table)
whens = []
for key, value in selections:
whens.append((column == key, value))
diff --git a/trytond/model/model.py b/trytond/model/model.py
index d105038..a6426bf 100644
--- a/trytond/model/model.py
+++ b/trytond/model/model.py
@@ -35,12 +35,39 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
}
cls._error_messages = {}
- # Copy fields
+ if hasattr(cls, '__depend_methods'):
+ cls.__depend_methods = cls.__depend_methods.copy()
+ else:
+ cls.__depend_methods = collections.defaultdict(set)
+
+ # Copy fields and update depends
for attr in dir(cls):
if attr.startswith('_'):
continue
- if isinstance(getattr(cls, attr), fields.Field):
- setattr(cls, attr, copy.deepcopy(getattr(cls, attr)))
+ if not isinstance(getattr(cls, attr), fields.Field):
+ continue
+ field_name = attr
+ field = copy.deepcopy(getattr(cls, field_name))
+ setattr(cls, field_name, field)
+
+ for attribute in ('on_change', 'on_change_with', 'autocomplete',
+ 'selection_change_with'):
+ if attribute == 'selection_change_with':
+ if isinstance(
+ getattr(field, 'selection', None), basestring):
+ function_name = field.selection
+ else:
+ continue
+ else:
+ function_name = '%s_%s' % (attribute, field_name)
+ function = getattr(cls, function_name, None)
+ if function:
+ if getattr(function, 'depends', None):
+ setattr(field, attribute,
+ getattr(field, attribute) | function.depends)
+ if getattr(function, 'depend_methods', None):
+ cls.__depend_methods[(field_name, attribute)] |= \
+ function.depend_methods
@classmethod
def __post_setup__(cls):
@@ -78,9 +105,19 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
for attribute in ('on_change', 'on_change_with', 'autocomplete'):
function_name = '%s_%s' % (attribute, field_name)
- if getattr(field, attribute, False):
+ if getattr(cls, function_name, None):
cls.__rpc__.setdefault(function_name, RPC(instantiate=0))
+ # Update depend on methods
+ for (field_name, attribute), others in (
+ cls.__depend_methods.iteritems()):
+ field = getattr(cls, field_name)
+ for other in others:
+ other_field = getattr(cls, other)
+ setattr(field, attribute,
+ getattr(field, attribute)
+ | getattr(other_field, attribute))
+
# Set name to fields
for name, field in cls._fields.iteritems():
if field.name is None:
@@ -120,10 +157,12 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
Translation.register_error_messages(cls, module_name)
@classmethod
- def default_get(cls, fields_names, with_rec_name=True):
+ def default_get(cls, fields_names, with_rec_name=True,
+ with_on_change=True):
'''
Return a dict with the default values for each field in fields_names.
If with_rec_name is True, rec_name will be added.
+ If with_on_change is True, on_change will be added.
'''
pool = Pool()
Property = pool.get('ir.property')
@@ -147,7 +186,8 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
value[field_name + '.rec_name'] = Target(
value[field_name]).rec_name
- value = cls._default_on_change(value)
+ if with_on_change:
+ value = cls._default_on_change(value)
if not with_rec_name:
for field in value.keys():
if field.endswith('.rec_name'):
@@ -186,6 +226,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
pool = Pool()
Translation = pool.get('ir.translation')
FieldAccess = pool.get('ir.model.field.access')
+ ModelAccess = pool.get('ir.model.access')
#Add translation to cache
language = Transaction().language
@@ -236,10 +277,15 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
'loading',
'filename',
'selection_change_with',
+ 'domain',
):
if getattr(cls._fields[field], arg, None) is not None:
- res[field][arg] = copy.copy(getattr(cls._fields[field],
- arg))
+ value = getattr(cls._fields[field], arg)
+ if isinstance(value, set):
+ value = list(value)
+ else:
+ value = copy.copy(value)
+ res[field][arg] = value
if not accesses.get(field, {}).get('write', True):
res[field]['readonly'] = True
if res[field].get('states') and \
@@ -308,7 +354,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
relation = copy.copy(
cls._fields[field].get_target().__name__)
res[field]['relation'] = relation
- res[field]['domain'] = copy.copy(cls._fields[field].domain)
res[field]['context'] = copy.copy(cls._fields[field].context)
res[field]['create'] = accesses.get(field, {}).get('create',
True)
@@ -318,6 +363,14 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
and hasattr(cls._fields[field], 'field'):
res[field]['relation_field'] = copy.copy(
cls._fields[field].field)
+ if res[field]['type'] == 'many2one':
+ target = cls._fields[field].get_target()
+ for target_name, target_field in target._fields.iteritems():
+ if (target_field._type == 'one2many'
+ and target_field.model_name == cls.__name__
+ and target_field.field == field):
+ res[field]['relation_field'] = target_name
+ break
if res[field]['type'] in ('datetime', 'time'):
res[field]['format'] = copy.copy(cls._fields[field].format)
if res[field]['type'] == 'selection':
@@ -337,11 +390,13 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
if attr in res[field]:
res[field][attr] = encoder.encode(res[field][attr])
- if fields_names:
+ for i in res.keys():
# filter out fields which aren't in the fields_names list
- for i in res.keys():
+ if fields_names:
if i not in fields_names:
del res[i]
+ elif not ModelAccess.check_relation(cls.__name__, i, mode='read'):
+ del res[i]
return res
def on_change_with(self, fieldnames):
diff --git a/trytond/model/modelsingleton.py b/trytond/model/modelsingleton.py
index 266e187..f9ff034 100644
--- a/trytond/model/modelsingleton.py
+++ b/trytond/model/modelsingleton.py
@@ -49,11 +49,15 @@ class ModelSingleton(ModelStorage):
return res
@classmethod
- def write(cls, records, values):
+ def write(cls, records, values, *args):
singleton = cls.get_singleton()
if not singleton:
- return cls.create([values])
- return super(ModelSingleton, cls).write([singleton], values)
+ singleton, = cls.create([values])
+ actions = (records, values) + args
+ args = []
+ for values in actions[1:None:2]:
+ args.extend(([singleton], values))
+ return super(ModelSingleton, cls).write(*args)
@classmethod
def delete(cls, records):
@@ -79,12 +83,13 @@ class ModelSingleton(ModelStorage):
return res
@classmethod
- def default_get(cls, fields_names, with_rec_name=True):
+ def default_get(cls, fields_names, with_rec_name=True,
+ with_on_change=True):
if '_timestamp' in fields_names:
fields_names = list(fields_names)
fields_names.remove('_timestamp')
default = super(ModelSingleton, cls).default_get(fields_names,
- with_rec_name=with_rec_name)
+ with_rec_name=with_rec_name, with_on_change=with_on_change)
singleton = cls.get_singleton()
if singleton:
if with_rec_name:
diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py
index 798a893..58b7dc7 100644
--- a/trytond/model/modelsql.py
+++ b/trytond/model/modelsql.py
@@ -1,9 +1,9 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
-import contextlib
import re
+import datetime
from functools import reduce
-from itertools import islice, izip
+from itertools import islice, izip, chain
from sql import Table, Column, Literal, Desc, Asc, Expression, Flavor
from sql.functions import Now, Extract
@@ -11,7 +11,7 @@ from sql.conditionals import Coalesce
from sql.operators import Or, And, Operator
from sql.aggregate import Count, Max
-from trytond.model import ModelStorage
+from trytond.model import ModelStorage, ModelView
from trytond.model import fields
from trytond import backend
from trytond.tools import reduce_ids
@@ -20,6 +20,7 @@ from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.cache import LRUDict
from trytond.exceptions import ConcurrencyException
+from trytond.rpc import RPC
_RE_UNIQUE = re.compile('UNIQUE\s*\((.*)\)', re.I)
_RE_CHECK = re.compile('CHECK\s*\((.*)\)', re.I)
@@ -39,6 +40,10 @@ class ModelSQL(ModelStorage):
cls._sql_constraints = []
cls._order = [('id', 'ASC')]
cls._sql_error_messages = {}
+ if issubclass(cls, ModelView):
+ cls.__rpc__.update({
+ 'history_revisions': RPC(),
+ })
if not cls._table:
cls._table = cls.__name__.replace('.', '_')
@@ -218,8 +223,10 @@ class ModelSQL(ModelStorage):
if not ((target_records
or (values[field_name] in create_records))
and (values[field_name] not in delete_records)):
+ error_args = cls._get_error_args(field_name)
+ error_args['value'] = values[field_name]
cls.raise_user_error('foreign_model_missing',
- error_args=cls._get_error_args(field_name))
+ error_args=error_args)
for name, _, error in cls._sql_constraints:
if name in exception[0]:
cls.raise_user_error(error)
@@ -228,6 +235,40 @@ class ModelSQL(ModelStorage):
cls.raise_user_error(error)
@classmethod
+ def history_revisions(cls, ids):
+ pool = Pool()
+ ModelAccess = pool.get('ir.model.access')
+ User = pool.get('res.user')
+ cursor = Transaction().cursor
+
+ ModelAccess.check(cls.__name__, 'read')
+
+ table = cls.__table_history__()
+ user = User.__table__()
+ revisions = []
+ in_max = cursor.IN_MAX
+ for i in range(0, len(ids), in_max):
+ sub_ids = ids[i:i + in_max]
+ where = reduce_ids(table.id, sub_ids)
+ cursor.execute(*table.join(user, 'LEFT',
+ Coalesce(table.write_uid, table.create_uid) == user.id)
+ .select(
+ Coalesce(table.write_date, table.create_date),
+ table.id,
+ user.name,
+ where=where))
+ revisions.append(cursor.fetchall())
+ revisions = list(chain(*revisions))
+ revisions.sort(reverse=True)
+ # SQLite uses char for COALESCE
+ if revisions and isinstance(revisions[0][0], basestring):
+ strptime = datetime.datetime.strptime
+ format_ = '%Y-%m-%d %H:%M:%S.%f'
+ revisions = [(strptime(timestamp, format_), id_, name)
+ for timestamp, id_, name in revisions]
+ return revisions
+
+ @classmethod
def __insert_history(cls, ids, deleted=False):
transaction = Transaction()
cursor = transaction.cursor
@@ -263,6 +304,62 @@ class ModelSQL(ModelStorage):
[[id_, Now(), user] for id_ in sub_ids]))
@classmethod
+ def restore_history(cls, ids, datetime):
+ 'Restore record ids from history at the date time'
+ if not cls._history:
+ return
+ transaction = Transaction()
+ cursor = transaction.cursor
+ in_max = cursor.IN_MAX
+ table = cls.__table__()
+ history = cls.__table_history__()
+ columns = []
+ hcolumns = []
+ for fname, field in sorted(cls._fields.iteritems()):
+ if hasattr(field, 'set'):
+ continue
+ columns.append(Column(table, fname))
+ if fname == 'write_uid':
+ hcolumns.append(Literal(transaction.user))
+ elif fname == 'write_date':
+ hcolumns.append(Now())
+ else:
+ hcolumns.append(Column(history, fname))
+
+ to_delete = []
+ to_update = []
+ for id_ in ids:
+ column_datetime = Coalesce(history.write_date, history.create_date)
+ hwhere = (column_datetime <= datetime) & (history.id == id_)
+ horder = column_datetime.desc
+ cursor.execute(*history.select(*hcolumns,
+ where=hwhere, order_by=horder, limit=1))
+ values = cursor.fetchone()
+ if not values:
+ to_delete.append(id_)
+ else:
+ to_update.append(id_)
+ values = list(values)
+ cursor.execute(*table.update(columns, values,
+ where=table.id == id_))
+ rowcount = cursor.rowcount
+ if rowcount == -1 or rowcount is None:
+ cursor.execute(*table.select(table.id,
+ where=table.id == id_))
+ rowcount = len(cursor.fetchall())
+ if rowcount < 1:
+ cursor.execute(*table.insert(columns, [values]))
+
+ if to_delete:
+ for i in range(0, len(to_delete), in_max):
+ sub_ids = to_delete[i:i + in_max]
+ where = reduce_ids(table.id, sub_ids)
+ cursor.execute(*table.delete(where=where))
+ cls.__insert_history(to_delete, True)
+ if to_update:
+ cls.__insert_history(to_update)
+
+ @classmethod
def __check_timestamp(cls, ids):
transaction = Transaction()
cursor = transaction.cursor
@@ -325,7 +422,8 @@ class ModelSQL(ModelStorage):
default.append(f)
if default:
- defaults = cls.default_get(default, with_rec_name=False)
+ defaults = cls.default_get(default, with_rec_name=False,
+ with_on_change=False)
values.update(cls._clean_defaults(defaults))
insert_columns = [table.create_uid, table.create_date]
@@ -356,8 +454,8 @@ class ModelSQL(ModelStorage):
id_new = cursor.lastid()
new_ids.append(id_new)
except DatabaseIntegrityError, exception:
- with contextlib.nested(Transaction().new_cursor(),
- Transaction().set_user(0)):
+ with Transaction().new_cursor(), \
+ Transaction().set_user(0):
cls.__raise_integrity_error(exception, values)
raise
@@ -377,6 +475,7 @@ class ModelSQL(ModelStorage):
set()).update(new_ids)
translation_values = {}
+ fields_to_set = {}
for values, new_id in izip(vlist, new_ids):
for fname, value in values.iteritems():
field = cls._fields[fname]
@@ -385,17 +484,23 @@ class ModelSQL(ModelStorage):
translation_values.setdefault(
'%s,%s' % (cls.__name__, fname), {})[new_id] = value
if hasattr(field, 'set'):
- field.set([new_id], cls, fname, value)
+ fields_to_set.setdefault(fname, []).extend(
+ ([new_id], value))
if translation_values:
for name, translations in translation_values.iteritems():
Translation.set_ids(name, 'model', Transaction().language,
translations.keys(), translations.values())
+ for fname, fargs in fields_to_set.iteritems():
+ field = cls._fields[fname]
+ field.set(cls, fname, *fargs)
+
cls.__insert_history(new_ids)
records = cls.browse(new_ids)
- cls._validate(records)
+ for i in range(0, len(records), RECORD_CACHE_SIZE):
+ cls._validate(records[i:i + RECORD_CACHE_SIZE])
field_names = cls._fields.keys()
cls._update_mptt(field_names, [new_ids] * len(field_names))
@@ -408,12 +513,16 @@ class ModelSQL(ModelStorage):
pool = Pool()
Rule = pool.get('ir.rule')
Translation = pool.get('ir.translation')
+ ModelAccess = pool.get('ir.model.access')
+ if not fields_names:
+ fields_names = []
+ for field_name in cls._fields.keys():
+ if ModelAccess.check_relation(cls.__name__, field_name,
+ mode='read'):
+ fields_names.append(field_name)
super(ModelSQL, cls).read(ids, fields_names=fields_names)
cursor = Transaction().cursor
- if not fields_names:
- fields_names = cls._fields.keys()
-
if not ids:
return []
@@ -449,7 +558,7 @@ class ModelSQL(ModelStorage):
table = cls.__table_history__()
column = Coalesce(table.write_date, table.create_date)
history_clause = (column <= Transaction().context['_datetime'])
- history_order = column.desc
+ history_order = (column.desc, Column(table, '__id').desc)
history_limit = 1
columns = []
@@ -629,94 +738,106 @@ class ModelSQL(ModelStorage):
return result
@classmethod
- def write(cls, records, values):
+ def write(cls, records, values, *args):
DatabaseIntegrityError = backend.get('DatabaseIntegrityError')
transaction = Transaction()
cursor = transaction.cursor
pool = Pool()
Translation = pool.get('ir.translation')
Config = pool.get('ir.configuration')
- ids = map(int, records)
in_max = cursor.IN_MAX
+ assert not len(args) % 2
+ all_records = sum(((records, values) + args)[0:None:2], [])
+ all_ids = [r.id for r in all_records]
+ all_field_names = set()
+
# Call before cursor cache cleaning
trigger_eligibles = cls.trigger_write_get_eligibles(records)
- super(ModelSQL, cls).write(records, values)
-
- if not records:
- return
+ super(ModelSQL, cls).write(records, values, *args)
if cls.table_query():
return
table = cls.__table__()
- values = values.copy()
+ cls.__check_timestamp(all_ids)
- cls.__check_timestamp(ids)
+ fields_to_set = {}
+ actions = iter((records, values) + args)
+ for records, values in zip(actions, actions):
+ ids = [r.id for r in records]
+ values = values.copy()
- # Clean values
- for key in ('create_uid', 'create_date',
- 'write_uid', 'write_date', 'id'):
- if key in values:
- del values[key]
+ # Clean values
+ for key in ('create_uid', 'create_date',
+ 'write_uid', 'write_date', 'id'):
+ if key in values:
+ del values[key]
- columns = [table.write_uid, table.write_date]
- update_values = [transaction.user, Now()]
- store_translation = Transaction().language == Config.get_language()
- for fname, value in values.iteritems():
- field = cls._fields[fname]
- if not hasattr(field, 'set'):
- if not getattr(field, 'translate', False) or store_translation:
- columns.append(Column(table, fname))
- update_values.append(field.sql_format(value))
+ columns = [table.write_uid, table.write_date]
+ update_values = [transaction.user, Now()]
+ store_translation = Transaction().language == Config.get_language()
+ for fname, value in values.iteritems():
+ field = cls._fields[fname]
+ if not hasattr(field, 'set'):
+ if (not getattr(field, 'translate', False)
+ or store_translation):
+ columns.append(Column(table, fname))
+ update_values.append(field.sql_format(value))
- domain = pool.get('ir.rule').domain_get(cls.__name__, mode='write')
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
- red_sql = reduce_ids(table.id, sub_ids)
- where = red_sql
- if domain:
- where &= table.id.in_(domain)
- cursor.execute(*table.select(table.id, where=where))
- rowcount = cursor.rowcount
- if rowcount == -1 or rowcount is None:
- rowcount = len(cursor.fetchall())
- if not rowcount == len({}.fromkeys(sub_ids)):
+ domain = pool.get('ir.rule').domain_get(cls.__name__, mode='write')
+ for i in range(0, len(ids), in_max):
+ sub_ids = ids[i:i + in_max]
+ red_sql = reduce_ids(table.id, sub_ids)
+ where = red_sql
if domain:
- cursor.execute(*table.select(table.id, where=red_sql))
- rowcount = cursor.rowcount
- if rowcount == -1 or rowcount is None:
- rowcount = len(cursor.fetchall())
- if rowcount == len({}.fromkeys(sub_ids)):
- cls.raise_user_error('access_error', cls.__name__)
- cls.raise_user_error('write_error', cls.__name__)
- try:
- cursor.execute(*table.update(columns, update_values,
- where=red_sql))
- except DatabaseIntegrityError, exception:
- with contextlib.nested(Transaction().new_cursor(),
- Transaction().set_user(0)):
- cls.__raise_integrity_error(exception, values,
- values.keys())
- raise
+ where &= table.id.in_(domain)
+ cursor.execute(*table.select(table.id, where=where))
+ rowcount = cursor.rowcount
+ if rowcount == -1 or rowcount is None:
+ rowcount = len(cursor.fetchall())
+ if not rowcount == len({}.fromkeys(sub_ids)):
+ if domain:
+ cursor.execute(*table.select(table.id, where=red_sql))
+ rowcount = cursor.rowcount
+ if rowcount == -1 or rowcount is None:
+ rowcount = len(cursor.fetchall())
+ if rowcount == len({}.fromkeys(sub_ids)):
+ cls.raise_user_error('access_error', cls.__name__)
+ cls.raise_user_error('write_error', cls.__name__)
+ try:
+ cursor.execute(*table.update(columns, update_values,
+ where=red_sql))
+ except DatabaseIntegrityError, exception:
+ with Transaction().new_cursor(), \
+ Transaction().set_user(0):
+ cls.__raise_integrity_error(exception, values,
+ values.keys())
+ raise
- for fname, value in values.iteritems():
- field = cls._fields[fname]
- if (getattr(field, 'translate', False)
- and not hasattr(field, 'set')):
- Translation.set_ids(
- '%s,%s' % (cls.__name__, fname), 'model',
- transaction.language, ids, [value] * len(ids))
- if hasattr(field, 'set'):
- field.set(ids, cls, fname, value)
+ for fname, value in values.iteritems():
+ field = cls._fields[fname]
+ if (getattr(field, 'translate', False)
+ and not hasattr(field, 'set')):
+ Translation.set_ids(
+ '%s,%s' % (cls.__name__, fname), 'model',
+ transaction.language, ids, [value] * len(ids))
+ if hasattr(field, 'set'):
+ fields_to_set.setdefault(fname, []).extend((ids, value))
- cls.__insert_history(ids)
- cls._validate(records)
+ field_names = cls._fields.keys()
+ cls._update_mptt(field_names, [ids] * len(field_names), values)
+ all_field_names |= set(values.keys())
- field_names = cls._fields.keys()
- cls._update_mptt(field_names, [ids] * len(field_names), values)
+ for fname, fargs in fields_to_set.iteritems():
+ field = cls._fields[fname]
+ field.set(cls, fname, *fargs)
+ cls.__insert_history(all_ids)
+ for i in range(0, len(all_records), RECORD_CACHE_SIZE):
+ cls._validate(all_records[i:i + RECORD_CACHE_SIZE],
+ field_names=all_field_names)
cls.trigger_write(trigger_eligibles)
@classmethod
@@ -836,12 +957,9 @@ class ModelSQL(ModelStorage):
if Model.search([
(field_name, 'in', sub_ids),
], order=[]):
- error_args = []
- error_args.append(cls._get_error_args('id')[1])
- error_args.extend(list(
- Model._get_error_args(field_name)))
+ error_args = Model._get_error_args(field_name)
cls.raise_user_error('foreign_model_exist',
- error_args=tuple(error_args))
+ error_args=error_args)
super(ModelSQL, cls).delete(sub_records)
@@ -884,11 +1002,6 @@ class ModelSQL(ModelStorage):
forder = field.convert_order(fname, tables, cls)
order_by.extend((Order(o) for o in forder))
- if type(limit) not in (float, int, long, type(None)):
- raise Exception('Error', 'Wrong limit type (%s)!' % type(limit))
- if type(offset) not in (float, int, long, type(None)):
- raise Exception('Error', 'Wrong offset type (%s)!' % type(offset))
-
main_table, _ = tables[None]
def convert_from(table, tables):
diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py
index 806db49..6bb5753 100644
--- a/trytond/model/modelstorage.py
+++ b/trytond/model/modelstorage.py
@@ -3,10 +3,6 @@
import datetime
import time
-import logging
-import contextlib
-import traceback
-import sys
import csv
import warnings
try:
@@ -17,10 +13,11 @@ except ImportError:
from decimal import Decimal
from itertools import islice, ifilter, chain, izip
from functools import reduce
+from operator import itemgetter
from trytond.model import Model
from trytond.model import fields
-from trytond.tools import safe_eval, reduce_domain
+from trytond.tools import safe_eval, reduce_domain, memoize
from trytond.pyson import PYSONEncoder, PYSONDecoder, PYSON
from trytond.const import OPERATORS, RECORD_CACHE_SIZE, BROWSE_FIELD_TRESHOLD
from trytond.transaction import Transaction
@@ -53,7 +50,8 @@ class ModelStorage(Model):
'create': RPC(readonly=False,
result=lambda r: map(int, r)),
'read': RPC(),
- 'write': RPC(readonly=False, instantiate=0),
+ 'write': RPC(readonly=False,
+ instantiate=slice(0, None, 2)),
'delete': RPC(readonly=False, instantiate=0),
'copy': RPC(readonly=False, instantiate=0,
result=lambda r: map(int, r)),
@@ -120,13 +118,19 @@ class ModelStorage(Model):
ModelAccess = pool.get('ir.model.access')
ModelFieldAccess = pool.get('ir.model.field.access')
+ if not fields_names:
+ fields_names = []
+ for field_name in cls._fields.keys():
+ if ModelAccess.check_relation(cls.__name__, field_name,
+ mode='read'):
+ fields_names.append(field_name)
+
ModelAccess.check(cls.__name__, 'read')
- ModelFieldAccess.check(cls.__name__,
- fields_names or cls._fields.keys(), 'read')
+ ModelFieldAccess.check(cls.__name__, fields_names, 'read')
return []
@classmethod
- def write(cls, records, values):
+ def write(cls, records, values, *args):
'''
Write values on records.
'''
@@ -137,15 +141,21 @@ class ModelStorage(Model):
ModelAccess.check(cls.__name__, 'write')
ModelFieldAccess.check(cls.__name__,
[x for x in values if x in cls._fields], 'write')
- if not cls.check_xml_record(records, values):
- cls.raise_user_error('write_xml_record',
- error_description='xml_record_desc')
+
+ assert not len(args) % 2
+ actions = iter((records, values) + args)
+ all_records = []
+ for records, values in zip(actions, actions):
+ if not cls.check_xml_record(records, values):
+ cls.raise_user_error('write_xml_record',
+ error_description='xml_record_desc')
+ all_records += records
# Increase transaction counter
Transaction().counter += 1
# Clean local cache
- for record in records:
+ for record in all_records:
local_cache = record._local_cache.get(record.id)
if local_cache:
local_cache.clear()
@@ -153,7 +163,7 @@ class ModelStorage(Model):
# Clean cursor cache
for cache in Transaction().cursor.cache.itervalues():
if cls.__name__ in cache:
- for record in records:
+ for record in all_records:
if record.id in cache[cls.__name__]:
cache[cls.__name__][record.id].clear()
@@ -245,7 +255,6 @@ class ModelStorage(Model):
def convert_data(field_defs, data):
data = data.copy()
- data_o2m = {}
for field_name in field_defs:
ftype = field_defs[field_name]['type']
@@ -271,46 +280,26 @@ class ModelStorage(Model):
pass
elif ftype in ('one2many',):
if data[field_name]:
- data_o2m[field_name] = data[field_name]
- data[field_name] = None
+ data[field_name] = [('copy', data[field_name])]
elif ftype == 'many2many':
if data[field_name]:
- data[field_name] = [('set', data[field_name])]
+ data[field_name] = [('add', data[field_name])]
if 'id' in data:
del data['id']
- return data, data_o2m
+ return data
- new_ids = {}
fields_names = [n for n, f in cls._fields.iteritems()
if (not isinstance(f, fields.Function)
or isinstance(f, fields.Property))]
ids = map(int, records)
datas = cls.read(ids, fields_names=fields_names)
field_defs = cls.fields_get(fields_names=fields_names)
- data_ids = []
to_create = []
- o2m_to_create = []
for data in datas:
- data_ids.append(data['id'])
- data, data_o2m = convert_data(field_defs, data)
+ data = convert_data(field_defs, data)
to_create.append(data)
- o2m_to_create.append(data_o2m)
new_records = cls.create(to_create)
- for data_id, new_record, data_o2m in izip(data_ids, new_records,
- o2m_to_create):
- new_ids[data_id] = new_record.id
- for field_name in data_o2m:
- Relation = pool.get(
- field_defs[field_name]['relation'])
- relation_field = field_defs[field_name]['relation_field']
- if relation_field:
- field = Relation._fields[relation_field]
- if field._type == 'reference':
- value = str(new_record)
- else:
- value = new_record.id
- Relation.copy(Relation.browse(data_o2m[field_name]),
- default={relation_field: value})
+ new_ids = dict(izip(ids, map(int, new_records)))
fields_translate = {}
for field_name, field in field_defs.iteritems():
@@ -332,7 +321,7 @@ class ModelStorage(Model):
fields_names=fields_translate.keys() + ['id'])
for data in datas:
data_id = data['id']
- data, _ = convert_data(fields_translate, data)
+ data = convert_data(fields_translate, data)
cls.write([cls(new_ids[data_id])], data)
return cls.browse(new_ids.values())
@@ -525,127 +514,108 @@ class ModelStorage(Model):
'''
Create records for all values in data.
The field names of values must be defined in fields_names.
- It returns a tuple with
- - the number of records imported
- - the last values if failed
- - the exception if failed
- - the warning if failed
'''
pool = Pool()
- def process_lines(data, prefix, fields_def, position=0):
-
- def warn(msgname, *args):
- msg = cls.raise_user_error(msgname, args,
- raise_exception=False)
- logger.warn(msg)
-
- def get_selection(selection, value):
- res = None
- if not isinstance(selection, (tuple, list)):
- selection = getattr(cls, selection)()
- for key, _ in selection:
- if str(key) == value:
- res = key
- break
- if value and not res:
- warn('not_found_in_selection', value, '/'.join(field))
- return res
-
- def get_many2one(relation, value):
- if not value:
- return None
- Relation = pool.get(relation)
- res = Relation.search([
- ('rec_name', '=', value),
+ @memoize(1000)
+ def get_many2one(relation, value):
+ if not value:
+ return None
+ Relation = pool.get(relation)
+ res = Relation.search([
+ ('rec_name', '=', value),
+ ], limit=2)
+ if len(res) < 1:
+ cls.raise_user_error('relation_not_found', (value, relation))
+ elif len(res) > 1:
+ cls.raise_user_error('too_many_relations_found',
+ (value, relation))
+ else:
+ res = res[0].id
+ return res
+
+ @memoize(1000)
+ def get_many2many(relation, value):
+ if not value:
+ return None
+ res = []
+ Relation = pool.get(relation)
+ for word in csv.reader(StringIO.StringIO(value), delimiter=',',
+ quoting=csv.QUOTE_NONE, escapechar='\\').next():
+ res2 = Relation.search([
+ ('rec_name', '=', word),
], limit=2)
- if len(res) < 1:
- warn('relation_not_found', value, relation)
- res = None
- elif len(res) > 1:
- warn('too_many_relations_found', value, relation)
- res = None
+ if len(res2) < 1:
+ cls.raise_user_error('relation_not_found',
+ (word, relation))
+ elif len(res2) > 1:
+ cls.raise_user_error('too_many_relations_found',
+ (word, relation))
else:
- res = res[0].id
- return res
-
- def get_many2many(relation, value):
- if not value:
- return None
- res = []
- Relation = pool.get(relation)
- for word in csv.reader(StringIO.StringIO(value), delimiter=',',
- quoting=csv.QUOTE_NONE, escapechar='\\').next():
- res2 = Relation.search([
- ('rec_name', '=', word),
- ], limit=2)
- if len(res2) < 1:
- warn('relation_not_found', word, relation)
- elif len(res2) > 1:
- warn('too_many_relations_found', word, relation)
- else:
- res.extend(res2)
- if len(res):
- res = [('set', [x.id for x in res])]
- return res
-
- def get_one2one(relation, value):
- return ('set', get_many2one(relation, value))
-
- def get_reference(value):
- if not value:
- return None
+ res.extend(res2)
+ if len(res):
+ res = [('add', [x.id for x in res])]
+ return res
+
+ def get_one2one(relation, value):
+ return ('add', get_many2one(relation, value))
+
+ @memoize(1000)
+ def get_reference(value, field):
+ if not value:
+ return None
+ try:
+ relation, value = value.split(',', 1)
+ except Exception:
+ cls.raise_user_error('reference_syntax_error',
+ (value, '/'.join(field)))
+ Relation = pool.get(relation)
+ res = Relation.search([
+ ('rec_name', '=', value),
+ ], limit=2)
+ if len(res) < 1:
+ cls.raise_user_error('relation_not_found', (value, relation))
+ elif len(res) > 1:
+ cls.raise_user_error('too_many_relations_found',
+ (value, relation))
+ else:
+ res = '%s,%s' % (relation, res[0].id)
+ return res
+
+ @memoize(1000)
+ def get_by_id(value, field):
+ if not value:
+ return None
+ relation = None
+ ftype = fields_def[field[-1][:-3]]['type']
+ if ftype == 'many2many':
+ value = csv.reader(StringIO.StringIO(value), delimiter=',',
+ quoting=csv.QUOTE_NONE, escapechar='\\').next()
+ elif ftype == 'reference':
try:
relation, value = value.split(',', 1)
except Exception:
- warn('reference_syntax_error', value, '/'.join(field))
- return None
- Relation = pool.get(relation)
- res = Relation.search([
- ('rec_name', '=', value),
- ], limit=2)
- if len(res) < 1:
- warn('relation_not_found', value, relation)
- res = None
- elif len(res) > 1:
- warn('too_many_relations_found', value, relation)
- res = None
- else:
- res = '%s,%s' % (relation, res[0].id)
- return res
-
- def get_by_id(value):
- if not value:
- return None
- relation = None
- ftype = fields_def[field[-1][:-3]]['type']
- if ftype == 'many2many':
- value = csv.reader(StringIO.StringIO(value), delimiter=',',
- quoting=csv.QUOTE_NONE, escapechar='\\').next()
- elif ftype == 'reference':
- try:
- relation, value = value.split(',', 1)
- except Exception:
- warn('reference_syntax_error', value, '/'.join(field))
- return None
- value = [value]
- else:
- value = [value]
- res_ids = []
- for word in value:
- try:
- module, xml_id = word.rsplit('.', 1)
- except Exception:
- warn('xml_id_syntax_error', word, '/'.join(field))
- continue
- db_id = ModelData.get_id(module, xml_id)
- res_ids.append(db_id)
- if ftype == 'many2many' and res_ids:
- return [('set', res_ids)]
- elif ftype == 'reference' and res_ids:
- return '%s,%s' % (relation, str(res_ids[0]))
- return res_ids and res_ids[0] or False
+ cls.raise_user_error('reference_syntax_error',
+ (value, '/'.join(field)))
+ value = [value]
+ else:
+ value = [value]
+ res_ids = []
+ for word in value:
+ try:
+ module, xml_id = word.rsplit('.', 1)
+ except Exception:
+ cls.raise_user_error('xml_id_syntax_error',
+ (word, '/'.join(field)))
+ db_id = ModelData.get_id(module, xml_id)
+ res_ids.append(db_id)
+ if ftype == 'many2many' and res_ids:
+ return [('add', res_ids)]
+ elif ftype == 'reference' and res_ids:
+ return '%s,%s' % (relation, str(res_ids[0]))
+ return res_ids and res_ids[0] or False
+ def process_lines(data, prefix, fields_def, position=0):
line = data[position]
row = {}
translate = {}
@@ -660,7 +630,7 @@ class ModelStorage(Model):
is_prefix_len = (len(field) == (prefix_len + 1))
value = line[i]
if is_prefix_len and field[-1].endswith(':id'):
- row[field[0][:-3]] = get_by_id(value)
+ row[field[0][:-3]] = get_by_id(value, field)
elif is_prefix_len and ':lang=' in field[-1]:
field_name, lang = field[-1].split(':lang=')
translate.setdefault(lang, {})[field_name] = value or False
@@ -684,15 +654,13 @@ class ModelStorage(Model):
elif field_type == 'numeric':
res = Decimal(value) if value else None
elif field_type == 'date':
- res = (datetime.date(
- *time.strptime(value, '%Y-%m-%d')[:3])
+ res = (datetime.datetime.strptime(value,
+ '%Y-%m-%d').date()
if value else None)
elif field_type == 'datetime':
- res = (datetime.datetime(
- *time.strptime(value, '%Y-%m-%d %H:%M:%S')[:6])
+ res = (datetime.datetime.strptime(value,
+ '%Y-%m-%d %H:%M:%S')
if value else None)
- elif field_type == 'selection':
- res = get_selection(this_field_def['selection'], value)
elif field_type == 'many2one':
res = get_many2one(this_field_def['relation'], value)
elif field_type == 'many2many':
@@ -700,7 +668,7 @@ class ModelStorage(Model):
elif field_type == 'one2one':
res = get_one2one(this_field_def['relation'], value)
elif field_type == 'reference':
- res = get_reference(value)
+ res = get_reference(value, field)
else:
res = value or None
row[field[-1]] = res
@@ -742,42 +710,25 @@ class ModelStorage(Model):
ModelData = pool.get('ir.model.data')
- # logger for collecting warnings for the client
- logger = logging.Logger("import")
- warning_stream = StringIO.StringIO()
- logger.addHandler(logging.StreamHandler(warning_stream))
-
len_fields_names = len(fields_names)
assert all(len(x) == len_fields_names for x in data)
fields_names = [x.split('/') for x in fields_names]
fields_def = cls.fields_get()
- done = 0
- warning = ''
+ to_create, translations, languages = [], [], set()
while len(data):
- res = {}
- try:
- (res, _, translate) = \
- process_lines(data, [], fields_def)
- warning = warning_stream.getvalue()
- if warning:
- # XXX should raise Exception
- Transaction().cursor.rollback()
- return (-1, res, warning, '')
- new_id, = cls.create([res])
- for lang in translate:
- with Transaction().set_context(language=lang):
- cls.write(new_id, translate[lang])
- except Exception, exp:
- logger = logging.getLogger('import')
- logger.error(exp)
- # XXX should raise Exception
- Transaction().cursor.rollback()
- tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
- warning = '%s\n%s' % (tb_s, warning)
- return (-1, res, exp, warning)
- done += 1
- return (done, 0, 0, 0)
+ (res, _, translate) = \
+ process_lines(data, [], fields_def)
+ to_create.append(res)
+ translations.append(translate)
+ languages.update(translate)
+ new_records = cls.create(to_create)
+ for language in languages:
+ translated = [t.get(language, {}) for t in translations]
+ with Transaction().set_context(language=language):
+ cls.write(*chain(*ifilter(itemgetter(1),
+ izip(([r] for r in new_records), translated))))
+ return len(new_records)
@classmethod
def check_xml_record(cls, records, values):
@@ -863,7 +814,10 @@ class ModelStorage(Model):
def _get_error_args(cls, field_name):
pool = Pool()
ModelField = pool.get('ir.model.field')
- error_args = (field_name, cls.__name__)
+ error_args = {
+ 'field': field_name,
+ 'model': cls.__name__
+ }
if ModelField:
model_fields = ModelField.search([
('name', '=', field_name),
@@ -871,8 +825,10 @@ class ModelStorage(Model):
], limit=1)
if model_fields:
model_field, = model_fields
- error_args = (model_field.field_description,
- model_field.model.name)
+ error_args.update({
+ 'field': model_field.field_description,
+ 'model': model_field.model.name,
+ })
return error_args
@classmethod
@@ -880,7 +836,7 @@ class ModelStorage(Model):
pass
@classmethod
- def _validate(cls, records):
+ def _validate(cls, records, field_names=None):
pool = Pool()
# Ensure that records are readable
with Transaction().set_user(0, set_context=True):
@@ -926,71 +882,70 @@ class ModelStorage(Model):
return True
return False
+ def validate_domain(field):
+ if not field.domain:
+ return
+ if field._type == 'dict':
+ return
+ if field._type in ('many2one', 'one2many'):
+ Relation = pool.get(field.model_name)
+ elif field._type in ('many2many', 'one2one'):
+ Relation = field.get_target()
+ else:
+ Relation = cls
+ if is_pyson(field.domain):
+ pyson_domain = PYSONEncoder().encode(field.domain)
+ for record in records:
+ env = EvalEnvironment(record, cls)
+ env.update(Transaction().context)
+ env['current_date'] = datetime.datetime.today()
+ env['time'] = time
+ env['context'] = Transaction().context
+ env['active_id'] = record.id
+ domain = PYSONDecoder(env).decode(pyson_domain)
+ validate_relation_domain(
+ field, [record], Relation, domain)
+ else:
+ validate_relation_domain(
+ field, records, Relation, field.domain)
+
+ def validate_relation_domain(field, records, Relation, domain):
+ if field._type in ('many2one', 'one2many', 'many2many', 'one2one'):
+ relations = []
+ for record in records:
+ if getattr(record, field.name):
+ if field._type in ('many2one', 'one2one'):
+ relations.append(getattr(record, field.name))
+ else:
+ relations.extend(getattr(record, field.name))
+ else:
+ relations = records
+ if relations:
+ finds = Relation.search(['AND',
+ [('id', 'in', [r.id for r in relations])],
+ domain,
+ ])
+ if set(relations) != set(finds):
+ cls.raise_user_error('domain_validation_record',
+ error_args=cls._get_error_args(field.name))
+
+ field_names = set(field_names or [])
+ function_fields = {name for name, field in cls._fields.iteritems()
+ if isinstance(field, fields.Function)}
ctx_pref['active_test'] = False
with Transaction().set_context(ctx_pref):
for field_name, field in cls._fields.iteritems():
+ depends = set(field.depends)
+ if (field_names
+ and field_name not in field_names
+ and not (depends & field_names)
+ and not (depends & function_fields)):
+ continue
if isinstance(field, fields.Function) and \
not field.setter:
continue
- # validate domain
- if (field._type in
- ('many2one', 'many2many', 'one2many', 'one2one')
- and field.domain):
- if field._type in ('many2one', 'one2many'):
- Relation = pool.get(field.model_name)
- else:
- Relation = field.get_target()
- if is_pyson(field.domain):
- pyson_domain = PYSONEncoder().encode(field.domain)
- for record in records:
- env = EvalEnvironment(record, cls)
- env.update(Transaction().context)
- env['current_date'] = datetime.datetime.today()
- env['time'] = time
- env['context'] = Transaction().context
- env['active_id'] = record.id
- domain = PYSONDecoder(env).decode(pyson_domain)
- relation_ids = []
- if getattr(record, field_name):
- if field._type in ('many2one', 'one2one'):
- relation_ids.append(
- getattr(record, field_name).id)
- else:
- relation_ids.extend(
- [x.id for x in getattr(record,
- field_name)])
- if relation_ids and not Relation.search([
- 'AND',
- [('id', 'in', relation_ids)],
- domain,
- ]):
- cls.raise_user_error(
- 'domain_validation_record',
- error_args=cls._get_error_args(
- field_name))
- else:
- relation_ids = []
- for record in records:
- if getattr(record, field_name):
- if field._type in ('many2one', 'one2one'):
- relation_ids.append(
- getattr(record, field_name).id)
- else:
- relation_ids.extend(
- [x.id for x in getattr(record,
- field_name)])
- if relation_ids:
- finds = Relation.search([
- 'AND',
- [('id', 'in', relation_ids)],
- field.domain,
- ])
- find_ids = map(int, finds)
- if not set(relation_ids) == set(find_ids):
- cls.raise_user_error(
- 'domain_validation_record',
- error_args=cls._get_error_args(
- field_name))
+
+ validate_domain(field)
def required_test(value, field_name):
if (isinstance(value, (type(None), type(False), list,
@@ -1037,16 +992,19 @@ class ModelStorage(Model):
field_size = PYSONDecoder(env).decode(pyson_size)
else:
field_size = field.size
- if (len(getattr(record, field_name) or '')
- > field_size >= 0):
- cls.raise_user_error(
- 'size_validation_record',
- error_args=cls._get_error_args(field_name))
+ size = len(getattr(record, field_name) or '')
+ if (size > field_size >= 0):
+ error_args = cls._get_error_args(field_name)
+ error_args['size'] = size
+ cls.raise_user_error('size_validation_record',
+ error_args=error_args)
def digits_test(value, digits, field_name):
def raise_user_error():
+ error_args = cls._get_error_args(field_name)
+ error_args['digits'] = digits[1]
cls.raise_user_error('digits_validation_record',
- error_args=cls._get_error_args(field_name))
+ error_args=error_args)
if value is None:
return
if isinstance(value, Decimal):
@@ -1098,8 +1056,10 @@ class ModelStorage(Model):
test.add('')
test.add(None)
if value not in test:
+ error_args = cls._get_error_args(field_name)
+ error_args['value'] = value
cls.raise_user_error('selection_validation_record',
- error_args=cls._get_error_args(field_name))
+ error_args=error_args)
def format_test(value, format, field_name):
if not value:
@@ -1108,8 +1068,10 @@ class ModelStorage(Model):
value = value.time()
if value != datetime.datetime.strptime(
value.strftime(format), format).time():
+ error_args = cls._get_error_args(field_name)
+ error_args['value'] = value
cls.raise_user_error('time_format_validation_record',
- error_args=cls._get_error_args(field_name))
+ error_args=error_args)
# validate time format
if (field._type in ('datetime', 'time')
@@ -1154,7 +1116,7 @@ class ModelStorage(Model):
vals2 = obj._clean_defaults(defaults2)
vals[field].append(('create', [vals2]))
elif fld_def._type in ('many2many',):
- vals[field] = [('set', defaults[field])]
+ vals[field] = [('add', defaults[field])]
elif fld_def._type in ('boolean',):
vals[field] = bool(defaults[field])
else:
@@ -1164,15 +1126,16 @@ class ModelStorage(Model):
def __init__(self, id=None, **kwargs):
_ids = kwargs.pop('_ids', None)
_local_cache = kwargs.pop('_local_cache', None)
- super(ModelStorage, self).__init__(id, **kwargs)
self._cursor = Transaction().cursor
self._user = Transaction().user
self._context = Transaction().context
+ if id is not None:
+ id = int(id)
if _ids is not None:
self._ids = _ids
assert id in _ids
else:
- self._ids = [self.id]
+ self._ids = [id]
self._cursor_cache = self._cursor.get_cache(self._context)
@@ -1182,6 +1145,8 @@ class ModelStorage(Model):
self._local_cache = LRUDict(RECORD_CACHE_SIZE)
self._local_cache.counter = Transaction().counter
+ super(ModelStorage, self).__init__(id, **kwargs)
+
@property
def _cache(self):
cache = self._cursor_cache
@@ -1260,10 +1225,17 @@ class ModelStorage(Model):
def filter_(id_):
return (name not in self._cache.get(id_, {})
and name not in self._local_cache.get(id_, {}))
+
+ def unique(ids):
+ s = set()
+ for id_ in ids:
+ if id_ not in s:
+ s.add(id_)
+ yield id_
index = self._ids.index(self.id)
ids = chain(islice(self._ids, index, None),
islice(self._ids, 0, max(index - 1, 0)))
- ids = islice(ifilter(filter_, ids), self._cursor.IN_MAX)
+ ids = islice(unique(ifilter(filter_, ids)), self._cursor.IN_MAX)
def instantiate(field, value, data):
if field._type in ('many2one', 'one2one', 'reference'):
@@ -1304,9 +1276,9 @@ class ModelStorage(Model):
model2ids = {}
model2cache = {}
# Read the data
- with contextlib.nested(Transaction().set_cursor(self._cursor),
- Transaction().set_user(self._user),
- Transaction().set_context(self._context)):
+ with Transaction().set_cursor(self._cursor), \
+ Transaction().set_user(self._user), \
+ Transaction().set_context(self._context):
if self.id in self._cache and name in self._cache[self.id]:
# Use values from cache
ids = islice(chain(islice(self._ids, index, None),
@@ -1360,17 +1332,41 @@ class ModelStorage(Model):
value = value.id
if field._type in ('one2many', 'many2many'):
targets = value
- value = [
- ('set', []),
- ]
+ if self.id >= 0:
+ _values, self._values = self._values, None
+ try:
+ to_remove = [t.id for t in getattr(self, fname)]
+ finally:
+ self._values = _values
+ else:
+ to_remove = []
+ to_add = []
+ to_create = []
+ to_write = []
for target in targets:
if target.id < 0:
- value.append(('create', [target._save_values]))
+ if field._type == 'one2many':
+ # Don't store old target link
+ setattr(target, field.field, None)
+ to_create.append(target._save_values)
else:
- value[0][1].append(target.id)
- if target._save_values:
- value.append(
- ('write', [target.id], target._save_values))
+ if target.id in to_remove:
+ to_remove.remove(target.id)
+ else:
+ to_add.append(target.id)
+ target_values = target._save_values
+ if target_values:
+ to_write.append(
+ ('write', [target.id], target_values))
+ value = []
+ if to_remove:
+ value.append(('remove', to_remove))
+ if to_add:
+ value.append(('add', to_add))
+ if to_create:
+ value.append(('create', to_create))
+ if to_write:
+ value.extend(to_write)
values[fname] = value
return values
@@ -1380,9 +1376,9 @@ class ModelStorage(Model):
self._values = None
if save_values or self.id < 0:
try:
- with contextlib.nested(Transaction().set_cursor(self._cursor),
- Transaction().set_user(self._user),
- Transaction().set_context(self._context)):
+ with Transaction().set_cursor(self._cursor), \
+ Transaction().set_user(self._user), \
+ Transaction().set_context(self._context):
if self.id < 0:
self._ids.remove(self.id)
try:
diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py
index eec4db9..ae853e2 100644
--- a/trytond/model/modelview.py
+++ b/trytond/model/modelview.py
@@ -295,33 +295,9 @@ class ModelView(Model):
fields_to_remove = list(x for x, y in fread_accesses.iteritems()
if not y)
- def check_relation(model, field):
- if field._type in ('one2many', 'many2one'):
- if not ModelAccess.check(field.model_name, mode='read',
- raise_exception=False):
- return False
- if field._type in ('many2many', 'one2one'):
- if (field.target
- and not ModelAccess.check(field.target, mode='read',
- raise_exception=False)):
- return False
- elif (field.relation_name
- and not ModelAccess.check(field.relation_name,
- mode='read', raise_exception=False)):
- return False
- if field._type == 'reference':
- selection = field.selection
- if isinstance(selection, basestring):
- selection = getattr(model, field.selection)()
- for model_name, _ in selection:
- if not ModelAccess.check(model_name, mode='read',
- raise_exception=False):
- return False
- return True
-
# Find relation field without read access
for name, field in cls._fields.iteritems():
- if not check_relation(cls, field):
+ if not ModelAccess.check_relation(cls.__name__, name, mode='read'):
fields_to_remove.append(name)
for name, field in cls._fields.iteritems():
diff --git a/trytond/modules/__init__.py b/trytond/modules/__init__.py
index 6dbebf4..f954fb5 100644
--- a/trytond/modules/__init__.py
+++ b/trytond/modules/__init__.py
@@ -4,7 +4,6 @@ import os
import sys
import itertools
import logging
-import contextlib
from functools import reduce
import imp
import operator
@@ -226,7 +225,10 @@ def load_module_graph(graph, pool, lang=None):
if (is_module_to_install(module)
or package_state in ('to install', 'to upgrade')):
if package_state not in ('to install', 'to upgrade'):
- package_state = 'to install'
+ if package_state == 'installed':
+ package_state = 'to upgrade'
+ else:
+ package_state = 'to install'
for child in package.childs:
module2state[child.name] = package_state
for type in classes.keys():
@@ -362,13 +364,8 @@ def register_classes():
def load_modules(database_name, pool, update=False, lang=None):
res = True
- if not Transaction().cursor:
- contextmanager = Transaction().start(database_name, 0)
- else:
- contextmanager = contextlib.nested(Transaction().new_cursor(),
- Transaction().set_user(0),
- Transaction().reset_context())
- with contextmanager:
+
+ def _load_modules():
cursor = Transaction().cursor
if update:
# Migration from 2.2: workflow module removed
@@ -425,5 +422,15 @@ def load_modules(database_name, pool, update=False, lang=None):
Module = pool.get('ir.module.module')
Module.update_list()
cursor.commit()
+
+ if not Transaction().cursor:
+ with Transaction().start(database_name, 0):
+ _load_modules()
+ else:
+ with Transaction().new_cursor(), \
+ Transaction().set_user(0), \
+ Transaction().reset_context():
+ _load_modules()
+
Cache.resets(database_name)
return res
diff --git a/trytond/protocols/common.py b/trytond/protocols/common.py
index 7c1dca2..959b83a 100644
--- a/trytond/protocols/common.py
+++ b/trytond/protocols/common.py
@@ -4,12 +4,6 @@ import errno
import os
import socket
import threading
-import sys
-import gzip
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
from SocketServer import StreamRequestHandler
@@ -57,91 +51,3 @@ class RegisterHandlerMixin:
self.server.handlers.remove(self)
except KeyError:
pass
-
-
-class GZipRequestHandlerMixin:
-
- if sys.version_info[:2] <= (2, 6):
- # Copy from SimpleXMLRPCServer.py with gzip encoding added
- def do_POST(self):
- """Handles the HTTP POST request.
-
- Attempts to interpret all HTTP POST requests as XML-RPC calls,
- which are forwarded to the server's _dispatch method for handling.
- """
-
- # Check that the path is legal
- if not self.is_rpc_path_valid():
- self.report_404()
- return
-
- try:
- # Get arguments by reading body of request.
- # We read this in chunks to avoid straining
- # socket.read(); around the 10 or 15Mb mark, some platforms
- # begin to have problems (bug #792570).
- max_chunk_size = 10 * 1024 * 1024
- size_remaining = int(self.headers["content-length"])
- L = []
- while size_remaining:
- chunk_size = min(size_remaining, max_chunk_size)
- L.append(self.rfile.read(chunk_size))
- size_remaining -= len(L[-1])
- data = ''.join(L)
-
- data = self.decode_request_content(data)
- if data is None:
- return # response has been sent
-
- # In previous versions of SimpleXMLRPCServer, _dispatch
- # could be overridden in this class, instead of in
- # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
- # check to see if a subclass implements _dispatch and dispatch
- # using that method if present.
- response = self.server._marshaled_dispatch(
- data, getattr(self, '_dispatch', None)
- )
- except Exception: # This should only happen if the module is buggy
- # internal error, report as HTTP server error
- self.send_response(500)
- self.end_headers()
- else:
- # got a valid XML RPC response
- self.send_response(200)
- self.send_header("Content-type", "text/xml")
-
- # Handle gzip encoding
- if ('gzip' in self.headers.get('Accept-Encoding',
- '').split(',')
- and len(response) > self.encode_threshold):
- buffer = StringIO.StringIO()
- output = gzip.GzipFile(mode='wb', fileobj=buffer)
- output.write(response)
- output.close()
- buffer.seek(0)
- response = buffer.getvalue()
- self.send_header('Content-Encoding', 'gzip')
-
- self.send_header("Content-length", str(len(response)))
- self.end_headers()
- self.wfile.write(response)
-
- def decode_request_content(self, data):
- #support gzip encoding of request
- encoding = self.headers.get("content-encoding", "identity").lower()
- if encoding == "identity":
- return data
- if encoding == "gzip":
- f = StringIO.StringIO(data)
- gzf = gzip.GzipFile(mode="rb", fileobj=f)
- try:
- decoded = gzf.read()
- except IOError:
- self.send_response(400, "error decoding gzip content")
- f.close()
- gzf.close()
- return decoded
- else:
- self.send_response(501, "encoding %r not supported" % encoding)
- self.send_header("Content-length", "0")
- self.end_headers()
diff --git a/trytond/protocols/dispatcher.py b/trytond/protocols/dispatcher.py
index 81e8dfc..2d1915d 100644
--- a/trytond/protocols/dispatcher.py
+++ b/trytond/protocols/dispatcher.py
@@ -56,8 +56,6 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
return True
elif method == 'version':
return VERSION
- elif method == 'timezone_get':
- return CONFIG['timezone']
elif method == 'list_lang':
return [
('bg_BG', 'Български'),
@@ -85,17 +83,8 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
elif method == 'list':
if CONFIG['prevent_dblist']:
raise Exception('AccessDenied')
- database = Database().connect()
- Flavor.set(Database.flavor)
- try:
- cursor = database.cursor()
- try:
- res = database.list(cursor)
- finally:
- cursor.close(close=True)
- except Exception:
- res = []
- return res
+ with Transaction().start(None, 0, close=True) as transaction:
+ return transaction.database.list(transaction.cursor)
elif method == 'create':
return create(*args, **kwargs)
elif method == 'restore':
@@ -224,18 +213,13 @@ def create(database_name, password, lang, admin_password):
logger = logging.getLogger('database')
try:
- database = Database().connect()
- cursor = database.cursor(autocommit=True)
- try:
- database.create(cursor, database_name)
- cursor.commit()
- cursor.close(close=True)
- except Exception:
- cursor.close()
- raise
+ with Transaction().start(None, 0, close=True, autocommit=True) \
+ as transaction:
+ transaction.database.create(transaction.cursor, database_name)
+ transaction.cursor.commit()
with Transaction().start(database_name, 0) as transaction:
- database.init(transaction.cursor)
+ Database.init(transaction.cursor)
transaction.cursor.execute(*ir_configuration.insert(
[ir_configuration.language], [[lang]]))
transaction.cursor.commit()
@@ -279,11 +263,11 @@ def drop(database_name, password):
time.sleep(1)
logger = logging.getLogger('database')
- database = Database().connect()
- cursor = database.cursor(autocommit=True)
- try:
+ with Transaction().start(None, 0, close=True, autocommit=True) \
+ as transaction:
+ cursor = transaction.cursor
try:
- database.drop(cursor, database_name)
+ Database.drop(cursor, database_name)
cursor.commit()
except Exception:
logger.error('DROP DB: %s failed' % (database_name,))
@@ -293,8 +277,6 @@ def drop(database_name, password):
else:
logger.info('DROP DB: %s' % (database_name))
Pool.stop(database_name)
- finally:
- cursor.close(close=True)
return True
diff --git a/trytond/protocols/jsonrpc.py b/trytond/protocols/jsonrpc.py
index 201ba41..27ce605 100644
--- a/trytond/protocols/jsonrpc.py
+++ b/trytond/protocols/jsonrpc.py
@@ -3,8 +3,7 @@
from trytond.protocols.sslsocket import SSLSocket
from trytond.protocols.dispatcher import dispatch
from trytond.config import CONFIG
-from trytond.protocols.common import daemon, GZipRequestHandlerMixin, \
- RegisterHandlerMixin
+from trytond.protocols.common import daemon, RegisterHandlerMixin
from trytond.exceptions import UserError, UserWarning, NotLogged, \
ConcurrencyException
import SimpleXMLRPCServer
@@ -38,11 +37,12 @@ def object_hook(dct):
if '__class__' in dct:
if dct['__class__'] == 'datetime':
return datetime.datetime(dct['year'], dct['month'], dct['day'],
- dct['hour'], dct['minute'], dct['second'])
+ dct['hour'], dct['minute'], dct['second'], dct['microsecond'])
elif dct['__class__'] == 'date':
return datetime.date(dct['year'], dct['month'], dct['day'])
elif dct['__class__'] == 'time':
- return datetime.time(dct['hour'], dct['minute'], dct['second'])
+ return datetime.time(dct['hour'], dct['minute'], dct['second'],
+ dct['microsecond'])
elif dct['__class__'] == 'buffer':
return buffer(base64.decodestring(dct['base64']))
elif dct['__class__'] == 'Decimal':
@@ -67,6 +67,7 @@ class JSONEncoder(json.JSONEncoder):
'hour': obj.hour,
'minute': obj.minute,
'second': obj.second,
+ 'microsecond': obj.microsecond,
}
return {'__class__': 'date',
'year': obj.year,
@@ -78,6 +79,7 @@ class JSONEncoder(json.JSONEncoder):
'hour': obj.hour,
'minute': obj.minute,
'second': obj.second,
+ 'microsecond': obj.microsecond,
}
elif isinstance(obj, buffer):
return {'__class__': 'buffer',
@@ -157,8 +159,7 @@ class GenericJSONRPCRequestHandler:
return res
-class SimpleJSONRPCRequestHandler(GZipRequestHandlerMixin,
- RegisterHandlerMixin,
+class SimpleJSONRPCRequestHandler(RegisterHandlerMixin,
GenericJSONRPCRequestHandler,
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler,
SimpleHTTPServer.SimpleHTTPRequestHandler):
@@ -305,34 +306,21 @@ class SimpleJSONRPCServer(SocketServer.TCPServer,
def server_close(self):
SocketServer.TCPServer.server_close(self)
- for handler in self.handlers:
+ for handler in self.handlers.copy():
self.shutdown_request(handler.request)
- if sys.version_info[:2] <= (2, 6):
-
- def shutdown_request(self, request):
- """Called to shutdown and close an individual request."""
- try:
- #explicitly shutdown. socket.close() merely releases
- #the socket and waits for GC to perform the actual close.
- request.shutdown(socket.SHUT_WR)
- except socket.error:
- pass # some platforms may raise ENOTCONN here
- self.close_request(request)
-
class SimpleThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
SimpleJSONRPCServer):
timeout = 1
daemon_threads = True
+ disable_nagle_algorithm = True
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1)
self.socket.setsockopt(socket.SOL_SOCKET,
socket.SO_KEEPALIVE, 1)
- self.socket.setsockopt(socket.IPPROTO_TCP,
- socket.TCP_NODELAY, 1)
SimpleJSONRPCServer.server_bind(self)
diff --git a/trytond/protocols/xmlrpc.py b/trytond/protocols/xmlrpc.py
index 5e13d7d..0625f1e 100644
--- a/trytond/protocols/xmlrpc.py
+++ b/trytond/protocols/xmlrpc.py
@@ -3,8 +3,7 @@
from trytond.protocols.sslsocket import SSLSocket
from trytond.protocols.dispatcher import dispatch
from trytond.config import CONFIG
-from trytond.protocols.common import daemon, GZipRequestHandlerMixin, \
- RegisterHandlerMixin
+from trytond.protocols.common import daemon, RegisterHandlerMixin
from trytond.exceptions import UserError, UserWarning, NotLogged, \
ConcurrencyException
from trytond import security
@@ -50,6 +49,7 @@ def dump_time(self, value, write):
'hour': value.hour,
'minute': value.minute,
'second': value.second,
+ 'microsecond': value.microsecond,
}
self.dump_struct(value, write)
@@ -85,7 +85,8 @@ def end_struct(self, data):
if dct['__class__'] == 'date':
dct = datetime.date(dct['year'], dct['month'], dct['day'])
elif dct['__class__'] == 'time':
- dct = datetime.time(dct['hour'], dct['minute'], dct['second'])
+ dct = datetime.time(dct['hour'], dct['minute'], dct['second'],
+ dct['microsecond'])
elif dct['__class__'] == 'Decimal':
dct = Decimal(dct['decimal'])
self._stack[mark:] = [dct]
@@ -145,8 +146,7 @@ class GenericXMLRPCRequestHandler:
security.logout(database_name, user, session)
-class SimpleXMLRPCRequestHandler(GZipRequestHandlerMixin,
- RegisterHandlerMixin,
+class SimpleXMLRPCRequestHandler(RegisterHandlerMixin,
GenericXMLRPCRequestHandler,
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
protocol_version = "HTTP/1.1"
@@ -202,21 +202,9 @@ class SimpleThreadedXMLRPCServer(SocketServer.ThreadingMixIn,
def server_close(self):
SimpleXMLRPCServer.SimpleXMLRPCServer.server_close(self)
- for handler in self.handlers:
+ for handler in self.handlers.copy():
self.shutdown_request(handler.request)
- if sys.version_info[:2] <= (2, 6):
-
- def shutdown_request(self, request):
- """Called to shutdown and close an individual request."""
- try:
- #explicitly shutdown. socket.close() merely releases
- #the socket and waits for GC to perform the actual close.
- request.shutdown(socket.SHUT_WR)
- except socket.error:
- pass # some platforms may raise ENOTCONN here
- self.close_request(request)
-
class SimpleThreadedXMLRPCServer6(SimpleThreadedXMLRPCServer):
address_family = socket.AF_INET6
diff --git a/trytond/pyson.py b/trytond/pyson.py
index 1fc1e64..6771f20 100644
--- a/trytond/pyson.py
+++ b/trytond/pyson.py
@@ -537,6 +537,32 @@ class DateTime(Date):
)
+class Len(PYSON):
+
+ def __init__(self, value):
+ super(Len, self).__init__()
+ if isinstance(value, PYSON):
+ assert value.types().issubset(set([dict, list, str])), \
+ 'value must be a dict or a list or a string'
+ else:
+ assert type(value) in [dict, list, str], \
+ 'value must be a dict or list or a string'
+ self._value = value
+
+ def pyson(self):
+ return {
+ '__class__': 'Len',
+ 'v': self._value,
+ }
+
+ def types(self):
+ return set([int, long])
+
+ @staticmethod
+ def eval(dct, context):
+ return len(dct['v'])
+
+
class Id(PYSON):
"""The database id for filesystem id"""
@@ -567,4 +593,5 @@ CONTEXT = {
'In': In,
'Date': Date,
'DateTime': DateTime,
+ 'Len': Len,
}
diff --git a/trytond/report/report.py b/trytond/report/report.py
index 2536b1e..42a6d5d 100644
--- a/trytond/report/report.py
+++ b/trytond/report/report.py
@@ -122,8 +122,9 @@ class Report(URLMixin, PoolBase):
raise Exception('Error', 'Report (%s) not find!' % cls.__name__)
action_report = action_reports[0]
records = None
- if action_report.model:
- records = cls._get_records(ids, action_report.model, data)
+ model = action_report.model or data.get('model')
+ if model:
+ records = cls._get_records(ids, model, data)
type, data = cls.parse(action_report, records, data, {})
return (type, buffer(data), action_report.direct_print,
action_report.name)
diff --git a/trytond/res/group.py b/trytond/res/group.py
index 2d4ce47..800b13d 100644
--- a/trytond/res/group.py
+++ b/trytond/res/group.py
@@ -82,11 +82,15 @@ class Group(ModelSQL, ModelView):
pool.get('res.user')._get_groups_cache.clear()
# Restart the cache for get_preferences
pool.get('res.user')._get_preferences_cache.clear()
+ # Restart the cache for model access and view
+ pool.get('ir.model.access')._get_access_cache.clear()
+ pool.get('ir.model.field.access')._get_access_cache.clear()
+ ModelView._fields_view_get_cache.clear()
return res
@classmethod
- def write(cls, groups, vals):
- super(Group, cls).write(groups, vals)
+ def write(cls, groups, values, *args):
+ super(Group, cls).write(groups, values, *args)
pool = Pool()
# Restart the cache on the domain_get method
pool.get('ir.rule')._domain_get_cache.clear()
@@ -94,6 +98,10 @@ class Group(ModelSQL, ModelView):
pool.get('res.user')._get_groups_cache.clear()
# Restart the cache for get_preferences
pool.get('res.user')._get_preferences_cache.clear()
+ # Restart the cache for model access and view
+ pool.get('ir.model.access')._get_access_cache.clear()
+ pool.get('ir.model.field.access')._get_access_cache.clear()
+ ModelView._fields_view_get_cache.clear()
@classmethod
def delete(cls, groups):
@@ -105,6 +113,10 @@ class Group(ModelSQL, ModelView):
pool.get('res.user')._get_groups_cache.clear()
# Restart the cache for get_preferences
pool.get('res.user')._get_preferences_cache.clear()
+ # Restart the cache for model access and view
+ pool.get('ir.model.access')._get_access_cache.clear()
+ pool.get('ir.model.field.access')._get_access_cache.clear()
+ ModelView._fields_view_get_cache.clear()
class Group2:
diff --git a/trytond/res/ir.py b/trytond/res/ir.py
index 784ca53..a867611 100644
--- a/trytond/res/ir.py
+++ b/trytond/res/ir.py
@@ -44,8 +44,8 @@ class UIMenuGroup(ModelSQL):
return res
@classmethod
- def write(cls, records, vals):
- super(UIMenuGroup, cls).write(records, vals)
+ def write(cls, records, values, *args):
+ super(UIMenuGroup, cls).write(records, values, *args)
# Restart the cache on the domain_get method
Pool().get('ir.rule')._domain_get_cache.clear()
@@ -91,12 +91,16 @@ class ActionGroup(ModelSQL):
return res
@classmethod
- def write(cls, records, vals):
+ def write(cls, records, values, *args):
Action = Pool().get('ir.action')
- if vals.get('action'):
- vals = vals.copy()
- vals['action'] = Action.get_action_id(vals['action'])
- super(ActionGroup, cls).write(records, vals)
+ actions = iter((records, values) + args)
+ args = []
+ for records, values in zip(actions, actions):
+ if values.get('action'):
+ values = values.copy()
+ values['action'] = Action.get_action_id(values['action'])
+ args.extend((records, values))
+ super(ActionGroup, cls).write(*args)
# Restart the cache on the domain_get method
Pool().get('ir.rule')._domain_get_cache.clear()
@@ -153,9 +157,9 @@ class ModelButtonGroup(ModelSQL):
return result
@classmethod
- def write(cls, records, values):
+ def write(cls, records, values, *args):
pool = Pool()
- super(ModelButtonGroup, cls).write(records, values)
+ super(ModelButtonGroup, cls).write(records, values, *args)
# Restart the cache for get_groups
pool.get('ir.model.button')._groups_cache.clear()
@@ -219,8 +223,8 @@ class Lang:
__name__ = 'ir.lang'
@classmethod
- def write(cls, langs, vals):
- super(Lang, cls).write(langs, vals)
+ def write(cls, langs, values, *args):
+ super(Lang, cls).write(langs, values, *args)
# Restart the cache for get_preferences
Pool().get('res.user')._get_preferences_cache.clear()
@@ -256,9 +260,9 @@ class SequenceTypeGroup(ModelSQL):
return res
@classmethod
- def write(cls, records, vals):
+ def write(cls, records, values, *args):
Rule = Pool().get('ir.rule')
- super(SequenceTypeGroup, cls).write(records, vals)
+ super(SequenceTypeGroup, cls).write(records, values, *args)
# Restart the cache on the domain_get method
Rule._domain_get_cache.clear()
@@ -314,10 +318,10 @@ class ModuleConfigWizardItem:
return result
@classmethod
- def write(cls, items, values):
+ def write(cls, items, values, *args):
pool = Pool()
User = pool.get('res.user')
- super(ModuleConfigWizardItem, cls).write(items, values)
+ super(ModuleConfigWizardItem, cls).write(items, values, *args)
# Restart the cache for get_preferences
User._get_preferences_cache.clear()
diff --git a/trytond/res/ir.xml b/trytond/res/ir.xml
index 45c6557..73c8d96 100644
--- a/trytond/res/ir.xml
+++ b/trytond/res/ir.xml
@@ -787,9 +787,9 @@ this repository contains the full copyright notices and license terms. -->
<field name="request_user" ref="user_admin"/>
<field name="user" ref="user_trigger"/>
<field name="active" eval="True"/>
- <field name="interval_number">5</field>
+ <field name="interval_number" eval="5"/>
<field name="interval_type">minutes</field>
- <field name="number_calls">-1</field>
+ <field name="number_calls" eval="-1"/>
<field name="repeat_missed" eval="False"/>
<field name="model">ir.trigger</field>
<field name="function">trigger_time</field>
diff --git a/trytond/res/locale/bg_BG.po b/trytond/res/locale/bg_BG.po
index a68b490..83777ba 100644
--- a/trytond/res/locale/bg_BG.po
+++ b/trytond/res/locale/bg_BG.po
@@ -365,6 +365,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr "Парола"
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr ""
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr ""
@@ -377,10 +381,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr "Правила"
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr "Ключ"
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr ""
@@ -393,10 +393,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr "Лента със статус"
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr "Времева зона"
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr "Предупреждения"
diff --git a/trytond/res/locale/ca_ES.po b/trytond/res/locale/ca_ES.po
index fd2c2f7..71decc4 100644
--- a/trytond/res/locale/ca_ES.po
+++ b/trytond/res/locale/ca_ES.po
@@ -12,7 +12,7 @@ msgstr "Contrasenya incorrecta."
msgctxt "error:res.user:"
msgid "You can not have two users with the same login!"
-msgstr "No podeu tenir dos usuaris amb el mateix identificador d'entrada!"
+msgstr "No podeu tenir dos usuaris amb el mateix nom d'usuari."
msgctxt "error:res.user:"
msgid ""
@@ -20,9 +20,9 @@ msgid ""
"as it is used internally for resources\n"
"created by the system (updates, module installation, ...)"
msgstr ""
-"No podeu eliminar l'usuari del sistema\n"
-"que es utilitzat per recursos del sistema\n"
-"(actualitzacions, instal·lacions,...)"
+"No podeu eliminar l'usuari principal que\n"
+"s'utilitza internament pels recursos creats\n"
+"pel sistema (actualitzacions, instal·lacions,...)."
msgctxt "field:ir.action-res.group,action:"
msgid "Action"
@@ -62,7 +62,7 @@ msgstr "Actiu"
msgctxt "field:ir.model.button-res.group,button:"
msgid "Button"
-msgstr "Butons"
+msgstr "Botó"
msgctxt "field:ir.model.button-res.group,create_date:"
msgid "Create Date"
@@ -274,7 +274,7 @@ msgstr "Usuari creació"
msgctxt "field:res.group,field_access:"
msgid "Access Field"
-msgstr "Camp accés"
+msgstr "Accés a camps"
msgctxt "field:res.group,id:"
msgid "ID"
@@ -282,11 +282,11 @@ msgstr "ID"
msgctxt "field:res.group,menu_access:"
msgid "Access Menu"
-msgstr "Permís menú"
+msgstr "Accés a menús"
msgctxt "field:res.group,model_access:"
msgid "Access Model"
-msgstr "Permís model"
+msgstr "Accés a models"
msgctxt "field:res.group,name:"
msgid "Name"
@@ -330,7 +330,7 @@ msgstr "Usuari creació"
msgctxt "field:res.user,email:"
msgid "Email"
-msgstr "Email"
+msgstr "Correu electrònic"
msgctxt "field:res.user,groups:"
msgid "Groups"
@@ -364,9 +364,13 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr "Contrasenya"
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr "Hash de la contrasenya"
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
-msgstr "PySON Menú"
+msgstr "Menú PySON"
msgctxt "field:res.user,rec_name:"
msgid "Name"
@@ -376,10 +380,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr "Regles"
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr "Salt"
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr "Sessions"
@@ -392,10 +392,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr "Progrés"
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr "Zona horaria"
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr "Alertes"
@@ -542,11 +538,11 @@ msgstr "Usuari modificació"
msgctxt "help:ir.sequence.type,groups:"
msgid "Groups allowed to edit the sequences of this type"
-msgstr "Grups permesos per editar les seqüències d'aquest tipus"
+msgstr "Els grups permesos per editar les seqüències d'aquest tipus."
msgctxt "help:res.user,actions:"
msgid "Actions that will be run at login"
-msgstr "Accions que seràn executades al identificar-se"
+msgstr "Les accions que s'executaran quan s'identifiqui."
msgctxt "model:ir.action,name:act_group_form"
msgid "Groups"
@@ -566,11 +562,11 @@ msgstr "Acció - grup"
msgctxt "model:ir.cron,name:cron_trigger_time"
msgid "Run On Time Triggers"
-msgstr "Executa al moment del disparador"
+msgstr "Executa disparadors per temps"
msgctxt "model:ir.model.button-res.group,name:"
msgid "Model Button - Group"
-msgstr "Model butó - grup"
+msgstr "Model Botó - Grup"
msgctxt "model:ir.model.field-res.group,name:"
msgid "Model Field Group Rel"
@@ -602,7 +598,7 @@ msgstr "Usuaris"
msgctxt "model:ir.ui.menu-res.group,name:"
msgid "UI Menu - Group"
-msgstr "UI menú - Grup"
+msgstr "Menú UI - Grup"
msgctxt "model:res.group,name:"
msgid "Group"
@@ -618,11 +614,11 @@ msgstr "Usuari"
msgctxt "model:res.user,name:user_admin"
msgid "Administrator"
-msgstr "Administració"
+msgstr "Administrador"
msgctxt "model:res.user,name:user_trigger"
msgid "Cron Trigger"
-msgstr "Cron disparador"
+msgstr "Planificador de disparadors"
msgctxt "model:res.user-ir.action,name:"
msgid "User - Action"
@@ -634,7 +630,7 @@ msgstr "Usuari - Grup"
msgctxt "model:res.user.config.start,name:"
msgid "User Config Init"
-msgstr "Configuració usuari"
+msgstr "Configuració inicial usuari"
msgctxt "model:res.user.login.attempt,name:"
msgid "Login Attempt"
@@ -646,7 +642,7 @@ msgstr "Alerta usuari"
msgctxt "view:res.group:"
msgid "Access Permissions"
-msgstr "Accés permisos"
+msgstr "Permisos d'accés"
msgctxt "view:res.group:"
msgid "Group"
@@ -666,7 +662,7 @@ msgstr "Afegir usuaris"
msgctxt "view:res.user.config.start:"
msgid "Be careful that the login must be unique!"
-msgstr "El identificador ha de ser únic."
+msgstr "L'identificador ha de ser únic."
msgctxt "view:res.user.config.start:"
msgid "You can now add some users into the system."
@@ -674,7 +670,7 @@ msgstr "Ara podeu afegir usuaris al sistema."
msgctxt "view:res.user.warning:"
msgid "Warning"
-msgstr "Esperant"
+msgstr "Alerta"
msgctxt "view:res.user.warning:"
msgid "Warnings"
@@ -682,7 +678,7 @@ msgstr "Alertes"
msgctxt "view:res.user:"
msgid "Access Permissions"
-msgstr "Accés permisos"
+msgstr "Permisos d'accés"
msgctxt "view:res.user:"
msgid "Actions"
@@ -710,12 +706,12 @@ msgstr "Cancel·la"
msgctxt "wizard_button:res.user.config,start,user:"
msgid "Ok"
-msgstr "Dacord"
+msgstr "D'acord"
msgctxt "wizard_button:res.user.config,user,add:"
msgid "Add"
-msgstr "Afegir"
+msgstr "Afegeix"
msgctxt "wizard_button:res.user.config,user,end:"
msgid "End"
-msgstr "Finalitzat"
+msgstr "Finalitza"
diff --git a/trytond/res/locale/cs_CZ.po b/trytond/res/locale/cs_CZ.po
index 3531d01..57228c3 100644
--- a/trytond/res/locale/cs_CZ.po
+++ b/trytond/res/locale/cs_CZ.po
@@ -361,6 +361,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr ""
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr ""
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr ""
@@ -373,10 +377,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr ""
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr ""
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr ""
@@ -389,10 +389,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr ""
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr ""
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr ""
diff --git a/trytond/res/locale/de_DE.po b/trytond/res/locale/de_DE.po
index 6ab5064..7b1e739 100644
--- a/trytond/res/locale/de_DE.po
+++ b/trytond/res/locale/de_DE.po
@@ -363,6 +363,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr "Passwort"
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr "Passwort Hash"
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr "PySON Menü"
@@ -375,10 +379,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr "Datensatzregeln"
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr "Salt"
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr "Sitzungen"
@@ -391,10 +391,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr "Status Anzeige"
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr "Zeitzone"
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr "Warnungen"
@@ -546,7 +542,7 @@ msgstr ""
msgctxt "help:res.user,actions:"
msgid "Actions that will be run at login"
-msgstr "Aktionen, die nach dem Login ausgeführt werden"
+msgstr "Diese Aktionen werden nach dem Login ausgeführt"
msgctxt "model:ir.action,name:act_group_form"
msgid "Groups"
diff --git a/trytond/res/locale/es_AR.po b/trytond/res/locale/es_AR.po
index 55a354e..f889836 100644
--- a/trytond/res/locale/es_AR.po
+++ b/trytond/res/locale/es_AR.po
@@ -82,7 +82,7 @@ msgstr "ID"
msgctxt "field:ir.model.button-res.group,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.model.button-res.group,write_date:"
msgid "Write Date"
@@ -218,7 +218,7 @@ msgstr "ID"
msgctxt "field:ir.sequence.type-res.group,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:ir.sequence.type-res.group,sequence_type:"
msgid "Sequence Type"
@@ -364,6 +364,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr "Contraseña"
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr "Hash de contraseña"
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr "Menú PySON"
@@ -376,10 +380,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr "Reglas"
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr "Sal"
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr "Sesiones"
@@ -392,10 +392,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr "Barra de estado"
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr "Zona horaria"
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr "Avisos"
@@ -426,7 +422,7 @@ msgstr "ID"
msgctxt "field:res.user-ir.action,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:res.user-ir.action,user:"
msgid "User"
@@ -494,7 +490,7 @@ msgstr "Nombre de usuario"
msgctxt "field:res.user.login.attempt,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:res.user.login.attempt,write_date:"
msgid "Write Date"
@@ -670,7 +666,7 @@ msgstr "Marcar para actualizar"
msgctxt "view:res.group:"
msgid "Access Permissions"
-msgstr "Permisos de Acceso"
+msgstr "Permisos de acceso"
msgctxt "view:res.group:"
msgid "Group"
@@ -698,15 +694,15 @@ msgstr "Ahora puede agregar algunos usuarios al sistema."
msgctxt "view:res.user.warning:"
msgid "Warning"
-msgstr "Advertencia"
+msgstr "Aviso"
msgctxt "view:res.user.warning:"
msgid "Warnings"
-msgstr "Advertencias"
+msgstr "Avisos"
msgctxt "view:res.user:"
msgid "Access Permissions"
-msgstr "Permisos de Acceso"
+msgstr "Permisos de acceso"
msgctxt "view:res.user:"
msgid "Actions"
diff --git a/trytond/res/locale/es_CO.po b/trytond/res/locale/es_CO.po
index 3eb1929..b351bbe 100644
--- a/trytond/res/locale/es_CO.po
+++ b/trytond/res/locale/es_CO.po
@@ -4,7 +4,7 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:res.group:"
msgid "The name of the group must be unique!"
-msgstr "¡El nombre del grupo debe ser único!"
+msgstr "El nombre del grupo debe ser único!"
msgctxt "error:res.user:"
msgid "Wrong password!"
@@ -12,7 +12,7 @@ msgstr "¡Contraseña incorrecta!"
msgctxt "error:res.user:"
msgid "You can not have two users with the same login!"
-msgstr "No puede tener dos usuarios con el mismo nombre de usuario"
+msgstr "No puede tener dos usuarios con el mismo nombre de usuario!"
msgctxt "error:res.user:"
msgid ""
@@ -82,7 +82,7 @@ msgstr "ID"
msgctxt "field:ir.model.button-res.group,rec_name:"
msgid "Name"
-msgstr "Nombre del campo"
+msgstr "Nombre"
msgctxt "field:ir.model.button-res.group,write_date:"
msgid "Write Date"
@@ -190,15 +190,15 @@ msgstr "Modificado por Usuario"
msgctxt "field:ir.sequence,groups:"
msgid "User Groups"
-msgstr "Grupos de usuarios"
+msgstr "Grupos de Usuarios"
msgctxt "field:ir.sequence.strict,groups:"
msgid "User Groups"
-msgstr "Grupos de usuarios"
+msgstr "Grupos de Usuarios"
msgctxt "field:ir.sequence.type,groups:"
msgid "User Groups"
-msgstr "Grupos de usuarios"
+msgstr "Grupos de Usuarios"
msgctxt "field:ir.sequence.type-res.group,create_date:"
msgid "Create Date"
@@ -210,7 +210,7 @@ msgstr "Creado por Usuario"
msgctxt "field:ir.sequence.type-res.group,group:"
msgid "User Groups"
-msgstr "Grupos de usuarios"
+msgstr "Grupos de Usuarios"
msgctxt "field:ir.sequence.type-res.group,id:"
msgid "ID"
@@ -218,11 +218,11 @@ msgstr "ID"
msgctxt "field:ir.sequence.type-res.group,rec_name:"
msgid "Name"
-msgstr "Nombre del campo"
+msgstr "Nombre"
msgctxt "field:ir.sequence.type-res.group,sequence_type:"
msgid "Sequence Type"
-msgstr "Tipo de secuencia"
+msgstr "Tipo de Secuencia"
msgctxt "field:ir.sequence.type-res.group,write_date:"
msgid "Write Date"
@@ -354,7 +354,7 @@ msgstr "Nombre de usuario"
msgctxt "field:res.user,menu:"
msgid "Menu Action"
-msgstr "Menú de acciones"
+msgstr "Menú de Acciones"
msgctxt "field:res.user,name:"
msgid "Name"
@@ -364,6 +364,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr "Contraseña"
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr "Contraseña Hash"
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr "Menú PySON"
@@ -376,10 +380,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr "Reglas"
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr "Sal"
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr "Sesiones"
@@ -390,11 +390,7 @@ msgstr "Firma"
msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
-msgstr "Barra de estado"
-
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr "Zona horaria"
+msgstr "Barra de Estado"
msgctxt "field:res.user,warnings:"
msgid "Warnings"
@@ -426,7 +422,7 @@ msgstr "ID"
msgctxt "field:res.user-ir.action,rec_name:"
msgid "Name"
-msgstr "Nombre del campo"
+msgstr "Nombre"
msgctxt "field:res.user-ir.action,user:"
msgid "User"
@@ -494,7 +490,7 @@ msgstr "Nombre de usuario"
msgctxt "field:res.user.login.attempt,rec_name:"
msgid "Name"
-msgstr "Nombre del campo"
+msgstr "Nombre"
msgctxt "field:res.user.login.attempt,write_date:"
msgid "Write Date"
@@ -542,7 +538,7 @@ msgstr "Modificado por Usuario"
msgctxt "help:ir.sequence.type,groups:"
msgid "Groups allowed to edit the sequences of this type"
-msgstr "Los grupos son autorizados para editar secuencias en este tipo"
+msgstr "Grupos autorizados para editar secuencias en este tipo"
msgctxt "help:res.user,actions:"
msgid "Actions that will be run at login"
@@ -566,7 +562,7 @@ msgstr "Acción - Grupo"
msgctxt "model:ir.cron,name:cron_trigger_time"
msgid "Run On Time Triggers"
-msgstr "Ejecutar Disparadores Al Tiempo"
+msgstr "Ejecutar Disparadores a Tiempo"
msgctxt "model:ir.model.button-res.group,name:"
msgid "Model Button - Group"
@@ -578,11 +574,11 @@ msgstr "Relación entre grupo y campo del modelo"
msgctxt "model:ir.rule.group-res.group,name:"
msgid "Rule Group - Group"
-msgstr "Regla de grupo - grupo"
+msgstr "Regla de Grupo - Grupo"
msgctxt "model:ir.rule.group-res.user,name:"
msgid "Rule Group - User"
-msgstr "Regla de grupo - usuario"
+msgstr "Regla de Grupo - Usuario"
msgctxt "model:ir.sequence.type-res.group,name:"
msgid "Sequence Type - Group"
@@ -642,31 +638,31 @@ msgstr "Intento de Login"
msgctxt "model:res.user.warning,name:"
msgid "User Warning"
-msgstr "Aviso al usuario"
+msgstr "Aviso a Usuario"
msgctxt "view:ir.module.module:"
msgid "Cancel Installation"
-msgstr "Cancelar instalación"
+msgstr "Cancelar Instalación"
msgctxt "view:ir.module.module:"
msgid "Cancel Uninstallation"
-msgstr "Cancelar la desinstalación"
+msgstr "Cancelar la Desinstalación"
msgctxt "view:ir.module.module:"
msgid "Cancel Upgrade"
-msgstr "Cancelar actualización"
+msgstr "Cancelar Actualización"
msgctxt "view:ir.module.module:"
msgid "Mark for Installation"
-msgstr "Marcar para Instalación"
+msgstr "Instalar"
msgctxt "view:ir.module.module:"
msgid "Mark for Uninstallation (beta)"
-msgstr "Marcar para desinstalar (beta)"
+msgstr "Desinstalar (beta)"
msgctxt "view:ir.module.module:"
msgid "Mark for Upgrade"
-msgstr "Marcar para actualizar"
+msgstr "Actualizar"
msgctxt "view:res.group:"
msgid "Access Permissions"
@@ -690,7 +686,7 @@ msgstr "Agregar Usuarios"
msgctxt "view:res.user.config.start:"
msgid "Be careful that the login must be unique!"
-msgstr "¡Asegúrese de hacer los nombres de usuario únicos!"
+msgstr "Asegúrese de que los nombres de usuario sean únicos!"
msgctxt "view:res.user.config.start:"
msgid "You can now add some users into the system."
@@ -714,7 +710,7 @@ msgstr "Acciones"
msgctxt "view:res.user:"
msgid "Group Membership"
-msgstr "Membresía del grupo"
+msgstr "Miembro del Grupo"
msgctxt "view:res.user:"
msgid "Preferences"
diff --git a/trytond/res/locale/es_ES.po b/trytond/res/locale/es_ES.po
index 3abb1f4..7ed0da6 100644
--- a/trytond/res/locale/es_ES.po
+++ b/trytond/res/locale/es_ES.po
@@ -20,9 +20,9 @@ msgid ""
"as it is used internally for resources\n"
"created by the system (updates, module installation, ...)"
msgstr ""
-"No puede eliminar el usuario del sistema\n"
-"que se usa para recursos del sistema\n"
-"(actualizaciones, instalaciones, ...)."
+"No puede eliminar el usuario principal que\n"
+"se usa internamente para los recursos creados\n"
+"por el sistema (actualizaciones, instalaciones, ...)."
msgctxt "field:ir.action-res.group,action:"
msgid "Action"
@@ -62,7 +62,7 @@ msgstr "Activo"
msgctxt "field:ir.model.button-res.group,button:"
msgid "Button"
-msgstr "Inferior"
+msgstr "Botón"
msgctxt "field:ir.model.button-res.group,create_date:"
msgid "Create Date"
@@ -354,7 +354,7 @@ msgstr "Nombre de usuario"
msgctxt "field:res.user,menu:"
msgid "Menu Action"
-msgstr "Menú de acciones"
+msgstr "Acción menú"
msgctxt "field:res.user,name:"
msgid "Name"
@@ -364,6 +364,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr "Contraseña"
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr "Hash de la contraseña"
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr "Menú PySON"
@@ -376,10 +380,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr "Reglas"
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr "Sal"
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr "Sesiones"
@@ -392,10 +392,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr "Barra de estado"
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr "Zona horaria"
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr "Avisos"
@@ -570,7 +566,7 @@ msgstr "Ejecutar disparadores por tiempo"
msgctxt "model:ir.model.button-res.group,name:"
msgid "Model Button - Group"
-msgstr "Botón modelo - Grupo"
+msgstr "Modelo Botón - Grupo"
msgctxt "model:ir.model.field-res.group,name:"
msgid "Model Field Group Rel"
@@ -622,7 +618,7 @@ msgstr "Administrador"
msgctxt "model:res.user,name:user_trigger"
msgid "Cron Trigger"
-msgstr "Disparador planificador"
+msgstr "Planificador de disparadores"
msgctxt "model:res.user-ir.action,name:"
msgid "User - Action"
@@ -634,7 +630,7 @@ msgstr "Usuario - Grupo"
msgctxt "model:res.user.config.start,name:"
msgid "User Config Init"
-msgstr "Configuración usuario"
+msgstr "Configuración inicial usuario"
msgctxt "model:res.user.login.attempt,name:"
msgid "Login Attempt"
@@ -690,7 +686,7 @@ msgstr "Añadir usuarios"
msgctxt "view:res.user.config.start:"
msgid "Be careful that the login must be unique!"
-msgstr "Debe tener en cuenta que el identificador debe ser único."
+msgstr "El identificador debe ser único."
msgctxt "view:res.user.config.start:"
msgid "You can now add some users into the system."
diff --git a/trytond/res/locale/fr_FR.po b/trytond/res/locale/fr_FR.po
index 31bc64b..758ba94 100644
--- a/trytond/res/locale/fr_FR.po
+++ b/trytond/res/locale/fr_FR.po
@@ -364,6 +364,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr "Mot de passe"
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr "Hachage de mot de passe"
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr "Menu PySON"
@@ -376,10 +380,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr "Règles"
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr "Sel"
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr "Sessions"
@@ -392,10 +392,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr "Barre d'état"
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr "Fuseau horaire"
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr "Avertissements"
diff --git a/trytond/res/locale/nl_NL.po b/trytond/res/locale/nl_NL.po
index 9d70347..3bf2b91 100644
--- a/trytond/res/locale/nl_NL.po
+++ b/trytond/res/locale/nl_NL.po
@@ -387,6 +387,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr ""
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr ""
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr ""
@@ -400,10 +404,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr ""
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr ""
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr ""
@@ -416,10 +416,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr ""
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr ""
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr ""
diff --git a/trytond/res/locale/ru_RU.po b/trytond/res/locale/ru_RU.po
index b1b4488..22ebae5 100644
--- a/trytond/res/locale/ru_RU.po
+++ b/trytond/res/locale/ru_RU.po
@@ -364,6 +364,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr "Пароль"
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr ""
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr "Меню PySON"
@@ -376,10 +380,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr "Правила"
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr "Соль"
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr "Сессии"
@@ -392,10 +392,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr "Строка состояния"
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr "Часовой пояс"
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr "Предупреждения"
diff --git a/trytond/res/locale/sl_SI.po b/trytond/res/locale/sl_SI.po
index 8e8eb0e..e481897 100644
--- a/trytond/res/locale/sl_SI.po
+++ b/trytond/res/locale/sl_SI.po
@@ -363,6 +363,10 @@ msgctxt "field:res.user,password:"
msgid "Password"
msgstr "Geslo"
+msgctxt "field:res.user,password_hash:"
+msgid "Password Hash"
+msgstr "Zgoščena vrednost gesla"
+
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
msgstr "PySON meni"
@@ -375,10 +379,6 @@ msgctxt "field:res.user,rule_groups:"
msgid "Rules"
msgstr "Pravila"
-msgctxt "field:res.user,salt:"
-msgid "Salt"
-msgstr "Sol"
-
msgctxt "field:res.user,sessions:"
msgid "Sessions"
msgstr "Seje"
@@ -391,10 +391,6 @@ msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
msgstr "Statusna vrstica"
-msgctxt "field:res.user,timezone:"
-msgid "Timezone"
-msgstr "Časovni pas"
-
msgctxt "field:res.user,warnings:"
msgid "Warnings"
msgstr "Opozorila"
diff --git a/trytond/res/user.py b/trytond/res/user.py
index e50c8fd..4416c53 100644
--- a/trytond/res/user.py
+++ b/trytond/res/user.py
@@ -10,7 +10,14 @@ import datetime
from itertools import groupby, ifilter
from operator import attrgetter
from sql import Literal
+from sql.conditionals import Coalesce
from sql.aggregate import Count
+from sql.operators import Concat
+
+try:
+ import bcrypt
+except ImportError:
+ bcrypt = None
from ..model import ModelView, ModelSQL, fields
from ..wizard import Wizard, StateView, Button, StateTransition
@@ -23,13 +30,6 @@ from ..config import CONFIG
from ..pyson import PYSONEncoder
from ..rpc import RPC
-try:
- import pytz
- TIMEZONES = [(x, x) for x in pytz.common_timezones]
-except ImportError:
- TIMEZONES = []
-TIMEZONES += [(None, '')]
-
__all__ = [
'User', 'LoginAttempt', 'UserAction', 'UserGroup', 'Warning_',
'UserConfigStart', 'UserConfig',
@@ -41,8 +41,9 @@ class User(ModelSQL, ModelView):
__name__ = "res.user"
name = fields.Char('Name', required=True, select=True, translate=True)
login = fields.Char('Login', required=True)
- password = fields.Sha('Password')
- salt = fields.Char('Salt', size=8)
+ password_hash = fields.Char('Password Hash')
+ password = fields.Function(fields.Char('Password'), getter='get_password',
+ setter='set_password')
signature = fields.Text('Signature')
active = fields.Boolean('Active')
menu = fields.Many2One('ir.action', 'Menu Action',
@@ -61,7 +62,6 @@ class User(ModelSQL, ModelView):
])
language_direction = fields.Function(fields.Char('Language Direction'),
'get_language_direction')
- timezone = fields.Selection(TIMEZONES, 'Timezone', translate=False)
email = fields.Char('Email')
status_bar = fields.Function(fields.Char('Status Bar'), 'get_status_bar')
warnings = fields.One2Many('res.user.warning', 'user', 'Warnings')
@@ -97,7 +97,6 @@ class User(ModelSQL, ModelView):
cls._context_fields = [
'language',
'language_direction',
- 'timezone',
'groups',
]
cls._error_messages.update({
@@ -111,8 +110,9 @@ class User(ModelSQL, ModelView):
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
+ cursor = Transaction().cursor
super(User, cls).__register__(module_name)
- table = TableHandler(Transaction().cursor, cls, module_name)
+ table = TableHandler(cursor, cls, module_name)
# Migration from 1.6
@@ -135,9 +135,16 @@ class User(ModelSQL, ModelView):
# Migration from 2.6
table.drop_column('login_try', exception=True)
- @staticmethod
- def default_password():
- return None
+ # Migration from 3.0
+ if table.column_exist('password') and table.column_exist('salt'):
+ sqltable = cls.__table__()
+ password_hash_new = Concat('sha1$', Concat(sqltable.password,
+ Concat('$', Coalesce(sqltable.salt, ''))))
+ cursor.execute(*sqltable.update(
+ columns=[sqltable.password_hash],
+ values=[password_hash_new]))
+ table.drop_column('password', exception=True)
+ table.drop_column('salt', exception=True)
@staticmethod
def default_active():
@@ -172,6 +179,20 @@ class User(ModelSQL, ModelView):
def get_status_bar(self, name):
return self.name
+ def get_password(self, name):
+ return 'x' * 10
+
+ @classmethod
+ def set_password(cls, users, name, value):
+ if value == 'x' * 10:
+ return
+ to_write = []
+ for user in users:
+ to_write.extend([[user], {
+ 'password_hash': cls.hash_password(value),
+ }])
+ cls.write(*to_write)
+
@staticmethod
def get_sessions(users, name):
Session = Pool().get('ir.session')
@@ -202,13 +223,6 @@ class User(ModelSQL, ModelView):
Action = pool.get('ir.action')
if 'menu' in vals:
vals['menu'] = Action.get_action_id(vals['menu'])
- if 'password' in vals:
- if vals['password'] == 'x' * 10:
- del vals['password']
- elif vals['password']:
- vals['salt'] = ''.join(random.sample(
- string.ascii_letters + string.digits, 8))
- vals['password'] += vals['salt']
return vals
@classmethod
@@ -220,13 +234,18 @@ class User(ModelSQL, ModelView):
return res
@classmethod
- def write(cls, users, vals):
- vals = cls._convert_vals(vals)
- super(User, cls).write(users, vals)
+ def write(cls, users, values, *args):
+ actions = iter((users, values) + args)
+ all_users = []
+ args = []
+ for users, values in zip(actions, actions):
+ all_users += users
+ args.extend((users, cls._convert_vals(values)))
+ super(User, cls).write(*args)
# Clean cursor cache as it could be filled by domain_get
for cache in Transaction().cursor.cache.itervalues():
if cls.__name__ in cache:
- for user in users:
+ for user in all_users:
if user.id in cache[cls.__name__]:
cache[cls.__name__][user.id].clear()
# Restart the cache for domain_get method
@@ -240,6 +259,8 @@ class User(ModelSQL, ModelView):
cls._get_preferences_cache.clear()
# Restart the cache of check
pool.get('ir.model.access')._get_access_cache.clear()
+ # Restart the cache of check
+ pool.get('ir.model.field.access')._get_access_cache.clear()
# Restart the cache
ModelView._fields_view_get_cache.clear()
@@ -252,14 +273,6 @@ class User(ModelSQL, ModelView):
cls._get_login_cache.clear()
@classmethod
- def read(cls, ids, fields_names=None):
- res = super(User, cls).read(ids, fields_names=fields_names)
- for val in res:
- if 'password' in val:
- val['password'] = 'x' * 10
- return res
-
- @classmethod
def search_rec_name(cls, name, clause):
users = cls.search([
('login', '=', clause[2]),
@@ -344,7 +357,7 @@ class User(ModelSQL, ModelView):
if preferences is not None:
return preferences.copy()
user = Transaction().user
- with Transaction().set_user(0):
+ with Transaction().set_user(0, set_context=True):
user = cls(user)
preferences = cls._get_preferences(user, context_only=context_only)
cls._get_preferences_cache.set(key, preferences)
@@ -397,6 +410,7 @@ class User(ModelSQL, ModelView):
def convert2selection(definition, name):
del definition[name]['relation']
+ del definition[name]['domain']
definition[name]['type'] = 'selection'
selection = []
definition[name]['selection'] = selection
@@ -444,9 +458,9 @@ class User(ModelSQL, ModelView):
return result
cursor = Transaction().cursor
table = cls.__table__()
- cursor.execute(*table.select(table.id, table.password, table.salt,
+ cursor.execute(*table.select(table.id, table.password_hash,
where=(table.login == login) & table.active))
- result = cursor.fetchone() or (None, None, None)
+ result = cursor.fetchone() or (None, None)
cls._get_login_cache.set(login, result)
return result
@@ -457,18 +471,69 @@ class User(ModelSQL, ModelView):
'''
LoginAttempt = Pool().get('res.user.login.attempt')
time.sleep(2 ** LoginAttempt.count(login) - 1)
- user_id, user_password, salt = cls._get_login(login)
+ user_id, password_hash = cls._get_login(login)
if user_id:
- password += salt or ''
- if isinstance(password, unicode):
- password = password.encode('utf-8')
- password_sha = hashlib.sha1(password).hexdigest()
- if password_sha == user_password:
+ if cls.check_password(password, password_hash):
LoginAttempt.remove(login)
return user_id
LoginAttempt.add(login)
return 0
+ @staticmethod
+ def hash_method():
+ return 'bcrypt' if bcrypt else 'sha1'
+
+ @classmethod
+ def hash_password(cls, password):
+ '''Hash given password in the form
+ <hash_method>$<password>$<salt>...'''
+ if not password:
+ return ''
+ return getattr(cls, 'hash_' + cls.hash_method())(password)
+
+ @classmethod
+ def check_password(cls, password, hash_):
+ if not hash_:
+ return False
+ hash_method = hash_.split('$', 1)[0]
+ return getattr(cls, 'check_' + hash_method)(password, hash_)
+
+ @classmethod
+ def hash_sha1(cls, password):
+ if isinstance(password, unicode):
+ password = password.encode('utf-8')
+ salt = ''.join(random.sample(string.ascii_letters + string.digits, 8))
+ hash_ = hashlib.sha1(password + salt).hexdigest()
+ return '$'.join(['sha1', hash_, salt])
+
+ @classmethod
+ def check_sha1(cls, password, hash_):
+ if isinstance(password, unicode):
+ password = password.encode('utf-8')
+ if isinstance(hash_, unicode):
+ hash_ = hash_.encode('utf-8')
+ hash_method, hash_, salt = hash_.split('$', 2)
+ salt = salt or ''
+ assert hash_method == 'sha1'
+ return hash_ == hashlib.sha1(password + salt).hexdigest()
+
+ @classmethod
+ def hash_bcrypt(cls, password):
+ if isinstance(password, unicode):
+ password = password.encode('utf-8')
+ hash_ = bcrypt.hashpw(password, bcrypt.gensalt())
+ return '$'.join(['bcrypt', hash_])
+
+ @classmethod
+ def check_bcrypt(cls, password, hash_):
+ if isinstance(password, unicode):
+ password = password.encode('utf-8')
+ if isinstance(hash_, unicode):
+ hash_ = hash_.encode('utf-8')
+ hash_method, hash_ = hash_.split('$', 1)
+ assert hash_method == 'bcrypt'
+ return hash_ == bcrypt.hashpw(password, hash_)
+
class LoginAttempt(ModelSQL):
"""Login Attempt
@@ -539,9 +604,12 @@ class UserAction(ModelSQL):
return super(UserAction, cls).create(vlist)
@classmethod
- def write(cls, records, values):
- values = cls._convert_values(values)
- super(UserAction, cls).write(records, values)
+ def write(cls, records, values, *args):
+ actions = iter((records, values) + args)
+ args = []
+ for records, values in zip(actions, actions):
+ args.extend((records, cls._convert_values(values)))
+ super(UserAction, cls).write(*args)
class UserGroup(ModelSQL):
diff --git a/trytond/res/view/user_form.xml b/trytond/res/view/user_form.xml
index 3cdcab7..629ff82 100644
--- a/trytond/res/view/user_form.xml
+++ b/trytond/res/view/user_form.xml
@@ -11,7 +11,7 @@ this repository contains the full copyright notices and license terms. -->
<label name="login"/>
<field name="login"/>
<label name="password"/>
- <field name="password"/>
+ <field name="password" widget="password"/>
<label name="email"/>
<field name="email" widget="email"/>
<separator name="signature" colspan="4"/>
@@ -29,8 +29,6 @@ this repository contains the full copyright notices and license terms. -->
<page string="Preferences" col="2" id="preferences">
<label name="language"/>
<field name="language" widget="selection"/>
- <label name="timezone"/>
- <field name="timezone"/>
</page>
</notebook>
</form>
diff --git a/trytond/res/view/user_form_preferences.xml b/trytond/res/view/user_form_preferences.xml
index 8e34286..42bbd25 100644
--- a/trytond/res/view/user_form_preferences.xml
+++ b/trytond/res/view/user_form_preferences.xml
@@ -9,7 +9,7 @@ this repository contains the full copyright notices and license terms. -->
<label name="email"/>
<field name="email" widget="email"/>
<label name="password"/>
- <field name="password"/>
+ <field name="password" widget="password"/>
<separator name="signature" colspan="4"/>
<field name="signature" colspan="4"/>
</page>
@@ -24,8 +24,6 @@ this repository contains the full copyright notices and license terms. -->
<page string="Preferences" col="2" id="preferences">
<label name="language"/>
<field name="language" widget="selection"/>
- <label name="timezone"/>
- <field name="timezone"/>
</page>
</notebook>
</form>
diff --git a/trytond/rpc.py b/trytond/rpc.py
index afa0827..1e3e2a9 100644
--- a/trytond/rpc.py
+++ b/trytond/rpc.py
@@ -8,7 +8,7 @@ class RPC(object):
'''Define RPC behavior
readonly: The transaction mode
- instantiate: The position of the argument to be instanciated
+ instantiate: The position or the slice of the arguments to be instanciated
result: The function to transform the result
'''
@@ -41,6 +41,12 @@ class RPC(object):
return obj(**data)
else:
return obj.browse(data)
- data = args[self.instantiate]
- args[self.instantiate] = instance(data)
+ if isinstance(self.instantiate, slice):
+ for i, data in enumerate(args[self.instantiate]):
+ start, _, step = self.instantiate.indices(len(args))
+ i = i * step + start
+ args[i] = instance(data)
+ else:
+ data = args[self.instantiate]
+ args[self.instantiate] = instance(data)
return args, kwargs, context, timestamp
diff --git a/trytond/server.py b/trytond/server.py
index 23a1989..720b224 100644
--- a/trytond/server.py
+++ b/trytond/server.py
@@ -29,7 +29,6 @@ class TrytonServer(object):
CONFIG.update_etc(options['configfile'])
CONFIG.update_cmdline(options)
- CONFIG.set_timezone()
if CONFIG['logfile']:
logf = CONFIG['logfile']
diff --git a/trytond/tests/__init__.py b/trytond/tests/__init__.py
index 3ce4543..8da0e0a 100644
--- a/trytond/tests/__init__.py
+++ b/trytond/tests/__init__.py
@@ -12,6 +12,7 @@ from .access import *
from .wizard import *
from .workflow import *
from .copy_ import *
+from history import *
def register():
@@ -21,6 +22,7 @@ def register():
Integer,
IntegerDefault,
IntegerRequired,
+ IntegerDomain,
Float,
FloatDefault,
FloatRequired,
@@ -39,9 +41,6 @@ def register():
TextRequired,
TextSize,
TextTranslate,
- Sha,
- ShaDefault,
- ShaRequired,
Date,
DateDefault,
DateRequired,
@@ -108,7 +107,6 @@ def register():
ImportDataNumericRequired,
ImportDataChar,
ImportDataText,
- ImportDataSha,
ImportDataDate,
ImportDataDateTime,
ImportDataSelection,
@@ -134,8 +132,15 @@ def register():
CopyOne2ManyTarget,
CopyOne2ManyReference,
CopyOne2ManyReferenceTarget,
+ CopyMany2Many,
+ CopyMany2ManyTarget,
+ CopyMany2ManyRelation,
+ CopyMany2ManyReference,
+ CopyMany2ManyReferenceTarget,
+ CopyMany2ManyReferenceRelation,
Many2OneTarget,
Many2OneDomainValidation,
+ TestHistory,
module='tests', type_='model')
Pool.register(
TestWizard,
diff --git a/trytond/tests/copy_.py b/trytond/tests/copy_.py
index c2eb916..d0d92cc 100644
--- a/trytond/tests/copy_.py
+++ b/trytond/tests/copy_.py
@@ -6,6 +6,9 @@ from trytond.model import ModelSQL, fields
__all__ = [
'CopyOne2Many', 'CopyOne2ManyTarget',
'CopyOne2ManyReference', 'CopyOne2ManyReferenceTarget',
+ 'CopyMany2Many', 'CopyMany2ManyTarget', 'CopyMany2ManyRelation',
+ 'CopyMany2ManyReference', 'CopyMany2ManyReferenceTarget',
+ 'CopyMany2ManyReferenceRelation',
]
@@ -40,3 +43,52 @@ class CopyOne2ManyReferenceTarget(ModelSQL):
(None, ''),
('test.copy.one2many_reference', 'One2Many'),
])
+
+
+class CopyMany2Many(ModelSQL):
+ "Copy Many2Many"
+ __name__ = 'test.copy.many2many'
+ name = fields.Char('Name')
+ many2many = fields.Many2Many('test.copy.many2many.rel', 'many2many',
+ 'many2many_target', 'Many2Many')
+
+
+class CopyMany2ManyTarget(ModelSQL):
+ "Copy Many2Many Target"
+ __name__ = 'test.copy.many2many.target'
+ name = fields.Char('Name')
+
+
+class CopyMany2ManyRelation(ModelSQL):
+ "Copy Many2Many Relation"
+ __name__ = 'test.copy.many2many.rel'
+ name = fields.Char('Name')
+ many2many = fields.Many2One('test.copy.many2many', 'Many2Many')
+ many2many_target = fields.Many2One('test.copy.many2many.target',
+ 'Many2Many Target')
+
+
+class CopyMany2ManyReference(ModelSQL):
+ "Copy Many2ManyReference"
+ __name__ = 'test.copy.many2many_reference'
+ name = fields.Char('Name')
+ many2many = fields.Many2Many('test.copy.many2many_reference.rel',
+ 'many2many', 'many2many_target', 'Many2Many')
+
+
+class CopyMany2ManyReferenceTarget(ModelSQL):
+ "Copy Many2ManyReference Target"
+ __name__ = 'test.copy.many2many_reference.target'
+ name = fields.Char('Name')
+
+
+class CopyMany2ManyReferenceRelation(ModelSQL):
+ "Copy Many2ManyReference Relation"
+ __name__ = 'test.copy.many2many_reference.rel'
+ name = fields.Char('Name')
+ many2many = fields.Reference('Many2Many', [
+ (None, ''),
+ ('test.copy.many2many_reference', 'Many2Many'),
+ ])
+ many2many_target = fields.Many2One('test.copy.many2many_reference.target',
+ 'Many2ManyReference Target')
diff --git a/trytond/tests/export_data.py b/trytond/tests/export_data.py
index 8c9f3c6..5f61332 100644
--- a/trytond/tests/export_data.py
+++ b/trytond/tests/export_data.py
@@ -25,7 +25,6 @@ class ExportData(ModelSQL):
numeric = fields.Numeric('Numeric')
char = fields.Char('Char')
text = fields.Text('Text')
- sha = fields.Sha('Sha')
date = fields.Date('Date')
datetime = fields.DateTime('DateTime')
selection = fields.Selection([
diff --git a/trytond/tests/history.py b/trytond/tests/history.py
new file mode 100644
index 0000000..b23d8ec
--- /dev/null
+++ b/trytond/tests/history.py
@@ -0,0 +1,12 @@
+#This file is part of Tryton. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+from trytond.model import ModelSQL, fields
+
+__all__ = ['TestHistory']
+
+
+class TestHistory(ModelSQL):
+ 'Test History'
+ __name__ = 'test.history'
+ _history = True
+ value = fields.Integer('Value')
diff --git a/trytond/tests/import_data.py b/trytond/tests/import_data.py
index 870bc21..ecc7a94 100644
--- a/trytond/tests/import_data.py
+++ b/trytond/tests/import_data.py
@@ -7,7 +7,7 @@ __all__ = [
'ImportDataBoolean', 'ImportDataInteger', 'ImportDataIntegerRequired',
'ImportDataFloat', 'ImportDataFloatRequired',
'ImportDataNumeric', 'ImportDataNumericRequired',
- 'ImportDataChar', 'ImportDataText', 'ImportDataSha',
+ 'ImportDataChar', 'ImportDataText',
'ImportDataDate', 'ImportDataDateTime', 'ImportDataSelection',
'ImportDataMany2OneTarget', 'ImportDataMany2One',
'ImportDataMany2ManyTarget', 'ImportDataMany2Many',
@@ -71,12 +71,6 @@ class ImportDataText(ModelSQL):
text = fields.Text('Text')
-class ImportDataSha(ModelSQL):
- "Import Data Sha"
- __name__ = 'test.import_data.sha'
- sha = fields.Sha('Sha')
-
-
class ImportDataDate(ModelSQL):
"Import Data Date"
__name__ = 'test.import_data.date'
diff --git a/trytond/tests/run-tests.py b/trytond/tests/run-tests.py
new file mode 100755
index 0000000..13bbd88
--- /dev/null
+++ b/trytond/tests/run-tests.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+#This file is part of Tryton. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+import logging
+import argparse
+import os
+import time
+import unittest
+
+from trytond.config import CONFIG
+
+if __name__ != '__main__':
+ raise ImportError('%s can not be imported' % __name__)
+
+logging.basicConfig(level=logging.ERROR)
+parser = argparse.ArgumentParser()
+parser.add_argument("-c", "--config", dest="config",
+ help="specify config file")
+parser.add_argument("-m", "--modules", action="store_true", dest="modules",
+ default=False, help="Run also modules tests")
+parser.add_argument("-v", action="count", default=0, dest="verbosity",
+ help="Increase verbosity")
+parser.add_argument('tests', metavar='test', nargs='*')
+opt = parser.parse_args()
+
+CONFIG['db_type'] = 'sqlite'
+CONFIG.update_etc(opt.config)
+if not CONFIG['admin_passwd']:
+ CONFIG['admin_passwd'] = 'admin'
+
+if CONFIG['db_type'] == 'sqlite':
+ database_name = ':memory:'
+else:
+ database_name = 'test_' + str(int(time.time()))
+os.environ['DB_NAME'] = database_name
+
+from trytond.tests.test_tryton import all_suite, modules_suite
+if not opt.modules:
+ suite = all_suite(opt.tests)
+else:
+ suite = modules_suite(opt.tests)
+unittest.TextTestRunner(verbosity=opt.verbosity).run(suite)
diff --git a/trytond/tests/test.py b/trytond/tests/test.py
index 6efeba4..f6cbaf7 100644
--- a/trytond/tests/test.py
+++ b/trytond/tests/test.py
@@ -8,12 +8,11 @@ from trytond.pyson import Eval
__all__ = [
'Boolean', 'BooleanDefault',
- 'Integer', 'IntegerDefault', 'IntegerRequired',
+ 'Integer', 'IntegerDefault', 'IntegerRequired', 'IntegerDomain',
'Float', 'FloatDefault', 'FloatRequired', 'FloatDigits',
'Numeric', 'NumericDefault', 'NumericRequired', 'NumericDigits',
'Char', 'CharDefault', 'CharRequired', 'CharSize', 'CharTranslate',
'Text', 'TextDefault', 'TextRequired', 'TextSize', 'TextTranslate',
- 'Sha', 'ShaDefault', 'ShaRequired',
'Date', 'DateDefault', 'DateRequired',
'DateTime', 'DateTimeDefault', 'DateTimeRequired', 'DateTimeFormat',
'Time', 'TimeDefault', 'TimeRequired', 'TimeFormat',
@@ -80,6 +79,12 @@ class IntegerRequired(ModelSQL):
required=True)
+class IntegerDomain(ModelSQL):
+ 'Integer Domain'
+ __name__ = 'test.integer_domain'
+ integer = fields.Integer('Integer', domain=[('integer', '>', 42)])
+
+
class Float(ModelSQL):
'Float'
__name__ = 'test.float'
@@ -226,31 +231,6 @@ class TextTranslate(ModelSQL):
required=False, translate=True)
-class Sha(ModelSQL):
- 'Sha'
- __name__ = 'test.sha'
- sha = fields.Sha(string='Sha', help='Test sha',
- required=False)
-
-
-class ShaDefault(ModelSQL):
- 'Sha Default'
- __name__ = 'test.sha_default'
- sha = fields.Sha(string='Sha', help='Test sha',
- required=False)
-
- @staticmethod
- def default_sha():
- return 'Sha'
-
-
-class ShaRequired(ModelSQL):
- 'Sha Required'
- __name__ = 'test.sha_required'
- sha = fields.Sha(string='Sha', help='Test sha',
- required=True)
-
-
class Date(ModelSQL):
'Date'
__name__ = 'test.date'
@@ -625,10 +605,11 @@ class Selection(ModelSQL):
('', ''), ('arabic', 'Arabic'), ('hexa', 'Hexadecimal')],
'Selection')
dyn_select = fields.Selection('get_selection',
- 'Instance Dynamic Selection', selection_change_with=['select'])
+ 'Instance Dynamic Selection')
dyn_select_static = fields.Selection('static_selection',
'Static Selection')
+ @fields.depends('select')
def get_selection(self):
if self.select == 'arabic':
return [('', '')] + [(str(i), str(i)) for i in range(1, 11)]
diff --git a/trytond/tests/test_access.py b/trytond/tests/test_access.py
index 5efe63f..6322ac4 100644
--- a/trytond/tests/test_access.py
+++ b/trytond/tests/test_access.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -9,9 +8,7 @@ from trytond.transaction import Transaction
class ModelAccessTestCase(unittest.TestCase):
- '''
- Test Model Access
- '''
+ 'Test Model Access'
def setUp(self):
install_module('tests')
@@ -21,9 +18,7 @@ class ModelAccessTestCase(unittest.TestCase):
self.group = POOL.get('res.group')
def test0010perm_read(self):
- '''
- Test Read Access
- '''
+ 'Test Read Access'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
model, = self.model.search([('model', '=', 'test.access')])
@@ -104,9 +99,7 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access._get_access_cache.clear()
def test0020perm_write(self):
- '''
- Test Write Access
- '''
+ 'Test Write Access'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
model, = self.model.search([('model', '=', 'test.access')])
@@ -186,9 +179,7 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access._get_access_cache.clear()
def test0030perm_create(self):
- '''
- Test Create Access
- '''
+ 'Test Create Access'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
model, = self.model.search([('model', '=', 'test.access')])
@@ -267,9 +258,7 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access._get_access_cache.clear()
def test0040perm_delete(self):
- '''
- Test Delete Access
- '''
+ 'Test Delete Access'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
model, = self.model.search([('model', '=', 'test.access')])
@@ -354,9 +343,7 @@ class ModelAccessTestCase(unittest.TestCase):
class ModelFieldAccessTestCase(unittest.TestCase):
- '''
- Test Model Field Access
- '''
+ 'Test Model Field Access'
def setUp(self):
install_module('tests')
@@ -366,9 +353,7 @@ class ModelFieldAccessTestCase(unittest.TestCase):
self.group = POOL.get('res.group')
def test0010perm_read(self):
- '''
- Test Read Access
- '''
+ 'Test Read Access'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
field1, = self.field.search([
@@ -578,9 +563,7 @@ class ModelFieldAccessTestCase(unittest.TestCase):
self.field_access._get_access_cache.clear()
def test0010perm_write(self):
- '''
- Test Write Access
- '''
+ 'Test Write Access'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
field1, = self.field.search([
@@ -793,7 +776,3 @@ def suite():
suite_.addTests(unittest.TestLoader(
).loadTestsFromTestCase(ModelFieldAccessTestCase))
return suite_
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_cache.py b/trytond/tests/test_cache.py
index f204826..9aa859c 100644
--- a/trytond/tests/test_cache.py
+++ b/trytond/tests/test_cache.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -39,7 +38,3 @@ def suite():
for testcase in (CacheTestCase,):
suite.addTests(func(testcase))
return suite
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_copy.py b/trytond/tests/test_copy.py
index 164d5c6..d6c3156 100644
--- a/trytond/tests/test_copy.py
+++ b/trytond/tests/test_copy.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -9,9 +8,7 @@ from trytond.transaction import Transaction
class CopyTestCase(unittest.TestCase):
- '''
- Test copy.
- '''
+ 'Test copy'
def setUp(self):
install_module('tests')
@@ -20,13 +17,16 @@ class CopyTestCase(unittest.TestCase):
self.one2many_reference = POOL.get('test.copy.one2many_reference')
self.one2many_reference_target = \
POOL.get('test.copy.one2many_reference.target')
+ self.many2many = POOL.get('test.copy.many2many')
+ self.many2many_target = POOL.get('test.copy.many2many.target')
+ self.many2many_reference = POOL.get('test.copy.many2many_reference')
+ self.many2many_reference_target = \
+ POOL.get('test.copy.many2many_reference.target')
def test0130one2many(self):
- '''
- Test one2many.
- '''
+ 'Test one2many'
with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
+ context=CONTEXT):
for One2many, Target in (
(self.one2many, self.one2many_target),
(self.one2many_reference, self.one2many_reference_target),
@@ -47,12 +47,31 @@ class CopyTestCase(unittest.TestCase):
self.assertEqual([x.name for x in one2many.one2many],
[x.name for x in one2many_copy.one2many])
- transaction.cursor.rollback()
+ def test0140many2many(self):
+ 'Test many2many'
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT):
+ for Many2many, Target in (
+ (self.many2many, self.many2many_target),
+ (self.many2many_reference,
+ self.many2many_reference_target),
+ ):
+ many2many = Many2many(name='Test')
+ many2many.many2many = [
+ Target(name='Target 1'),
+ Target(name='Target 2'),
+ ]
+ many2many.save()
+
+ many2many_copy, = Many2many.copy([many2many])
+
+ self.assertNotEqual(many2many, many2many_copy)
+ self.assertEqual(len(many2many.many2many),
+ len(many2many_copy.many2many))
+ self.assertEqual(many2many.many2many, many2many_copy.many2many)
+ self.assertEqual([x.name for x in many2many.many2many],
+ [x.name for x in many2many_copy.many2many])
def suite():
return unittest.TestLoader().loadTestsFromTestCase(CopyTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_exportdata.py b/trytond/tests/test_exportdata.py
index 6b9efbf..989d2d2 100644
--- a/trytond/tests/test_exportdata.py
+++ b/trytond/tests/test_exportdata.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -19,9 +18,7 @@ from trytond.transaction import Transaction
class ExportDataTestCase(unittest.TestCase):
- '''
- Test export_data.
- '''
+ 'Test export_data'
def setUp(self):
install_module('tests')
@@ -30,9 +27,7 @@ class ExportDataTestCase(unittest.TestCase):
self.export_data_relation = POOL.get('test.export_data.relation')
def test0010boolean(self):
- '''
- Test boolean.
- '''
+ 'Test boolean'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{
@@ -56,9 +51,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0020integer(self):
- '''
- Test integer.
- '''
+ 'Test integer'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{
@@ -80,9 +73,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0030float(self):
- '''
- Test float.
- '''
+ 'Test float'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{
@@ -104,9 +95,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0040numeric(self):
- '''
- Test numeric.
- '''
+ 'Test numeric'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{
@@ -130,9 +119,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0050char(self):
- '''
- Test char.
- '''
+ 'Test char'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{
@@ -154,9 +141,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0060text(self):
- '''
- Test text.
- '''
+ 'Test text'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{
@@ -177,25 +162,8 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
- def test0070sha(self):
- '''
- Test sha.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
- export1, = self.export_data.create([{
- 'sha': 'Test',
- }])
- self.assertEqual(
- self.export_data.export_data([export1], ['sha']),
- [['640ab2bae07bedc4c163f679a746f7ab7fb5d1fa']])
-
- transaction.cursor.rollback()
-
def test0080date(self):
- '''
- Test date.
- '''
+ 'Test date'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{
@@ -217,9 +185,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0090datetime(self):
- '''
- Test datetime.
- '''
+ 'Test datetime'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{
@@ -243,9 +209,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0100selection(self):
- '''
- Test selection.
- '''
+ 'Test selection'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{
@@ -269,9 +233,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0110many2one(self):
- '''
- Test many2one.
- '''
+ 'Test many2one'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
target, = self.export_data_target.create([{
@@ -299,16 +261,14 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0120many2many(self):
- '''
- Test many2many.
- '''
+ 'Test many2many'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
target1, = self.export_data_target.create([{
'name': 'Target 1',
}])
export1, = self.export_data.create([{
- 'many2many': [('set', [target1])],
+ 'many2many': [('add', [target1])],
}])
self.assertEqual(
self.export_data.export_data([export1], ['many2many/name']),
@@ -318,7 +278,7 @@ class ExportDataTestCase(unittest.TestCase):
'name': 'Target 2',
}])
self.export_data.write([export1], {
- 'many2many': [('set', [target1.id, target2.id])],
+ 'many2many': [('add', [target1.id, target2.id])],
})
self.assertEqual(
self.export_data.export_data([export1], ['id',
@@ -340,9 +300,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0130one2many(self):
- '''
- Test one2many.
- '''
+ 'Test one2many'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
export1, = self.export_data.create([{}])
@@ -376,9 +334,7 @@ class ExportDataTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0140reference(self):
- '''
- Test reference.
- '''
+ 'Test reference'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
target1, = self.export_data_target.create([{}])
@@ -405,7 +361,3 @@ class ExportDataTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(ExportDataTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_fields.py b/trytond/tests/test_fields.py
index 0d7ae44..8d260cc 100644
--- a/trytond/tests/test_fields.py
+++ b/trytond/tests/test_fields.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -20,9 +19,7 @@ from trytond.exceptions import UserError
class FieldsTestCase(unittest.TestCase):
- '''
- Test Fields.
- '''
+ 'Test Fields'
def setUp(self):
install_module('tests')
@@ -32,6 +29,7 @@ class FieldsTestCase(unittest.TestCase):
self.integer = POOL.get('test.integer')
self.integer_default = POOL.get('test.integer_default')
self.integer_required = POOL.get('test.integer_required')
+ self.integer_domain = POOL.get('test.integer_domain')
self.float = POOL.get('test.float')
self.float_default = POOL.get('test.float_default')
@@ -55,10 +53,6 @@ class FieldsTestCase(unittest.TestCase):
self.text_size = POOL.get('test.text_size')
self.text_translate = POOL.get('test.text_translate')
- self.sha = POOL.get('test.sha')
- self.sha_default = POOL.get('test.sha_default')
- self.sha_required = POOL.get('test.sha_required')
-
self.date = POOL.get('test.date')
self.date_default = POOL.get('test.date_default')
self.date_required = POOL.get('test.date_required')
@@ -119,9 +113,7 @@ class FieldsTestCase(unittest.TestCase):
self.m2o_target = POOL.get('test.many2one_target')
def test0010boolean(self):
- '''
- Test Boolean.
- '''
+ 'Test Boolean'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
boolean1, = self.boolean.create([{
@@ -219,9 +211,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0020integer(self):
- '''
- Test Integer.
- '''
+ 'Test Integer'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
integer1, = self.integer.create([{
@@ -400,10 +390,18 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
+ def test0021integer(self):
+ 'Test Integer with domain'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ self.integer_domain.create([{
+ 'integer': 100,
+ }])
+ self.assertRaises(UserError, self.integer_domain.create, [{
+ 'integer': 10,
+ }])
+
def test0030float(self):
- '''
- Test Float.
- '''
+ 'Test Float'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
float1, = self.float.create([{
@@ -606,9 +604,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0040numeric(self):
- '''
- Test Numeric.
- '''
+ 'Test Numeric'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
numeric1, = self.numeric.create([{
@@ -816,10 +812,21 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
+ def test0041numeric(self):
+ 'Test numeric search cast'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ numeric1, numeric2 = self.numeric.create([{
+ 'numeric': Decimal('1.1'),
+ }, {
+ 'numeric': Decimal('100.0'),
+ }])
+ numerics = self.numeric.search([
+ ('numeric', '<', Decimal('5')),
+ ])
+ self.assertEqual(numerics, [numeric1])
+
def test0050char(self):
- '''
- Test Char.
- '''
+ 'Test Char'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
for char in (self.char_translate, self.char):
@@ -1069,9 +1076,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0060text(self):
- '''
- Test Text.
- '''
+ 'Test Text'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
for text in (self.text_translate, self.text):
@@ -1319,202 +1324,8 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
- def test0070sha(self):
- '''
- Test Sha.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
- sha1, = self.sha.create([{
- 'sha': 'Test',
- }])
- self.assert_(sha1)
- self.assertEqual(sha1.sha,
- '640ab2bae07bedc4c163f679a746f7ab7fb5d1fa')
-
- sha = self.sha.search([
- ('sha', '=', 'Test'),
- ])
- self.assertEqual(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', '=', 'Foo'),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', '=', None),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', '!=', 'Test'),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', '!=', 'Foo'),
- ])
- self.assert_(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', '!=', None),
- ])
- self.assertEqual(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', 'in', ['Test']),
- ])
- self.assertEqual(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', 'in', ['Foo']),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', 'in', [None]),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', 'in', []),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', 'not in', ['Test']),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', 'not in', ['Foo']),
- ])
- self.assertEqual(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', 'not in', [None]),
- ])
- self.assertEqual(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', 'not in', []),
- ])
- self.assertEqual(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', 'like', 'Test'),
- ])
- self.assertEqual(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', 'like', 'Foo'),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', 'ilike', 'Test'),
- ])
- self.assertEqual(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', 'ilike', 'foo'),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', 'not like', 'Test'),
- ])
- self.assertEqual(sha, [])
-
- sha = self.sha.search([
- ('sha', 'not like', 'Foo'),
- ])
- self.assertEqual(sha, [sha1])
-
- sha = self.sha.search([
- ('sha', 'not ilike', 'foo'),
- ])
- self.assertEqual(sha, [sha1])
-
- sha2, = self.sha.create([{
- 'sha': None,
- }])
- self.assert_(sha2)
- self.assertEqual(sha2.sha, None)
-
- sha = self.sha.search([
- ('sha', '=', None),
- ])
- self.assertEqual(sha, [sha2])
-
- sha = self.sha.search([
- ('sha', 'in', [None, 'Test']),
- ])
- self.assertEqual(sha, [sha1, sha2])
-
- sha = self.sha.search([
- ('sha', 'not in', [None, 'Test']),
- ])
- self.assertEqual(sha, [])
-
- sha3, = self.sha.create([{}])
- self.assert_(sha3)
- self.assertEqual(sha3.sha, None)
-
- sha4, = self.sha_default.create([{}])
- self.assert_(sha4)
- self.assertEqual(sha4.sha,
- 'ba79baeb9f10896a46ae74715271b7f586e74640')
-
- self.sha.write([sha1], {
- 'sha': None,
- })
- self.assertEqual(sha1.sha, None)
-
- self.sha.write([sha2], {
- 'sha': 'Test',
- })
- self.assertEqual(sha2.sha,
- '640ab2bae07bedc4c163f679a746f7ab7fb5d1fa')
-
- self.assertRaises(Exception, self.sha_required.create, [{}])
- transaction.cursor.rollback()
-
- sha5, = self.sha_required.create([{
- 'sha': 'Test',
- }])
- self.assert_(sha5)
-
- sha6, = self.sha.create([{
- 'sha': u'é',
- }])
- self.assert_(sha6)
- self.assertEqual(sha6.sha,
- u'bf15be717ac1b080b4f1c456692825891ff5073d')
-
- sha = self.sha.search([
- ('sha', '=', u'é'),
- ])
- self.assertEqual(sha, [sha6])
-
- self.sha.write([sha6], {
- 'sha': 'é',
- })
- self.assertEqual(sha6.sha,
- u'bf15be717ac1b080b4f1c456692825891ff5073d')
-
- sha = self.sha.search([
- ('sha', '=', 'é'),
- ])
- self.assertEqual(sha, [sha6])
-
- transaction.cursor.rollback()
-
def test0080date(self):
- '''
- Test Date.
- '''
+ 'Test Date'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
today = datetime.date(2009, 1, 1)
@@ -1756,9 +1567,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0090datetime(self):
- '''
- Test DateTime.
- '''
+ 'Test DateTime'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
today = datetime.datetime(2009, 1, 1, 12, 0, 0)
@@ -2024,9 +1833,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0100time(self):
- '''
- Test Time.
- '''
+ 'Test Time'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
pre_evening = datetime.time(16, 30)
@@ -2268,9 +2075,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0110one2one(self):
- '''
- Test One2One.
- '''
+ 'Test One2One'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
target1, = self.one2one_target.create([{
@@ -2396,9 +2201,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0120one2many(self):
- '''
- Test One2Many.
- '''
+ 'Test One2Many'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
for one2many, one2many_target in (
@@ -2498,7 +2301,7 @@ class FieldsTestCase(unittest.TestCase):
one2many.write([one2many1], {
'targets': [
- ('unlink', [target2.id]),
+ ('remove', [target2.id]),
],
})
self.assertEqual(one2many1.targets, (target1,))
@@ -2509,7 +2312,7 @@ class FieldsTestCase(unittest.TestCase):
one2many.write([one2many1], {
'targets': [
- ('unlink_all',),
+ ('remove', [target1.id]),
],
})
self.assertEqual(one2many1.targets, ())
@@ -2520,7 +2323,7 @@ class FieldsTestCase(unittest.TestCase):
one2many.write([one2many1], {
'targets': [
- ('set', [target1.id, target2.id]),
+ ('add', [target1.id, target2.id]),
],
})
self.assertEqual(one2many1.targets,
@@ -2528,23 +2331,37 @@ class FieldsTestCase(unittest.TestCase):
one2many.write([one2many1], {
'targets': [
- ('delete', [target2.id]),
+ ('copy', [target1.id], {'name': 'copy1'}),
],
})
- self.assertEqual(one2many1.targets, (target1,))
targets = one2many_target.search([
- ('id', '=', target2.id),
+ ('id', 'not in', [target1.id, target2.id]),
])
- self.assertEqual(targets, [])
+ self.assertEqual(len(targets), 1)
+ self.assertEqual(targets[0].name, 'copy1')
one2many.write([one2many1], {
'targets': [
- ('delete_all',),
+ ('copy', [target2.id]),
],
})
- self.assertEqual(one2many1.targets, ())
+ self.assertEqual(len(one2many1.targets), 4)
+ targets = one2many_target.search([
+ ('id', 'not in', [target1.id, target2.id]),
+ ])
+ self.assertEqual(len(targets), 2)
+ names = set([target.name for target in targets])
+ self.assertEqual(names, set(('copy1', 'target2')))
+
+ copy_ids = [target.id for target in targets]
+ one2many.write([one2many1], {
+ 'targets': [
+ ('delete', [target2.id] + copy_ids),
+ ],
+ })
+ self.assertEqual(one2many1.targets, (target1,))
targets = one2many_target.search([
- ('id', '=', target1.id),
+ ('id', '=', target2.id),
])
self.assertEqual(targets, [])
@@ -2583,9 +2400,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0130many2many(self):
- '''
- Test Many2Many.
- '''
+ 'Test Many2Many'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
for many2many, many2many_target in (
@@ -2680,7 +2495,7 @@ class FieldsTestCase(unittest.TestCase):
many2many.write([many2many1], {
'targets': [
- ('unlink', [target2.id]),
+ ('remove', [target2.id]),
],
})
self.assertEqual(many2many1.targets, (target1,))
@@ -2691,7 +2506,7 @@ class FieldsTestCase(unittest.TestCase):
many2many.write([many2many1], {
'targets': [
- ('unlink_all',),
+ ('remove', [target1.id]),
],
})
self.assertEqual(many2many1.targets, ())
@@ -2702,7 +2517,7 @@ class FieldsTestCase(unittest.TestCase):
many2many.write([many2many1], {
'targets': [
- ('set', [target1.id, target2.id]),
+ ('add', [target1.id, target2.id]),
],
})
self.assertEqual(many2many1.targets,
@@ -2710,23 +2525,37 @@ class FieldsTestCase(unittest.TestCase):
many2many.write([many2many1], {
'targets': [
- ('delete', [target2.id]),
+ ('copy', [target1.id], {'name': 'copy1'}),
],
})
- self.assertEqual(many2many1.targets, (target1,))
targets = many2many_target.search([
- ('id', '=', target2.id),
+ ('id', 'not in', [target1.id, target2.id]),
])
- self.assertEqual(targets, [])
+ self.assertEqual(len(targets), 1)
+ self.assertEqual(targets[0].name, 'copy1')
many2many.write([many2many1], {
'targets': [
- ('delete_all',),
+ ('copy', [target2.id]),
],
})
- self.assertEqual(many2many1.targets, ())
+ self.assertEqual(len(many2many1.targets), 4)
+ targets = many2many_target.search([
+ ('id', 'not in', [target1.id, target2.id]),
+ ])
+ self.assertEqual(len(targets), 2)
+ names = set([target.name for target in targets])
+ self.assertEqual(names, set(('copy1', 'target2')))
+
+ copy_ids = [target.id for target in targets]
+ many2many.write([many2many1], {
+ 'targets': [
+ ('delete', [target2.id] + copy_ids),
+ ],
+ })
+ self.assertEqual(many2many1.targets, (target1,))
targets = many2many_target.search([
- ('id', '=', target1.id),
+ ('id', '=', target2.id),
])
self.assertEqual(targets, [])
@@ -2751,19 +2580,10 @@ class FieldsTestCase(unittest.TestCase):
'name': str(i),
} for i in range(6)])
- self.many2many_size.create([{
- 'targets': [('set', size_targets[:5])],
- }])
- self.assertRaises(Exception, self.many2many_size.create, [{
- 'targets': [('set', size_targets)],
- }])
-
transaction.cursor.rollback()
def test0140reference(self):
- '''
- Test Reference.
- '''
+ 'Test Reference'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
target1, = self.reference_target.create([{
@@ -2895,9 +2715,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0150property(self):
- '''
- Test Property with supported field types.
- '''
+ 'Test Property with supported field types'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
@@ -3164,6 +2982,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0160selection(self):
+ 'Test Selection'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
selection1, = self.selection.create([{'select': 'arabic'}])
@@ -3212,6 +3031,7 @@ class FieldsTestCase(unittest.TestCase):
self.assertEqual(selection6.select, 'latin')
def test0170dict(self):
+ 'Test Dict'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
dict1, = self.dict_.create([{
@@ -3239,6 +3059,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0180binary(self):
+ 'Test Binary'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
bin1, = self.binary.create([{
@@ -3272,6 +3093,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0190many2one(self):
+ 'Test Many2One'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
@@ -3297,7 +3119,3 @@ class FieldsTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(FieldsTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_history.py b/trytond/tests/test_history.py
new file mode 100644
index 0000000..5b4c064
--- /dev/null
+++ b/trytond/tests/test_history.py
@@ -0,0 +1,178 @@
+#This file is part of Tryton. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+import unittest
+import datetime
+
+from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
+ install_module
+from trytond.transaction import Transaction
+from trytond.exceptions import UserError
+from trytond.config import CONFIG
+
+
+class HistoryTestCase(unittest.TestCase):
+ 'Test History'
+
+ def setUp(self):
+ install_module('tests')
+
+ def test0010read(self):
+ 'Test read history'
+ History = POOL.get('test.history')
+
+ # Create some history entry
+ # It is needed to commit to have different timestamps
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(value=1)
+ history.save()
+ history_id = history.id
+ first = history.create_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(history_id)
+ history.value = 2
+ history.save()
+ second = history.write_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(history_id)
+ history.value = 3
+ history.save()
+ third = history.write_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ for timestamp, value in [
+ (first, 1),
+ (second, 2),
+ (third, 3),
+ (datetime.datetime.now(), 3),
+ (datetime.datetime.max, 3),
+ ]:
+ with Transaction().set_context(_datetime=timestamp):
+ history = History(history_id)
+ self.assertEqual(history.value, value)
+
+ with Transaction().set_context(_datetime=datetime.datetime.min):
+ self.assertRaises(UserError, History.read, [history_id])
+
+ @unittest.skipIf(CONFIG['db_type'] in ('sqlite', 'mysql'),
+ 'now() is not the start of the transaction')
+ def test0020read_same_timestamp(self):
+ 'Test read history with same timestamp'
+ History = POOL.get('test.history')
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(value=1)
+ history.save()
+ history_id = history.id
+ first = history.create_date
+
+ history.value = 2
+ history.save()
+ second = history.write_date
+
+ self.assertEqual(first, second)
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(history_id)
+ history.value = 3
+ history.save()
+ third = history.write_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ for timestamp, value in [
+ (first, 2),
+ (third, 3),
+ ]:
+ with Transaction().set_context(_datetime=timestamp):
+ history = History(history_id)
+ self.assertEqual(history.value, value)
+
+ def test0030history_revisions(self):
+ 'Test history revisions'
+ History = POOL.get('test.history')
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(value=1)
+ history.save()
+ history_id = history.id
+ first = history.create_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(history_id)
+ history.value = 2
+ history.save()
+ second = history.write_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(history_id)
+ history.value = 3
+ history.save()
+ third = history.write_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ revisions = History.history_revisions([history_id])
+ self.assertEqual(revisions, [
+ (third, history_id, u'Administrator'),
+ (second, history_id, u'Administrator'),
+ (first, history_id, u'Administrator'),
+ ])
+
+ def test0040restore_history(self):
+ 'Test restore history'
+ History = POOL.get('test.history')
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(value=1)
+ history.save()
+ history_id = history.id
+ first = history.create_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(history_id)
+ history.value = 2
+ history.save()
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ History.restore_history([history_id], first)
+ history = History(history_id)
+ self.assertEqual(history.value, 1)
+
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ History.restore_history([history_id], datetime.datetime.min)
+ self.assertRaises(UserError, History.read, [history_id])
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(HistoryTestCase)
diff --git a/trytond/tests/test_importdata.py b/trytond/tests/test_importdata.py
index a3dc761..8718032 100644
--- a/trytond/tests/test_importdata.py
+++ b/trytond/tests/test_importdata.py
@@ -1,17 +1,17 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import unittest
+from decimal import InvalidOperation
+
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
install_module
from trytond.transaction import Transaction
+from trytond.exceptions import UserError
class ImportDataTestCase(unittest.TestCase):
- '''
- Test import_data.
- '''
+ 'Test import_data'
def setUp(self):
install_module('tests')
@@ -24,7 +24,6 @@ class ImportDataTestCase(unittest.TestCase):
self.numeric_required = POOL.get('test.import_data.numeric_required')
self.char = POOL.get('test.import_data.char')
self.text = POOL.get('test.import_data.text')
- self.sha = POOL.get('test.import_data.sha')
self.date = POOL.get('test.import_data.date')
self.datetime = POOL.get('test.import_data.datetime')
self.selection = POOL.get('test.import_data.selection')
@@ -34,450 +33,368 @@ class ImportDataTestCase(unittest.TestCase):
self.reference = POOL.get('test.import_data.reference')
def test0010boolean(self):
- '''
- Test boolean.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
+ 'Test boolean'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
self.assertEqual(self.boolean.import_data(['boolean'],
- [['True']]), (1, 0, 0, 0))
+ [['True']]), 1)
self.assertEqual(self.boolean.import_data(['boolean'],
- [['1']]), (1, 0, 0, 0))
+ [['1']]), 1)
self.assertEqual(self.boolean.import_data(['boolean'],
- [['False']]), (1, 0, 0, 0))
+ [['False']]), 1)
self.assertEqual(self.boolean.import_data(['boolean'],
- [['0']]), (1, 0, 0, 0))
+ [['0']]), 1)
self.assertEqual(self.boolean.import_data(['boolean'],
- [['']]), (1, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.boolean.import_data(['boolean'],
- [['True'], ['False']]), (2, 0, 0, 0))
+ [['True'], ['False']]), 2)
- self.assertEqual(self.boolean.import_data(['boolean'],
- [['foo']])[0], -1)
-
- transaction.cursor.rollback()
+ self.assertRaises(ValueError, self.boolean.import_data,
+ ['boolean'], [['foo']])
def test0020integer(self):
- '''
- Test integer.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
+ 'Test integer'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
self.assertEqual(self.integer.import_data(['integer'],
- [['1']]), (1, 0, 0, 0))
+ [['1']]), 1)
self.assertEqual(self.integer.import_data(['integer'],
- [['-1']]), (1, 0, 0, 0))
+ [['-1']]), 1)
self.assertEqual(self.integer.import_data(['integer'],
- [['']]), (1, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.integer.import_data(['integer'],
- [['1'], ['2']]), (2, 0, 0, 0))
+ [['1'], ['2']]), 2)
- self.assertEqual(self.integer.import_data(['integer'],
- [['1.1']])[0], -1)
+ self.assertRaises(ValueError, self.integer.import_data,
+ ['integer'], [['1.1']])
- self.assertEqual(self.integer.import_data(['integer'],
- [['-1.1']])[0], -1)
+ self.assertRaises(ValueError, self.integer.import_data,
+ ['integer'], [['-1.1']])
- self.assertEqual(self.integer.import_data(['integer'],
- [['foo']])[0], -1)
+ self.assertRaises(ValueError, self.integer.import_data,
+ ['integer'], [['foo']])
self.assertEqual(self.integer.import_data(['integer'],
- [['0']]), (1, 0, 0, 0))
-
- transaction.cursor.rollback()
+ [['0']]), 1)
def test0021integer_required(self):
- '''
- Test required integer.
- '''
+ 'Test required integer'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
self.assertEqual(self.integer_required.import_data(['integer'],
- [['1']]), (1, 0, 0, 0))
+ [['1']]), 1)
self.assertEqual(self.integer_required.import_data(['integer'],
- [['-1']]), (1, 0, 0, 0))
+ [['-1']]), 1)
- self.assertEqual(self.integer_required.import_data(['integer'],
- [['']])[0], -1)
+ self.assertRaises(UserError, self.integer_required.import_data,
+ ['integer'], [['']])
+ transaction.cursor.rollback()
self.assertEqual(self.integer_required.import_data(['integer'],
- [['1'], ['2']]), (2, 0, 0, 0))
+ [['1'], ['2']]), 2)
- self.assertEqual(self.integer_required.import_data(['integer'],
- [['1.1']])[0], -1)
+ self.assertRaises(ValueError, self.integer_required.import_data,
+ ['integer'], [['1.1']])
- self.assertEqual(self.integer_required.import_data(['integer'],
- [['-1.1']])[0], -1)
+ self.assertRaises(ValueError, self.integer_required.import_data,
+ ['integer'], [['-1.1']])
- self.assertEqual(self.integer_required.import_data(['integer'],
- [['foo']])[0], -1)
+ self.assertRaises(ValueError, self.integer_required.import_data,
+ ['integer'], [['foo']])
self.assertEqual(self.integer_required.import_data(['integer'],
- [['0']]), (1, 0, 0, 0))
-
- transaction.cursor.rollback()
+ [['0']]), 1)
def test0030float(self):
- '''
- Test float.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
+ 'Test float'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
self.assertEqual(self.float.import_data(['float'],
- [['1.1']]), (1, 0, 0, 0))
+ [['1.1']]), 1)
self.assertEqual(self.float.import_data(['float'],
- [['-1.1']]), (1, 0, 0, 0))
+ [['-1.1']]), 1)
self.assertEqual(self.float.import_data(['float'],
- [['1']]), (1, 0, 0, 0))
+ [['1']]), 1)
self.assertEqual(self.float.import_data(['float'],
- [['']]), (1, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.float.import_data(['float'],
- [['1.1'], ['2.2']]), (2, 0, 0, 0))
+ [['1.1'], ['2.2']]), 2)
- self.assertEqual(self.float.import_data(['float'],
- [['foo']])[0], -1)
+ self.assertRaises(ValueError, self.float.import_data,
+ ['float'], [['foo']])
self.assertEqual(self.float.import_data(['float'],
- [['0']]), (1, 0, 0, 0))
+ [['0']]), 1)
self.assertEqual(self.float.import_data(['float'],
- [['0.0']]), (1, 0, 0, 0))
-
- transaction.cursor.rollback()
+ [['0.0']]), 1)
def test0031float_required(self):
- '''
- Test required float.
- '''
+ 'Test required float'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
self.assertEqual(self.float_required.import_data(['float'],
- [['1.1']]), (1, 0, 0, 0))
+ [['1.1']]), 1)
self.assertEqual(self.float_required.import_data(['float'],
- [['-1.1']]), (1, 0, 0, 0))
+ [['-1.1']]), 1)
self.assertEqual(self.float_required.import_data(['float'],
- [['1']]), (1, 0, 0, 0))
+ [['1']]), 1)
- self.assertEqual(self.float_required.import_data(['float'],
- [['']])[0], -1)
+ self.assertRaises(UserError, self.float_required.import_data,
+ ['float'], [['']])
+ transaction.cursor.rollback()
self.assertEqual(self.float_required.import_data(['float'],
- [['1.1'], ['2.2']]), (2, 0, 0, 0))
+ [['1.1'], ['2.2']]), 2)
- self.assertEqual(self.float_required.import_data(['float'],
- [['foo']])[0], -1)
+ self.assertRaises(ValueError, self.float_required.import_data,
+ ['float'], [['foo']])
self.assertEqual(self.float_required.import_data(['float'],
- [['0']]), (1, 0, 0, 0))
+ [['0']]), 1)
self.assertEqual(self.float_required.import_data(['float'],
- [['0.0']]), (1, 0, 0, 0))
-
- transaction.cursor.rollback()
+ [['0.0']]), 1)
def test0040numeric(self):
- '''
- Test numeric.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
+ 'Test numeric'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
self.assertEqual(self.numeric.import_data(['numeric'],
- [['1.1']]), (1, 0, 0, 0))
+ [['1.1']]), 1)
self.assertEqual(self.numeric.import_data(['numeric'],
- [['-1.1']]), (1, 0, 0, 0))
+ [['-1.1']]), 1)
self.assertEqual(self.numeric.import_data(['numeric'],
- [['1']]), (1, 0, 0, 0))
+ [['1']]), 1)
self.assertEqual(self.numeric.import_data(['numeric'],
- [['']]), (1, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.numeric.import_data(['numeric'],
- [['1.1'], ['2.2']]), (2, 0, 0, 0))
+ [['1.1'], ['2.2']]), 2)
- self.assertEqual(self.numeric.import_data(['numeric'],
- [['foo']])[0], -1)
+ self.assertRaises(InvalidOperation, self.numeric.import_data,
+ ['numeric'], [['foo']])
self.assertEqual(self.numeric.import_data(['numeric'],
- [['0']]), (1, 0, 0, 0))
+ [['0']]), 1)
self.assertEqual(self.numeric.import_data(['numeric'],
- [['0.0']]), (1, 0, 0, 0))
-
- transaction.cursor.rollback()
+ [['0.0']]), 1)
def test0041numeric_required(self):
- '''
- Test required numeric.
- '''
+ 'Test required numeric'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
self.assertEqual(self.numeric_required.import_data(['numeric'],
- [['1.1']]), (1, 0, 0, 0))
+ [['1.1']]), 1)
self.assertEqual(self.numeric_required.import_data(['numeric'],
- [['-1.1']]), (1, 0, 0, 0))
+ [['-1.1']]), 1)
self.assertEqual(self.numeric_required.import_data(['numeric'],
- [['1']]), (1, 0, 0, 0))
+ [['1']]), 1)
- self.assertEqual(self.numeric_required.import_data(['numeric'],
- [['']])[0], -1)
+ self.assertRaises(UserError, self.numeric_required.import_data,
+ ['numeric'], [['']])
+ transaction.cursor.rollback()
self.assertEqual(self.numeric_required.import_data(['numeric'],
- [['1.1'], ['2.2']]), (2, 0, 0, 0))
+ [['1.1'], ['2.2']]), 2)
- self.assertEqual(self.numeric_required.import_data(['numeric'],
- [['foo']])[0], -1)
+ self.assertRaises(InvalidOperation,
+ self.numeric_required.import_data, ['numeric'], [['foo']])
self.assertEqual(self.numeric_required.import_data(['numeric'],
- [['0']]), (1, 0, 0, 0))
+ [['0']]), 1)
self.assertEqual(self.numeric_required.import_data(['numeric'],
- [['0.0']]), (1, 0, 0, 0))
-
- transaction.cursor.rollback()
+ [['0.0']]), 1)
def test0050char(self):
- '''
- Test char.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
+ 'Test char'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
self.assertEqual(self.char.import_data(['char'],
- [['test']]), (1, 0, 0, 0))
+ [['test']]), 1)
self.assertEqual(self.char.import_data(['char'],
- [['']]), (1, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.char.import_data(['char'],
- [['test'], ['foo'], ['bar']]), (3, 0, 0, 0))
-
- transaction.cursor.rollback()
+ [['test'], ['foo'], ['bar']]), 3)
def test0060text(self):
- '''
- Test text.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
+ 'Test text'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
self.assertEqual(self.text.import_data(['text'],
- [['test']]), (1, 0, 0, 0))
+ [['test']]), 1)
self.assertEqual(self.text.import_data(['text'],
- [['']]), (1, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.text.import_data(['text'],
- [['test'], ['foo'], ['bar']]), (3, 0, 0, 0))
-
- transaction.cursor.rollback()
-
- def test0070sha(self):
- '''
- Test sha.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
- self.assertEqual(self.sha.import_data(['sha'],
- [['test']]), (1, 0, 0, 0))
-
- self.assertEqual(self.sha.import_data(['sha'],
- [['']]), (1, 0, 0, 0))
-
- self.assertEqual(self.sha.import_data(['sha'],
- [['test'], ['foo']]), (2, 0, 0, 0))
-
- transaction.cursor.rollback()
+ [['test'], ['foo'], ['bar']]), 3)
def test0080date(self):
- '''
- Test date.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
- self.assertEqual(self.date.import_data(['date'],
- [['2010-01-01']]), (1, 0, 0, 0))
-
+ 'Test date'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
self.assertEqual(self.date.import_data(['date'],
- [['']]), (1, 0, 0, 0))
+ [['2010-01-01']]), 1)
self.assertEqual(self.date.import_data(['date'],
- [['2010-01-01'], ['2010-02-01']]), (2, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.date.import_data(['date'],
- [['foo']])[0], -1)
+ [['2010-01-01'], ['2010-02-01']]), 2)
- transaction.cursor.rollback()
+ self.assertRaises(ValueError, self.date.import_data,
+ ['date'], [['foo']])
def test0090datetime(self):
- '''
- Test datetime.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
- self.assertEqual(self.datetime.import_data(['datetime'],
- [['2010-01-01 12:00:00']]), (1, 0, 0, 0))
-
+ 'Test datetime'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
self.assertEqual(self.datetime.import_data(['datetime'],
- [['']]), (1, 0, 0, 0))
+ [['2010-01-01 12:00:00']]), 1)
self.assertEqual(self.datetime.import_data(['datetime'],
- [['2010-01-01 12:00:00'], ['2010-01-01 13:30:00']]),
- (2, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.datetime.import_data(['datetime'],
- [['foo']])[0], -1)
+ [['2010-01-01 12:00:00'], ['2010-01-01 13:30:00']]), 2)
- transaction.cursor.rollback()
+ self.assertRaises(ValueError, self.datetime.import_data,
+ ['datetime'], [['foo']])
def test0100selection(self):
- '''
- Test selection.
- '''
+ 'Test selection'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
self.assertEqual(self.selection.import_data(['selection'],
- [['select1']]), (1, 0, 0, 0))
-
- self.assertEqual(self.selection.import_data(['selection'],
- [['']]), (1, 0, 0, 0))
+ [['select1']]), 1)
self.assertEqual(self.selection.import_data(['selection'],
- [['select1'], ['select2']]), (2, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.selection.import_data(['selection'],
- [['foo']])[0], -1)
+ [['select1'], ['select2']]), 2)
+ self.assertRaises(UserError, self.selection.import_data,
+ ['selection'], [['foo']])
transaction.cursor.rollback()
def test0110many2one(self):
- '''
- Test many2one.
- '''
+ 'Test many2one'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
self.assertEqual(self.many2one.import_data(['many2one'],
- [['Test']]), (1, 0, 0, 0))
+ [['Test']]), 1)
self.assertEqual(self.many2one.import_data(['many2one:id'],
- [['tests.import_data_many2one_target_test']]), (1, 0, 0, 0))
+ [['tests.import_data_many2one_target_test']]), 1)
self.assertEqual(self.many2one.import_data(['many2one'],
- [['']]), (1, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.many2one.import_data(['many2one'],
- [['Test'], ['Test']]), (2, 0, 0, 0))
+ [['Test'], ['Test']]), 2)
- self.assertEqual(self.many2one.import_data(['many2one'],
- [['foo']])[0], -1)
-
- self.assertEqual(self.many2one.import_data(['many2one'],
- [['Duplicate']])[0], -1)
+ self.assertRaises(UserError, self.many2one.import_data,
+ ['many2one'], [['foo']])
+ transaction.cursor.rollback()
- self.assertEqual(self.many2one.import_data(['many2one:id'],
- [['foo']])[0], -1)
+ self.assertRaises(UserError, self.many2one.import_data,
+ ['many2one'], [['Duplicate']])
+ transaction.cursor.rollback()
- self.assertEqual(self.many2one.import_data(['many2one:id'],
- [['tests.foo']])[0], -1)
+ self.assertRaises(UserError, self.many2one.import_data,
+ ['many2one:id'], [['foo']])
+ transaction.cursor.rollback()
+ self.assertRaises(Exception, self.many2one.import_data,
+ ['many2one:id'], [['tests.foo']])
transaction.cursor.rollback()
def test0120many2many(self):
- '''
- Test many2many.
- '''
+ 'Test many2many'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
self.assertEqual(self.many2many.import_data(['many2many'],
- [['Test 1']]), (1, 0, 0, 0))
+ [['Test 1']]), 1)
self.assertEqual(self.many2many.import_data(['many2many:id'],
- [['tests.import_data_many2many_target_test1']]), (1, 0, 0, 0))
+ [['tests.import_data_many2many_target_test1']]), 1)
self.assertEqual(self.many2many.import_data(['many2many'],
- [['Test 1,Test 2']]), (1, 0, 0, 0))
+ [['Test 1,Test 2']]), 1)
self.assertEqual(self.many2many.import_data(['many2many:id'],
[['tests.import_data_many2many_target_test1,'
- 'tests.import_data_many2many_target_test2']]),
- (1, 0, 0, 0))
-
- self.assertEqual(self.many2many.import_data(['many2many'],
- [['Test\, comma']]), (1, 0, 0, 0))
+ 'tests.import_data_many2many_target_test2']]), 1)
self.assertEqual(self.many2many.import_data(['many2many'],
- [['Test\, comma,Test 1']]), (1, 0, 0, 0))
+ [['Test\, comma']]), 1)
self.assertEqual(self.many2many.import_data(['many2many'],
- [['']]), (1, 0, 0, 0))
+ [['Test\, comma,Test 1']]), 1)
self.assertEqual(self.many2many.import_data(['many2many'],
- [['Test 1'], ['Test 2']]), (2, 0, 0, 0))
+ [['']]), 1)
self.assertEqual(self.many2many.import_data(['many2many'],
- [['foo']])[0], -1)
+ [['Test 1'], ['Test 2']]), 2)
- self.assertEqual(self.many2many.import_data(['many2many'],
- [['Test 1,foo']])[0], -1)
+ self.assertRaises(UserError, self.many2many.import_data,
+ ['many2many'], [['foo']])
+ transaction.cursor.rollback()
- self.assertEqual(self.many2many.import_data(['many2many'],
- [['Duplicate']])[0], -1)
+ self.assertRaises(UserError, self.many2many.import_data,
+ ['many2many'], [['Test 1,foo']])
+ transaction.cursor.rollback()
- self.assertEqual(self.many2many.import_data(['many2many'],
- [['Test 1,Duplicate']])[0], -1)
+ self.assertRaises(UserError, self.many2many.import_data,
+ ['many2many'], [['Duplicate']])
+ transaction.cursor.rollback()
+ self.assertRaises(UserError, self.many2many.import_data,
+ ['many2many'], [['Test 1,Duplicate']])
transaction.cursor.rollback()
def test0130one2many(self):
- '''
- Test one2many.
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
+ 'Test one2many'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
self.assertEqual(self.one2many.import_data(
- ['name', 'one2many/name'], [['Test', 'Test 1']]),
- (1, 0, 0, 0))
+ ['name', 'one2many/name'], [['Test', 'Test 1']]), 1)
self.assertEqual(self.one2many.import_data(
- ['name', 'one2many/name'], [
- ['Test', 'Test 1'], ['', 'Test 2']]),
- (1, 0, 0, 0))
+ ['name', 'one2many/name'],
+ [['Test', 'Test 1'], ['', 'Test 2']]), 1)
self.assertEqual(self.one2many.import_data(
['name', 'one2many/name'],
[
['Test 1', 'Test 1'],
['', 'Test 2'],
- ['Test 2', 'Test 1']]),
- (2, 0, 0, 0))
-
- transaction.cursor.rollback()
+ ['Test 2', 'Test 1']]), 2)
def test0140reference(self):
- '''
- Test reference.
- '''
+ 'Test reference'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
self.assertEqual(self.reference.import_data(['reference'],
- [['test.import_data.reference.selection,Test']]),
- (1, 0, 0, 0))
+ [['test.import_data.reference.selection,Test']]), 1)
reference, = self.reference.search([])
self.assertEqual(reference.reference.__name__,
'test.import_data.reference.selection')
@@ -485,46 +402,45 @@ class ImportDataTestCase(unittest.TestCase):
self.assertEqual(self.reference.import_data(['reference:id'],
[['test.import_data.reference.selection,'
- 'tests.import_data_reference_selection_test']]),
- (1, 0, 0, 0))
+ 'tests.import_data_reference_selection_test']]), 1)
reference, = self.reference.search([])
self.assertEqual(reference.reference.__name__,
'test.import_data.reference.selection')
transaction.cursor.rollback()
self.assertEqual(self.reference.import_data(['reference'],
- [['']]), (1, 0, 0, 0))
+ [['']]), 1)
reference, = self.reference.search([])
self.assertEqual(reference.reference, None)
transaction.cursor.rollback()
self.assertEqual(self.reference.import_data(['reference'],
[['test.import_data.reference.selection,Test'],
- ['test.import_data.reference.selection,Test']]),
- (2, 0, 0, 0))
+ ['test.import_data.reference.selection,Test']]), 2)
for reference in self.reference.search([]):
self.assertEqual(reference.reference.__name__,
'test.import_data.reference.selection')
transaction.cursor.rollback()
- self.assertEqual(self.reference.import_data(['reference'],
- [['test.import_data.reference.selection,foo']])[0], -1)
-
- self.assertEqual(self.reference.import_data(['reference'],
- [['test.import_data.reference.selection,Duplicate']])[0], -1)
+ self.assertRaises(UserError, self.reference.import_data,
+ ['reference'], [['test.import_data.reference.selection,foo']])
+ transaction.cursor.rollback()
- self.assertEqual(self.reference.import_data(['reference:id'],
- [['test.import_data.reference.selection,foo']])[0], -1)
+ self.assertRaises(UserError, self.reference.import_data,
+ ['reference'],
+ [['test.import_data.reference.selection,Duplicate']])
+ transaction.cursor.rollback()
- self.assertEqual(self.reference.import_data(['reference:id'],
- [['test.import_data.reference.selection,test.foo']])[0], -1)
+ self.assertRaises(UserError, self.reference.import_data,
+ ['reference:id'],
+ [['test.import_data.reference.selection,foo']])
+ transaction.cursor.rollback()
+ self.assertRaises(Exception, self.reference.import_data,
+ ['reference:id'],
+ [['test.import_data.reference.selection,test.foo']])
transaction.cursor.rollback()
def suite():
return unittest.TestLoader().loadTestsFromTestCase(ImportDataTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_mixins.py b/trytond/tests/test_mixins.py
index be6ecf2..a699f7b 100644
--- a/trytond/tests/test_mixins.py
+++ b/trytond/tests/test_mixins.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -46,7 +45,3 @@ def suite():
for testcase in (UrlTestCase,):
suite.addTests(func(testcase))
return suite
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_modelsingleton.py b/trytond/tests/test_modelsingleton.py
index a066d38..673e668 100644
--- a/trytond/tests/test_modelsingleton.py
+++ b/trytond/tests/test_modelsingleton.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -10,18 +9,14 @@ from trytond.transaction import Transaction
class ModelSingletonTestCase(unittest.TestCase):
- '''
- Test ModelSingleton
- '''
+ 'Test ModelSingleton'
def setUp(self):
install_module('tests')
self.singleton = POOL.get('test.singleton')
def test0010read(self):
- '''
- Test read method.
- '''
+ 'Test read method'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
singleton, = self.singleton.read([1], ['name'])
@@ -48,9 +43,7 @@ class ModelSingletonTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0020create(self):
- '''
- Test create method.
- '''
+ 'Test create method'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
singleton, = self.singleton.create([{'name': 'bar'}])
@@ -68,9 +61,7 @@ class ModelSingletonTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0030copy(self):
- '''
- Test copy method.
- '''
+ 'Test copy method'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
singleton, = self.singleton.search([])
@@ -90,9 +81,7 @@ class ModelSingletonTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0040default_get(self):
- '''
- Test default_get method.
- '''
+ 'Test default_get method'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
default = self.singleton.default_get(['name'])
@@ -120,9 +109,7 @@ class ModelSingletonTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0050search(self):
- '''
- Test search method.
- '''
+ 'Test search method'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
singletons = self.singleton.search([])
@@ -139,7 +126,3 @@ class ModelSingletonTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(ModelSingletonTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_modelsql.py b/trytond/tests/test_modelsql.py
index 2bedafc..233ed9a 100644
--- a/trytond/tests/test_modelsql.py
+++ b/trytond/tests/test_modelsql.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of Tryton. The COPYRIGHT file at the top level of this
# repository contains the full copyright notices and license terms.
@@ -14,9 +13,7 @@ from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
class ModelSQLTestCase(unittest.TestCase):
- '''
- Test ModelSQL
- '''
+ 'Test ModelSQL'
def setUp(self):
install_module('tests')
@@ -24,9 +21,7 @@ class ModelSQLTestCase(unittest.TestCase):
self.modelsql_timestamp = POOL.get('test.modelsql.timestamp')
def test0010required_field_missing(self):
- '''
- Test error message when a required field is missing.
- '''
+ 'Test error message when a required field is missing'
if CONFIG['db_type'] not in ('postgresql', 'mysql'):
# SQLite not concerned because tryton don't set "NOT NULL"
# constraint: 'ALTER TABLE' don't support NOT NULL constraint
@@ -49,9 +44,7 @@ class ModelSQLTestCase(unittest.TestCase):
self.fail('UserError should be caught')
def test0020check_timestamp(self):
- '''
- Test check timestamp.
- '''
+ 'Test check timestamp'
# cursor must be committed between each changes otherwise NOW() returns
# always the same timestamp.
with Transaction().start(DB_NAME, USER,
@@ -63,7 +56,7 @@ class ModelSQLTestCase(unittest.TestCase):
timestamp = self.modelsql_timestamp.read([record.id],
['_timestamp'])[0]['_timestamp']
- if CONFIG['db_type'] == 'sqlite':
+ if CONFIG['db_type'] in ('sqlite', 'mysql'):
# timestamp precision of sqlite is the second
time.sleep(1)
@@ -87,6 +80,3 @@ class ModelSQLTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(ModelSQLTestCase)
-
-if __name__ == '__main__':
- unittest.TextTestRunner(verbosity=2).run(suite())
diff --git a/trytond/tests/test_mptt.py b/trytond/tests/test_mptt.py
index 5b5fd49..7631456 100644
--- a/trytond/tests/test_mptt.py
+++ b/trytond/tests/test_mptt.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -10,9 +9,7 @@ from trytond.transaction import Transaction
class MPTTTestCase(unittest.TestCase):
- '''
- Test Modified Preorder Tree Traversal.
- '''
+ 'Test Modified Preorder Tree Traversal'
def setUp(self):
install_module('tests')
@@ -66,9 +63,7 @@ class MPTTTestCase(unittest.TestCase):
self.ReParent(record)
def test0010create(self):
- '''
- Create tree.
- '''
+ 'Test create tree'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
new_records = [None, None, None]
@@ -88,15 +83,14 @@ class MPTTTestCase(unittest.TestCase):
transaction.cursor.commit()
def test0030reparent(self):
- '''
- Re-parent.
- '''
+ 'Test re-parent'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
self.ReParent()
transaction.cursor.rollback()
def test0040active(self):
+ 'Test active'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
records = self.mptt.search([])
@@ -142,9 +136,7 @@ class MPTTTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0050delete(self):
- '''
- Delete.
- '''
+ 'Test delete'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
records = self.mptt.search([])
@@ -174,7 +166,3 @@ class MPTTTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(MPTTTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_pyson.py b/trytond/tests/test_pyson.py
index 8687c32..d295b98 100644
--- a/trytond/tests/test_pyson.py
+++ b/trytond/tests/test_pyson.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -9,14 +8,10 @@ from trytond import pyson
class PYSONTestCase(unittest.TestCase):
- '''
- Test PySON.
- '''
+ 'Test PySON'
def test0010Eval(self):
- '''
- Test pyson.Eval
- '''
+ 'Test pyson.Eval'
self.assert_(pyson.Eval('test').pyson() == {
'__class__': 'Eval',
'v': 'test',
@@ -36,9 +31,7 @@ class PYSONTestCase(unittest.TestCase):
self.assert_(pyson.PYSONDecoder().decode(eval) == 0)
def test0020Not(self):
- '''
- Test pyson.Not
- '''
+ 'Test pyson.Not'
self.assert_(pyson.Not(True).pyson() == {
'__class__': 'Not',
'v': True,
@@ -55,9 +48,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertTrue(pyson.PYSONDecoder().decode(eval))
def test0030Bool(self):
- '''
- Test pyson.Bool
- '''
+ 'Test pyson.Bool'
self.assert_(pyson.Bool('test').pyson() == {
'__class__': 'Bool',
'v': 'test',
@@ -96,9 +87,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertFalse(pyson.PYSONDecoder().decode(eval))
def test0040And(self):
- '''
- Test pyson.And
- '''
+ 'Test pyson.And'
self.assert_(pyson.And(True, False).pyson() == {
'__class__': 'And',
's': [True, False],
@@ -140,9 +129,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertFalse(pyson.PYSONDecoder().decode(eval))
def test0050Or(self):
- '''
- Test pyson.Or
- '''
+ 'Test pyson.Or'
self.assert_(pyson.Or(True, False).pyson() == {
'__class__': 'Or',
's': [True, False],
@@ -184,9 +171,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertTrue(pyson.PYSONDecoder().decode(eval))
def test0060Equal(self):
- '''
- Test pyson.Equal
- '''
+ 'Test pyson.Equal'
self.assert_(pyson.Equal('test', 'test').pyson() == {
'__class__': 'Equal',
's1': 'test',
@@ -204,9 +189,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertFalse(pyson.PYSONDecoder().decode(eval))
def test0070Greater(self):
- '''
- Test pyson.Greater
- '''
+ 'Test pyson.Greater'
self.assert_(pyson.Greater(1, 0).pyson() == {
'__class__': 'Greater',
's1': 1,
@@ -239,9 +222,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertTrue(pyson.PYSONDecoder().decode(eval))
def test0080Less(self):
- '''
- Test pyson.Less
- '''
+ 'Test pyson.Less'
self.assert_(pyson.Less(0, 1).pyson() == {
'__class__': 'Less',
's1': 0,
@@ -274,9 +255,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertTrue(pyson.PYSONDecoder().decode(eval))
def test0090If(self):
- '''
- Test pyson.If
- '''
+ 'Test pyson.If'
self.assert_(pyson.If(True, 'foo', 'bar').pyson() == {
'__class__': 'If',
'c': True,
@@ -298,9 +277,7 @@ class PYSONTestCase(unittest.TestCase):
self.assert_(pyson.PYSONDecoder().decode(eval) == 'bar')
def test0100Get(self):
- '''
- Test pyson.Get
- '''
+ 'Test pyson.Get'
self.assert_(pyson.Get({'foo': 'bar'}, 'foo', 'default').pyson() == {
'__class__': 'Get',
'v': {'foo': 'bar'},
@@ -327,9 +304,7 @@ class PYSONTestCase(unittest.TestCase):
self.assert_(pyson.PYSONDecoder().decode(eval) == 'default')
def test0110In(self):
- '''
- Test pyson.In
- '''
+ 'Test pyson.In'
self.assert_(pyson.In('foo', {'foo': 'bar'}).pyson() == {
'__class__': 'In',
'k': 'foo',
@@ -372,9 +347,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertFalse(pyson.PYSONDecoder().decode(eval))
def test0120Date(self):
- '''
- Test pyson.Date
- '''
+ 'Test pyson.Date'
self.assert_(pyson.Date(2010, 1, 12, -1, 12, -7).pyson() == {
'__class__': 'Date',
'y': 2010,
@@ -426,9 +399,7 @@ class PYSONTestCase(unittest.TestCase):
== datetime.date(2010, 2, 22))
def test0130DateTime(self):
- '''
- Test pyson.DateTime
- '''
+ 'Test pyson.DateTime'
self.assert_(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0,
-1, 12, -7, 2, 15, 30, 1).pyson() == {
'__class__': 'DateTime',
@@ -525,11 +496,28 @@ class PYSONTestCase(unittest.TestCase):
self.assert_(pyson.PYSONDecoder().decode(eval)
== datetime.datetime(2010, 2, 22, 10, 30, 20, 200))
- def test0900Composite(self):
- '''
- Test Composite
- '''
+ def test0140Len(self):
+ 'Test pyson.Len'
+ self.assert_(pyson.Len([1, 2, 3]).pyson() == {
+ '__class__': 'Len',
+ 'v': [1, 2, 3],
+ })
+
+ self.assertRaises(AssertionError, pyson.Len, object())
+
+ self.assert_(pyson.Len([1, 2, 3]).types() == set([int, long]))
+
+ eval = pyson.PYSONEncoder().encode(pyson.Len([1, 2, 3]))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval), 3)
+ eval = pyson.PYSONEncoder().encode(pyson.Len({1: 2, 3: 4}))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval), 2)
+
+ eval = pyson.PYSONEncoder().encode(pyson.Len('foo bar'))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval), 7)
+
+ def test0900Composite(self):
+ 'Test Composite'
eval = pyson.PYSONEncoder().encode(['id', pyson.If(pyson.Not(
pyson.In('company', pyson.Eval('context', {}))), '=', '!='),
pyson.Get(pyson.Eval('context', {}), 'company', -1)])
@@ -541,7 +529,3 @@ class PYSONTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(PYSONTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_sequence.py b/trytond/tests/test_sequence.py
index 0e3b870..727bb1c 100644
--- a/trytond/tests/test_sequence.py
+++ b/trytond/tests/test_sequence.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -10,18 +9,14 @@ from trytond.transaction import Transaction
class SequenceTestCase(unittest.TestCase):
- '''
- Test Sequence
- '''
+ 'Test Sequence'
def setUp(self):
install_module('tests')
self.sequence = POOL.get('ir.sequence')
def test0010incremental(self):
- '''
- Test incremental
- '''
+ 'Test incremental'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
sequence, = self.sequence.create([{
@@ -47,9 +42,7 @@ class SequenceTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0020decimal_timestamp(self):
- '''
- Test Decimal Timestamp
- '''
+ 'Test Decimal Timestamp'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
sequence, = self.sequence.create([{
@@ -72,9 +65,7 @@ class SequenceTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0030hexadecimal_timestamp(self):
- '''
- Test Hexadecimal Timestamp
- '''
+ 'Test Hexadecimal Timestamp'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
sequence, = self.sequence.create([{
@@ -98,9 +89,7 @@ class SequenceTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0040prefix_suffix(self):
- '''
- Test prefix/suffix
- '''
+ 'Test prefix/suffix'
with Transaction().start(DB_NAME, USER, context=CONTEXT):
sequence, = self.sequence.create([{
'name': 'Test incremental',
@@ -123,7 +112,3 @@ class SequenceTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(SequenceTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_tools.py b/trytond/tests/test_tools.py
index 86881af..a760611 100644
--- a/trytond/tests/test_tools.py
+++ b/trytond/tests/test_tools.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
@@ -13,61 +12,45 @@ from trytond.tools import reduce_ids, safe_eval, datetime_strftime, \
class ToolsTestCase(unittest.TestCase):
- '''
- Test tools.
- '''
+ 'Test tools'
table = sql.Table('test')
def test0000reduce_ids_empty(self):
- '''
- Test reduce_ids empty list.
- '''
+ 'Test reduce_ids empty list'
self.assertEqual(reduce_ids(self.table.id, []), sql.Literal(False))
def test0010reduce_ids_continue(self):
- '''
- Test reduce_ids continue list.
- '''
+ 'Test reduce_ids continue list'
self.assertEqual(reduce_ids(self.table.id, range(10)),
sql.operators.Or(((self.table.id >= 0) & (self.table.id <= 9),)))
def test0020reduce_ids_one_hole(self):
- '''
- Test reduce_ids continue list with one hole.
- '''
+ 'Test reduce_ids continue list with one hole'
self.assertEqual(reduce_ids(self.table.id, range(10) + range(20, 30)),
((self.table.id >= 0) & (self.table.id <= 9))
| ((self.table.id >= 20) & (self.table.id <= 29)))
def test0030reduce_ids_short_continue(self):
- '''
- Test reduce_ids short continue list.
- '''
+ 'Test reduce_ids short continue list'
self.assertEqual(reduce_ids(self.table.id, range(4)),
sql.operators.Or((self.table.id.in_(range(4)),)))
def test0040reduce_ids_complex(self):
- '''
- Test reduce_ids complex list.
- '''
+ 'Test reduce_ids complex list'
self.assertEqual(reduce_ids(self.table.id,
range(10) + range(25, 30) + range(15, 20)),
(((self.table.id >= 0) & (self.table.id <= 14))
| (self.table.id.in_(range(25, 30)))))
def test0050reduce_ids_complex_small_continue(self):
- '''
- Test reduce_ids complex list with small continue.
- '''
+ 'Test reduce_ids complex list with small continue'
self.assertEqual(reduce_ids(self.table.id,
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 18, 19, 21]),
(((self.table.id >= 1) & (self.table.id <= 12))
| (self.table.id.in_([15, 18, 19, 21]))))
def test0055reduce_ids_float(self):
- '''
- Test reduce_ids with integer as float.
- '''
+ 'Test reduce_ids with integer as float'
self.assertEqual(reduce_ids(self.table.id,
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
15.0, 18.0, 19.0, 21.0]),
@@ -76,44 +59,32 @@ class ToolsTestCase(unittest.TestCase):
self.assertRaises(AssertionError, reduce_ids, self.table.id, [1.1])
def test0060safe_eval_builtin(self):
- '''
- Attempt to access a unsafe builtin.
- '''
+ 'Attempt to access a unsafe builtin'
self.assertRaises(Exception, safe_eval, "open('test.txt', 'w')")
def test0061safe_eval_getattr(self):
- '''
- Attempt to get arround direct attr access.
- '''
+ 'Attempt to get arround direct attr access'
self.assertRaises(Exception, safe_eval, "getattr(int, '__abs__')")
def test0062safe_eval_func_globals(self):
- '''
- Attempt to access global enviroment where fun was defined.
- '''
+ 'Attempt to access global enviroment where fun was defined'
self.assertRaises(Exception, safe_eval,
"def x(): pass; print x.func_globals")
def test0063safe_eval_lowlevel(self):
- '''
- Lowlevel tricks to access 'object'.
- '''
+ "Lowlevel tricks to access 'object'"
self.assertRaises(Exception, safe_eval,
"().__class__.mro()[1].__subclasses__()")
def test0070datetime_strftime(self):
- '''
- Test datetime_strftime
- '''
+ 'Test datetime_strftime'
self.assert_(datetime_strftime(datetime.date(2005, 3, 2),
'%Y-%m-%d'), '2005-03-02')
self.assert_(datetime_strftime(datetime.date(1805, 3, 2),
'%Y-%m-%d'), '1805-03-02')
def test_reduce_domain(self):
- '''
- Test reduce_domain
- '''
+ 'Test reduce_domain'
clause = ('x', '=', 'x')
tests = (
([clause], ['AND', clause]),
@@ -147,7 +118,3 @@ def suite():
for testcase in (ToolsTestCase,):
suite.addTests(func(testcase))
return suite
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_transaction.py b/trytond/tests/test_transaction.py
index 6928a98..311d806 100644
--- a/trytond/tests/test_transaction.py
+++ b/trytond/tests/test_transaction.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import unittest
@@ -29,37 +28,61 @@ def manipulate_cursor(*args, **kwargs):
class TransactionTestCase(unittest.TestCase):
- '''
- Test the Transaction Context manager
- '''
+ 'Test the Transaction Context manager'
def setUp(self):
install_module('tests')
def test0010nonexistdb(self):
- '''
- Attempt opening a transaction with a non existant DB and ensure that
- it stops cleanly and allows starting of next transaction
- '''
+ '''Attempt opening a transaction with a non existant DB
+ and ensure that it stops cleanly and allows starting of next
+ transaction'''
self.assertRaises(
Exception, empty_transaction, "Non existant DB", USER,
context=CONTEXT)
self.assertTrue(empty_transaction(DB_NAME, USER, context=CONTEXT))
def test0020cursorclose(self):
- '''
- Manipulate the cursor during the transaction so that the close in
- transaction stop fails. Ensure that this does not affect opening of
- another transaction
- '''
+ '''Manipulate the cursor during the transaction so that
+ the close in transaction stop fails.
+ Ensure that this does not affect opening of another transaction'''
self.assertRaises(
Exception, manipulate_cursor, DB_NAME, USER, context=CONTEXT)
self.assertTrue(empty_transaction(DB_NAME, USER, context=CONTEXT))
+ def test0030set_user(self):
+ 'Test set_user'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT) \
+ as transaction:
+ self.assertEqual(transaction.user, USER)
+ self.assertEqual(transaction.context.get('user'), None)
+
+ with Transaction().set_user(0):
+ self.assertEqual(transaction.user, 0)
+ self.assertEqual(transaction.context.get('user'), None)
+
+ with Transaction().set_user(0, set_context=True):
+ self.assertEqual(transaction.user, 0)
+ self.assertEqual(transaction.context.get('user'), USER)
+
+ # Nested same set_user should keep original context user
+ with Transaction().set_user(0, set_context=True):
+ self.assertEqual(transaction.user, 0)
+ self.assertEqual(transaction.context.get('user'), USER)
+
+ # Unset user context
+ with Transaction().set_user(0, set_context=False):
+ self.assertEqual(transaction.user, 0)
+ self.assertEqual(transaction.context.get('user'), None)
+
+ # set context for non root
+ self.assertRaises(ValueError,
+ Transaction().set_user, 2, set_context=True)
+
+ # not set context for non root
+ with Transaction().set_user(2):
+ self.assertEqual(transaction.user, 2)
+
def suite():
return unittest.TestLoader().loadTestsFromTestCase(TransactionTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_trigger.py b/trytond/tests/test_trigger.py
index c54aa4c..23bd815 100644
--- a/trytond/tests/test_trigger.py
+++ b/trytond/tests/test_trigger.py
@@ -1,30 +1,11 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import unittest
import time
from xmlrpclib import MAXINT
-try:
- from itertools import combinations
-except ImportError:
- def combinations(iterable, r):
- pool = tuple(iterable)
- n = len(pool)
- if r > n:
- return
- indices = range(r)
- yield tuple(pool[i] for i in indices)
- while True:
- for i in reversed(range(r)):
- if indices[i] != i + n - r:
- break
- else:
- return
- indices[i] += 1
- for j in range(i + 1, r):
- indices[j] = indices[j - 1] + 1
- yield tuple(pool[i] for i in indices)
+from itertools import combinations
+
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
install_module
from trytond.tests.trigger import TRIGGER_LOGS
@@ -32,9 +13,7 @@ from trytond.transaction import Transaction
class TriggerTestCase(unittest.TestCase):
- '''
- Test Trigger
- '''
+ 'Test Trigger'
def setUp(self):
install_module('tests')
@@ -44,9 +23,7 @@ class TriggerTestCase(unittest.TestCase):
self.model = POOL.get('ir.model')
def test0010constraints(self):
- '''
- Test constraints
- '''
+ 'Test constraints'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
model, = self.model.search([
@@ -87,9 +64,7 @@ class TriggerTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0020on_create(self):
- '''
- Test on_create
- '''
+ 'Test on_create'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
model, = self.model.search([
@@ -160,9 +135,7 @@ class TriggerTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0030on_write(self):
- '''
- Test on_write
- '''
+ 'Test on_write'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
model, = self.model.search([
@@ -273,9 +246,7 @@ class TriggerTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0040on_delete(self):
- '''
- Test on_delete
- '''
+ 'Test on_delete'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
model, = self.model.search([
@@ -364,9 +335,7 @@ class TriggerTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0050on_time(self):
- '''
- Test on_time
- '''
+ 'Test on_time'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
model, = self.model.search([
@@ -462,7 +431,3 @@ class TriggerTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(TriggerTestCase)
-
-if __name__ == '__main__':
- suite = suite()
- unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py
index fb35ab2..dd0cf7d 100644
--- a/trytond/tests/test_tryton.py
+++ b/trytond/tests/test_tryton.py
@@ -1,71 +1,35 @@
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
-import logging
-logging.basicConfig(level=logging.ERROR)
-import sys
import os
-DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
- '..', '..', '..', 'trytond')))
-if os.path.isdir(DIR):
- sys.path.insert(0, os.path.dirname(DIR))
-
+import sys
import unittest
import doctest
from lxml import etree
-import time
-import optparse
-
-_MODULES = False
-_CONFIGFILE = None
-if __name__ == '__main__':
- parser = optparse.OptionParser()
-
- parser.add_option("-c", "--config", dest="config",
- help="specify config file")
- parser.add_option("-m", "--modules", action="store_true", dest="modules",
- default=False, help="Run also modules tests")
- opt, args = parser.parse_args()
-
- if args:
- parser.error("Invalid argument")
- if opt.modules:
- _MODULES = True
- if opt.config:
- _CONFIGFILE = opt.config
from trytond.config import CONFIG
-CONFIG['db_type'] = 'sqlite'
-CONFIG.update_etc(_CONFIGFILE)
-CONFIG.set_timezone()
-if not CONFIG['admin_passwd']:
- CONFIG['admin_passwd'] = 'admin'
-
from trytond.pool import Pool
from trytond import backend
from trytond.protocols.dispatcher import create
from trytond.transaction import Transaction
from trytond.pyson import PYSONEncoder, Eval
-Pool.start()
+__all__ = ['POOL', 'DB_NAME', 'USER', 'USER_PASSWORD', 'CONTEXT',
+ 'install_module', 'test_view', 'test_depends', 'doctest_dropdb',
+ 'suite', 'all_suite', 'modules_suite']
-if CONFIG['db_type'] == 'sqlite':
- DB_NAME = ':memory:'
-else:
- DB_NAME = 'test_' + str(int(time.time()))
+Pool.start()
USER = 1
USER_PASSWORD = 'admin'
CONTEXT = {}
+DB_NAME = os.environ['DB_NAME']
DB = backend.get('Database')(DB_NAME)
Pool.test = True
POOL = Pool(DB_NAME)
class ModelViewTestCase(unittest.TestCase):
- '''
- Test ModelView
- '''
+ 'Test ModelView'
def setUp(self):
install_module('ir')
@@ -73,32 +37,25 @@ class ModelViewTestCase(unittest.TestCase):
install_module('webdav')
def test0000test(self):
+ 'Test test'
self.assertRaises(Exception, install_module, 'nosuchmodule')
self.assertRaises(Exception, test_view, 'nosuchmodule')
def test0010ir(self):
- '''
- Test ir.
- '''
+ 'Test ir'
test_view('ir')
def test0020res(self):
- '''
- Test res.
- '''
+ 'Test res'
test_view('res')
def test0040webdav(self):
- '''
- Test webdav.
- '''
+ 'Test webdav'
test_view('webdav')
class FieldDependsTestCase(unittest.TestCase):
- '''
- Test Field depends
- '''
+ 'Test Field depends'
def setUp(self):
install_module('ir')
@@ -106,6 +63,7 @@ class FieldDependsTestCase(unittest.TestCase):
install_module('webdav')
def test0010depends(self):
+ 'Test depends'
test_depends()
@@ -218,6 +176,18 @@ def test_depends():
list(depends - set(model._fields)), mname, fname))
+def doctest_dropdb(test):
+ '''Remove SQLite memory database'''
+ from trytond.backend.sqlite.database import Database as SQLiteDatabase
+ database = SQLiteDatabase().connect()
+ cursor = database.cursor(autocommit=True)
+ try:
+ SQLiteDatabase.drop(cursor, ':memory:')
+ cursor.commit()
+ finally:
+ cursor.close()
+
+
def suite():
'''
Return test suite for other modules
@@ -225,60 +195,37 @@ def suite():
return unittest.TestSuite()
-def all_suite():
+def all_suite(modules=None):
'''
Return all tests suite of current module
'''
suite_ = suite()
- import trytond.tests.test_tools as test_tools
- suite_.addTests(test_tools.suite())
- import trytond.tests.test_pyson as test_pyson
- suite_.addTests(test_pyson.suite())
- import trytond.tests.test_transaction as test_transaction
- suite_.addTests(test_transaction.suite())
- import trytond.tests.test_fields as test_fields
- suite_.addTests(test_fields.suite())
- import trytond.tests.test_modelsingleton as test_modelsingleton
- suite_.addTests(test_modelsingleton.suite())
- suite_.addTests(unittest.TestLoader(
- ).loadTestsFromTestCase(ModelViewTestCase))
- suite_.addTests(unittest.TestLoader(
- ).loadTestsFromTestCase(FieldDependsTestCase))
- import trytond.tests.test_mptt as test_mptt
- suite_.addTests(test_mptt.suite())
- import trytond.tests.test_importdata as test_importdata
- suite_.addTests(test_importdata.suite())
- import trytond.tests.test_exportdata as test_exportdata
- suite_.addTests(test_exportdata.suite())
- import trytond.tests.test_trigger as test_trigger
- suite_.addTests(test_trigger.suite())
- import trytond.tests.test_sequence as test_sequence
- suite_.addTests(test_sequence.suite())
- import trytond.tests.test_access as test_access
- suite_.addTests(test_access.suite())
- import trytond.tests.test_mixins as test_mixins
- suite_.addTests(test_mixins.suite())
- import trytond.tests.test_wizard as test_wizard
- suite_.addTests(test_wizard.suite())
- import trytond.tests.test_modelsql as test_modelsql
- suite_.addTests(test_modelsql.suite())
- import trytond.tests.test_cache as test_cache
- suite_.addTests(test_cache.suite())
- import trytond.tests.test_copy as test_copy
- suite_.addTests(test_copy.suite())
+ for fn in os.listdir(os.path.dirname(__file__)):
+ if fn.startswith('test_') and fn.endswith('.py'):
+ if modules and fn[:-3] not in modules:
+ continue
+ modname = 'trytond.tests.' + fn[:-3]
+ __import__(modname)
+ module = module = sys.modules[modname]
+ suite_.addTest(module.suite())
return suite_
-def modules_suite():
+def modules_suite(modules=None):
'''
Return all tests suite of all modules
'''
- suite_ = all_suite()
+ if modules:
+ suite_ = suite()
+ else:
+ suite_ = all_suite()
from trytond.modules import create_graph, get_module_list, \
MODULES_PATH, EGG_MODULES
graph = create_graph(get_module_list())[0]
for package in graph:
module = package.name
+ if modules and module not in modules:
+ continue
test_module = 'trytond.modules.%s.tests' % module
if os.path.isdir(os.path.join(MODULES_PATH, module)) or \
module in EGG_MODULES:
@@ -310,10 +257,3 @@ def modules_suite():
tests.append(test)
tests.extend(doc_tests)
return unittest.TestSuite(tests)
-
-if __name__ == '__main__':
- if not _MODULES:
- _SUITE = all_suite()
- else:
- _SUITE = modules_suite()
- unittest.TextTestRunner(verbosity=2).run(_SUITE)
diff --git a/trytond/tests/test_user.py b/trytond/tests/test_user.py
new file mode 100644
index 0000000..8014845
--- /dev/null
+++ b/trytond/tests/test_user.py
@@ -0,0 +1,62 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of this
+# repository contains the full copyright notices and license terms.
+
+import unittest
+from trytond.transaction import Transaction
+from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
+ install_module
+from trytond.res.user import bcrypt
+
+
+class UserTestCase(unittest.TestCase):
+ 'Test User'
+
+ def setUp(self):
+ install_module('res')
+ self.user = POOL.get('res.user')
+
+ def create_user(self, login, password, hash_method=None):
+ user, = self.user.create([{
+ 'name': login,
+ 'login': login,
+ }])
+ if hash_method:
+ hash = getattr(self.user, 'hash_' + hash_method)
+ self.user.write([user], {
+ 'password_hash': hash(password),
+ })
+ else:
+ self.user.write([user], {
+ 'password': password,
+ })
+
+ def check_user(self, login, password):
+ user, = self.user.search([('login', '=', login)])
+ user_id = self.user.get_login(login, password)
+ self.assertEqual(user_id, user.id)
+
+ bad_user_id = self.user.get_login(login, password + 'wrong')
+ self.assertEqual(bad_user_id, 0)
+
+ def test0010test_hash(self):
+ 'Test default hash password'
+ with Transaction().start(DB_NAME, USER, CONTEXT):
+ self.create_user('user', '12345')
+ self.check_user('user', '12345')
+
+ def test0011test_sha1(self):
+ 'Test sha1 password'
+ with Transaction().start(DB_NAME, USER, CONTEXT):
+ self.create_user('user', '12345', 'sha1')
+ self.check_user('user', '12345')
+
+ @unittest.skipIf(bcrypt is None, 'requires bcrypt')
+ def test0012test_bcrypt(self):
+ 'Test bcrypt password'
+ with Transaction().start(DB_NAME, USER, CONTEXT):
+ self.create_user('user', '12345', 'bcrypt')
+ self.check_user('user', '12345')
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(UserTestCase)
diff --git a/trytond/tests/test_wizard.py b/trytond/tests/test_wizard.py
index 43207c2..4e99bed 100644
--- a/trytond/tests/test_wizard.py
+++ b/trytond/tests/test_wizard.py
@@ -94,6 +94,3 @@ class WizardTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(WizardTestCase)
-
-if __name__ == '__main__':
- unittest.TextTestRunner(verbosity=2).run(suite())
diff --git a/trytond/tests/test_workflow.py b/trytond/tests/test_workflow.py
index cf564cb..74adbdb 100644
--- a/trytond/tests/test_workflow.py
+++ b/trytond/tests/test_workflow.py
@@ -31,7 +31,3 @@ class WorkflowTestCase(unittest.TestCase):
def suite():
return unittest.TestLoader().loadTestsFromTestCase(WorkflowTestCase)
-
-
-if __name__ == '__main__':
- unittest.TextTestRunner(verbosity=2).run(suite())
diff --git a/trytond/tools/__init__.py b/trytond/tools/__init__.py
index 90e391d..e209cff 100644
--- a/trytond/tools/__init__.py
+++ b/trytond/tools/__init__.py
@@ -2,10 +2,6 @@
#this repository contains the full copyright notices and license terms.
from .misc import *
from .datetime_strftime import *
-try:
- from collections import OrderedDict
-except ImportError:
- from .ordereddict import OrderedDict
class ClassProperty(property):
diff --git a/trytond/tools/ordereddict.py b/trytond/tools/ordereddict.py
deleted file mode 100644
index 856bd15..0000000
--- a/trytond/tools/ordereddict.py
+++ /dev/null
@@ -1,287 +0,0 @@
-# Copyright (C) 2009 by Raymond Hettinger
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and
-# pypy.
-# Passes Python2.7's test suite and incorporates all the latest updates.
-
-try:
- from thread import get_ident as _get_ident
-except ImportError:
- from dummy_thread import get_ident as _get_ident
-
-try:
- from _abcoll import KeysView, ValuesView, ItemsView
-except ImportError:
- pass
-
-
-class OrderedDict(dict):
- 'Dictionary that remembers insertion order'
- # An inherited dict maps keys to values.
- # The inherited dict provides __getitem__, __len__, __contains__, and get.
- # The remaining methods are order-aware.
- # Big-O running times for all methods are the same as for regular
- # dictionaries.
-
- # The internal self.__map dictionary maps keys to links in a doubly linked
- # list.
- # The circular doubly linked list starts and ends with a sentinel element.
- # The sentinel element never gets deleted (this simplifies the algorithm).
- # Each link is stored as a list of length three: [PREV, NEXT, KEY].
-
- def __init__(self, *args, **kwds):
- '''Initialize an ordered dictionary. Signature is the same as for
- regular dictionaries, but keyword arguments are not recommended
- because their insertion order is arbitrary.
-
- '''
- if len(args) > 1:
- raise TypeError('expected at most 1 arguments, got %d' % len(args))
- try:
- self.__root
- except AttributeError:
- self.__root = root = [] # sentinel node
- root[:] = [root, root, None]
- self.__map = {}
- self.__update(*args, **kwds)
-
- def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
- 'od.__setitem__(i, y) <==> od[i]=y'
- # Setting a new item creates a new link which goes at the end of the
- # linked list, and the inherited dictionary is updated with the new
- # key/value pair.
- if key not in self:
- root = self.__root
- last = root[0]
- last[1] = root[0] = self.__map[key] = [last, root, key]
- dict_setitem(self, key, value)
-
- def __delitem__(self, key, dict_delitem=dict.__delitem__):
- 'od.__delitem__(y) <==> del od[y]'
- # Deleting an existing item uses self.__map to find the link which is
- # then removed by updating the links in the predecessor and successor
- # nodes.
- dict_delitem(self, key)
- link_prev, link_next, key = self.__map.pop(key)
- link_prev[1] = link_next
- link_next[0] = link_prev
-
- def __iter__(self):
- 'od.__iter__() <==> iter(od)'
- root = self.__root
- curr = root[1]
- while curr is not root:
- yield curr[2]
- curr = curr[1]
-
- def __reversed__(self):
- 'od.__reversed__() <==> reversed(od)'
- root = self.__root
- curr = root[0]
- while curr is not root:
- yield curr[2]
- curr = curr[0]
-
- def clear(self):
- 'od.clear() -> None. Remove all items from od.'
- try:
- for node in self.__map.itervalues():
- del node[:]
- root = self.__root
- root[:] = [root, root, None]
- self.__map.clear()
- except AttributeError:
- pass
- dict.clear(self)
-
- def popitem(self, last=True):
- '''od.popitem() -> (k, v), return and remove a (key, value) pair.
- Pairs are returned in LIFO order if last is true or FIFO order if
- false.
-
- '''
- if not self:
- raise KeyError('dictionary is empty')
- root = self.__root
- if last:
- link = root[0]
- link_prev = link[0]
- link_prev[1] = root
- root[0] = link_prev
- else:
- link = root[1]
- link_next = link[1]
- root[1] = link_next
- link_next[0] = root
- key = link[2]
- del self.__map[key]
- value = dict.pop(self, key)
- return key, value
-
- # -- the following methods do not depend on the internal structure --
-
- def keys(self):
- 'od.keys() -> list of keys in od'
- return list(self)
-
- def values(self):
- 'od.values() -> list of values in od'
- return [self[key] for key in self]
-
- def items(self):
- 'od.items() -> list of (key, value) pairs in od'
- return [(key, self[key]) for key in self]
-
- def iterkeys(self):
- 'od.iterkeys() -> an iterator over the keys in od'
- return iter(self)
-
- def itervalues(self):
- 'od.itervalues -> an iterator over the values in od'
- for k in self:
- yield self[k]
-
- def iteritems(self):
- 'od.iteritems -> an iterator over the (key, value) items in od'
- for k in self:
- yield (k, self[k])
-
- def update(*args, **kwds):
- '''od.update(E, **F) -> None. Update od from dict/iterable E and F.
-
- If E is a dict instance, does: for k in E: od[k] = E[k]
- If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
- Or if E is an iterable of items, does: for k, v in E: od[k] = v
- In either case, this is followed by: for k, v in F.items(): od[k] = v
-
- '''
- if len(args) > 2:
- raise TypeError('update() takes at most 2 positional '
- 'arguments (%d given)' % (len(args),))
- elif not args:
- raise TypeError('update() takes at least 1 argument (0 given)')
- self = args[0]
- # Make progressively weaker assumptions about "other"
- other = ()
- if len(args) == 2:
- other = args[1]
- if isinstance(other, dict):
- for key in other:
- self[key] = other[key]
- elif hasattr(other, 'keys'):
- for key in other.keys():
- self[key] = other[key]
- else:
- for key, value in other:
- self[key] = value
- for key, value in kwds.items():
- self[key] = value
-
- # let subclasses override update without breaking __init__
- __update = update
-
- __marker = object()
-
- def pop(self, key, default=__marker):
- '''od.pop(k[,d]) -> v, remove specified key and return the
- corresponding value. If key is not found, d is returned if given,
- otherwise KeyError is raised.
-
- '''
- if key in self:
- result = self[key]
- del self[key]
- return result
- if default is self.__marker:
- raise KeyError(key)
- return default
-
- def setdefault(self, key, default=None):
- 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
- if key in self:
- return self[key]
- self[key] = default
- return default
-
- def __repr__(self, _repr_running={}):
- 'od.__repr__() <==> repr(od)'
- call_key = id(self), _get_ident()
- if call_key in _repr_running:
- return '...'
- _repr_running[call_key] = 1
- try:
- if not self:
- return '%s()' % (self.__class__.__name__,)
- return '%s(%r)' % (self.__class__.__name__, self.items())
- finally:
- del _repr_running[call_key]
-
- def __reduce__(self):
- 'Return state information for pickling'
- items = [[k, self[k]] for k in self]
- inst_dict = vars(self).copy()
- for k in vars(OrderedDict()):
- inst_dict.pop(k, None)
- if inst_dict:
- return (self.__class__, (items,), inst_dict)
- return self.__class__, (items,)
-
- def copy(self):
- 'od.copy() -> a shallow copy of od'
- return self.__class__(self)
-
- @classmethod
- def fromkeys(cls, iterable, value=None):
- '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
- and values equal to v (which defaults to None).
-
- '''
- d = cls()
- for key in iterable:
- d[key] = value
- return d
-
- def __eq__(self, other):
- '''od.__eq__(y) <==> od==y. Comparison to another OD is
- order-sensitive while comparison to a regular mapping is
- order-insensitive.
-
- '''
- if isinstance(other, OrderedDict):
- return len(self) == len(other) and self.items() == other.items()
- return dict.__eq__(self, other)
-
- def __ne__(self, other):
- return not self == other
-
- # -- the following methods are only used in Python 2.7 --
-
- def viewkeys(self):
- "od.viewkeys() -> a set-like object providing a view on od's keys"
- return KeysView(self)
-
- def viewvalues(self):
- "od.viewvalues() -> an object providing a view on od's values"
- return ValuesView(self)
-
- def viewitems(self):
- "od.viewitems() -> a set-like object providing a view on od's items"
- return ItemsView(self)
diff --git a/trytond/transaction.py b/trytond/transaction.py
index be6ebea..39b5aab 100644
--- a/trytond/transaction.py
+++ b/trytond/transaction.py
@@ -58,6 +58,8 @@ class Transaction(local):
__metaclass__ = Singleton
cursor = None
+ database = None
+ close = None
user = None
context = None
create_records = None
@@ -65,21 +67,28 @@ class Transaction(local):
delete = None # TODO check to merge with delete_records
timestamp = None
- def start(self, database_name, user, readonly=False, context=None):
+ def start(self, database_name, user, readonly=False, context=None,
+ close=False, autocommit=False):
'''
Start transaction
'''
Database = backend.get('Database')
assert self.user is None
+ assert self.database is None
assert self.cursor is None
+ assert self.close is None
assert self.context is None
if not database_name:
database = Database().connect()
else:
database = Database(database_name).connect()
Flavor.set(Database.flavor)
- self.cursor = database.cursor(readonly=readonly)
+ cursor = database.cursor(readonly=readonly,
+ autocommit=autocommit)
self.user = user
+ self.database = database
+ self.cursor = cursor
+ self.close = close
self.context = context or {}
self.create_records = {}
self.delete_records = {}
@@ -93,9 +102,11 @@ class Transaction(local):
Stop transaction
'''
try:
- self.cursor.close()
+ self.cursor.close(close=self.close)
finally:
self.cursor = None
+ self.database = None
+ self.close = None
self.user = None
self.context = None
self.create_records = None
@@ -119,10 +130,16 @@ class Transaction(local):
return manager
def set_user(self, user, set_context=False):
+ if user != 0 and set_context:
+ raise ValueError('set_context only allowed for root')
manager = _AttributeManager(user=self.user,
- context=self.context.copy())
+ context=self.context)
+ self.context = self.context.copy()
if set_context:
- self.context.update({'user': self.user})
+ if user != self.user:
+ self.context['user'] = self.user
+ else:
+ self.context.pop('user', None)
self.user = user
return manager
@@ -131,11 +148,11 @@ class Transaction(local):
self.cursor = cursor
return manager
- def new_cursor(self):
+ def new_cursor(self, autocommit=False, readonly=False):
Database = backend.get('Database')
manager = _CursorManager(self.cursor)
database = Database(self.cursor.database_name).connect()
- self.cursor = database.cursor()
+ self.cursor = database.cursor(autocommit=autocommit, readonly=readonly)
return manager
@property
diff --git a/trytond/version.py b/trytond/version.py
index d6a2ba0..788322a 100644
--- a/trytond/version.py
+++ b/trytond/version.py
@@ -1,6 +1,6 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
PACKAGE = "trytond"
-VERSION = "3.0.4"
+VERSION = "3.2.0"
LICENSE = "GPL-3"
WEBSITE = "http://www.tryton.org/"
diff --git a/trytond/webdav/locale/ca_ES.po b/trytond/webdav/locale/ca_ES.po
index a44eb9a..cd15ec2 100644
--- a/trytond/webdav/locale/ca_ES.po
+++ b/trytond/webdav/locale/ca_ES.po
@@ -28,7 +28,7 @@ msgstr "Ruta"
msgctxt "field:ir.attachment,shares:"
msgid "Shares"
-msgstr "Directori compartit"
+msgstr "Directoris compartits"
msgctxt "field:ir.attachment,url:"
msgid "URL"
@@ -124,7 +124,7 @@ msgstr "Usuari"
msgctxt "field:webdav.share,write_date:"
msgid "Write Date"
-msgstr "Data modicación"
+msgstr "Data modificació"
msgctxt "field:webdav.share,write_uid:"
msgid "Write User"
diff --git a/trytond/webdav/locale/es_AR.po b/trytond/webdav/locale/es_AR.po
index 341d49e..593b79d 100644
--- a/trytond/webdav/locale/es_AR.po
+++ b/trytond/webdav/locale/es_AR.po
@@ -112,7 +112,7 @@ msgstr "Ruta"
msgctxt "field:webdav.share,rec_name:"
msgid "Name"
-msgstr "Nombre campo"
+msgstr "Nombre"
msgctxt "field:webdav.share,url:"
msgid "URL"
diff --git a/trytond/webdav/locale/es_CO.po b/trytond/webdav/locale/es_CO.po
index 37ad2d4..c2d65ff 100644
--- a/trytond/webdav/locale/es_CO.po
+++ b/trytond/webdav/locale/es_CO.po
@@ -7,12 +7,12 @@ msgid ""
"You can not create an attachment named \"%(attachment)s in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
-"No puede crear un adjunto llamado \"%(attachment)s\" porque existe una "
-"colección con el mismo nombre."
+"No puede crear un adjunto llamado \"%(attachment)s\" en la colección "
+"\"%(collections)s\" con el porque hay una colección con el ese nombre."
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
-msgstr "¡El nombre de la colección debe ser único dentro de dicha colección!"
+msgstr "El nombre de la colección debe ser único dentro de dicha colección!"
msgctxt "error:webdav.collection:"
msgid ""
@@ -40,7 +40,7 @@ msgstr "Hijos"
msgctxt "field:webdav.collection,complete_name:"
msgid "Complete Name"
-msgstr "Nombre completo"
+msgstr "Nombre Completo"
msgctxt "field:webdav.collection,create_date:"
msgid "Create Date"
@@ -112,7 +112,7 @@ msgstr "Ruta"
msgctxt "field:webdav.share,rec_name:"
msgid "Name"
-msgstr "Nombre del campo"
+msgstr "Nombre"
msgctxt "field:webdav.share,url:"
msgid "URL"
@@ -164,7 +164,7 @@ msgstr "Colección"
msgctxt "model:webdav.share,name:"
msgid "Share"
-msgstr "Recurso compartido"
+msgstr "Recurso Compartido"
msgctxt "view:ir.attachment:"
msgid "WebDAV"
@@ -180,7 +180,7 @@ msgstr "Colecciones"
msgctxt "view:webdav.share:"
msgid "Share"
-msgstr "Recurso compartido"
+msgstr "Recurso Compartido"
msgctxt "view:webdav.share:"
msgid "Shares"
diff --git a/trytond/webdav/locale/es_ES.po b/trytond/webdav/locale/es_ES.po
index 70f0f69..3b7312a 100644
--- a/trytond/webdav/locale/es_ES.po
+++ b/trytond/webdav/locale/es_ES.po
@@ -7,7 +7,7 @@ msgid ""
"You can not create an attachment named \"%(attachment)s in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
-"No puede crear el adjunto con el nombre \"%(attachment)s al directorio "
+"No puede crear el adjunto con el nombre \"%(attachment)s en el directorio "
"\"%(collection)s\" porque ya existe otro con este nombre."
msgctxt "error:webdav.collection:"
@@ -124,7 +124,7 @@ msgstr "Usuario"
msgctxt "field:webdav.share,write_date:"
msgid "Write Date"
-msgstr "Fecha modicación"
+msgstr "Fecha modificación"
msgctxt "field:webdav.share,write_uid:"
msgid "Write User"
diff --git a/trytond/webdav/webdav.py b/trytond/webdav/webdav.py
index d62179d..d35b1dc 100644
--- a/trytond/webdav/webdav.py
+++ b/trytond/webdav/webdav.py
@@ -797,39 +797,34 @@ class Attachment(ModelSQL, ModelView):
return result
@classmethod
- def set_shares(cls, attachments, name, value):
+ def set_shares(cls, attachments, name, values):
Share = Pool().get('webdav.share')
- if not value:
+ if not values:
return
- for action in value:
- if action[0] == 'create':
- to_create = []
- for attachment in attachments:
- for values in action[1]:
- values = values.copy()
- values['path'] = attachment.path
- to_create.append(values)
- if to_create:
- Share.create(to_create)
- elif action[0] == 'write':
- Share.write(action[1], action[2])
- elif action[0] == 'delete':
- Share.delete(Share.browse(action[1]))
- elif action[0] == 'delete_all':
- paths = [a.path for a in attachments]
- shares = Share.search([
- ('path', 'in', paths),
- ])
- Share.delete(shares)
- elif action[0] == 'unlink':
- pass
- elif action[0] == 'add':
- pass
- elif action[0] == 'unlink_all':
- pass
- elif action[0] == 'set':
- pass
- else:
- raise Exception('Bad arguments')
+ def create(vlist):
+ to_create = []
+ for attachment in attachments:
+ for values in vlist:
+ values = values.copy()
+ values['path'] = attachment.path
+ to_create.append(values)
+ if to_create:
+ Share.create(to_create)
+
+ def write(ids, values):
+ Share.write(Share.browse(ids), values)
+
+ def delete(share_ids):
+ Share.delete(Share.browse(share_ids))
+
+ actions = {
+ 'create': create,
+ 'write': write,
+ 'delete': delete,
+ }
+ for value in values:
+ action = value[0]
+ args = value[1:]
+ actions[action](*args)
diff --git a/trytond/wizard/wizard.py b/trytond/wizard/wizard.py
index a4b2cc7..c3c725e 100644
--- a/trytond/wizard/wizard.py
+++ b/trytond/wizard/wizard.py
@@ -190,7 +190,14 @@ class Wizard(WarningErrorMixin, URLMixin, PoolBase):
def delete(cls, session_id):
"Delete the session"
Session = Pool().get('ir.session.wizard')
+ end = getattr(cls, cls.end_state, None)
+ if end:
+ wizard = cls(session_id)
+ action = end(wizard)
+ else:
+ action = None
Session.delete([Session(session_id)])
+ return action
@classmethod
def execute(cls, session_id, data, state_name):
--
tryton-server
More information about the tryton-debian-vcs
mailing list