[tryton-debian-vcs] tryton-server branch upstream updated. upstream/3.4.3-1-g4aa257d
Mathias Behrle
tryton-debian-vcs at alioth.debian.org
Thu Apr 23 16:08:14 UTC 2015
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.4.3-1-g4aa257d
commit 4aa257def318feb3e7cd2f4caa03dca2497c9193
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Thu Apr 23 17:00:11 2015 +0200
Adding upstream version 3.6.0.
Signed-off-by: Mathias Behrle <mathiasb at m9s.biz>
diff --git a/CHANGELOG b/CHANGELOG
index 8aa419c..75a39d1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,11 +1,39 @@
-Version 3.4.3 - 2015-03-30
-* Bug fixes (see mercurial logs for details)
-
-Version 3.4.2 - 2015-02-16
-* Bug fixes (see mercurial logs for details)
-
-Version 3.4.1 - 2014-12-03
+Version 3.6.0 - 2015-04-20
* Bug fixes (see mercurial logs for details)
+* Use bytes and bytearray for Binary
+* Add button_change
+* Add support for PyPy
+* Add support for psycopg2cffi
+* Add noeval on PYSONDecoder
+* Add __repr__ to PYSON
+* Remove safe_eval
+* Add ModelView.view_attributes
+* Add pyson attribute on data field tag
+* Changed into JSON:
+ - record rule domain
+ - trigger condition
+ - 'states', 'domain', 'spell' and 'colors' view attributes
+ - view domain
+ - 'email', 'domain', 'context', 'order' and 'search_value' action fields
+* Add product attribute on form view for One2Many
+* Remove float_time widget
+* Add TimeDelta field
+* search_global yields record instead of id
+* Add ModelTestCase
+* Add test for missing default model access
+* Report API refactorization
+* Add test for access rights of menu and actions
+* Allow to use the dotted notation for order parameters
+* Use action_id to find report to use
+* Allow custom StateView without Model
+* Remove Pool.object_name_list
+* Add translated descriptor for Dict field
+* Clean private context keyword in RPC
+* Add cache section in configuration
+* Use dualmethod on ModelStorage.save
+* New API for on_change: instance changes
+* Add restore_history_before on ModelSQL
+* Remove img_{width,height} form attributes
Version 3.4.0 - 2014-10-20
* Bug fixes (see mercurial logs for details)
diff --git a/MANIFEST.in b/MANIFEST.in
index b5f249f..30f129b 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,7 +2,6 @@ include LICENSE
include COPYRIGHT
include README
include INSTALL
-include TODO
include CHANGELOG
include doc/*
recursive-include doc *.rst
diff --git a/PKG-INFO b/PKG-INFO
index b0a810f..a676571 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,12 +1,12 @@
Metadata-Version: 1.1
Name: trytond
-Version: 3.4.3
+Version: 3.6.0
Summary: Tryton server
Home-page: http://www.tryton.org/
Author: Tryton
Author-email: issue_tracker at tryton.org
License: GPL-3
-Download-URL: http://downloads.tryton.org/3.4/
+Download-URL: http://downloads.tryton.org/3.6/
Description: trytond
=======
@@ -116,4 +116,6 @@ Classifier: Natural Language :: Slovenian
Classifier: Natural Language :: Spanish
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
diff --git a/TODO b/TODO
deleted file mode 100644
index a6c6fb2..0000000
--- a/TODO
+++ /dev/null
@@ -1,7 +0,0 @@
-- Allow to pickle Model instance
-- Add check on action groups when executing wizard
-- Allow to create fields from the client interface
-- Improve encoding of default properties
-- Add check for index and constraint name lenght (limit 31)
-- Add order attribute on page tag
-- Find the on_change method for inherited fields
diff --git a/bin/trytond b/bin/trytond
index 6f03fe7..89d7b31 100755
--- a/bin/trytond
+++ b/bin/trytond
@@ -10,8 +10,8 @@ DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
if os.path.isdir(DIR):
sys.path.insert(0, os.path.dirname(DIR))
-import trytond
-from trytond.version import VERSION
+from trytond import __version__
+from trytond import server
def parse_commandline():
@@ -20,7 +20,7 @@ def parse_commandline():
parser = argparse.ArgumentParser(prog='trytond')
parser.add_argument('--version', action='version',
- version='%(prog)s ' + VERSION)
+ version='%(prog)s ' + __version__)
parser.add_argument("-c", "--config", dest="configfile", metavar='FILE',
default=os.environ.get('TRYTOND_CONFIG'), help="specify config file")
parser.add_argument('--dev', dest='dev', action='store_true',
@@ -65,7 +65,7 @@ if '--profile' in sys.argv:
options = parse_commandline()
statfile = tempfile.mkstemp(".stat", "trytond-")[1]
- profile.run('trytond.server.TrytonServer(options).run()', statfile)
+ profile.run('server.TrytonServer(options).run()', statfile)
s = pstats.Stats(statfile)
s.sort_stats('cumulative').print_stats()
s.sort_stats('call').print_stats()
@@ -77,4 +77,4 @@ if '--profile' in sys.argv:
os.remove(statfile)
else:
options = parse_commandline()
- trytond.server.TrytonServer(options).run()
+ server.TrytonServer(options).run()
diff --git a/doc/conf.py b/doc/conf.py
index 18e62c7..14a3199 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -1,6 +1,6 @@
# -*- 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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
#
# Tryton documentation build configuration file, created by
# sphinx-quickstart on Tue Mar 23 13:25:32 2010.
@@ -18,7 +18,7 @@
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.append(os.path.abspath('.'))
+# sys.path.append(os.path.abspath('.'))
# -- General configuration ----------------------------------------------------
@@ -33,7 +33,7 @@ templates_path = ['_templates']
source_suffix = '.rst'
# The encoding of source files.
-#source_encoding = 'utf-8'
+# source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
@@ -48,46 +48,46 @@ copyright = (u'2008-2011, Bertrand Chenal, Cédric Krier, Ian Wilson, '
# built documents.
#
# The short X.Y version.
-version = '3.4'
+version = '3.6'
# The full version, including alpha/beta/rc tags.
-release = '3.4'
+release = '3.6'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
-#language = None
+# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
-#today = ''
+# today = ''
# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
-#unused_docs = []
+# unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents
-#default_role = None
+# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
-#show_authors = False
+# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------
@@ -99,26 +99,26 @@ html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
-#html_theme_options = {}
+# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
-#html_title = None
+# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
-#html_logo = None
+# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
-#html_favicon = None
+# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
@@ -127,38 +127,38 @@ html_static_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
# If false, no module index is generated.
-#html_use_modindex = True
+# html_use_modindex = True
# If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
# If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = ''
+# html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'trytonddoc'
@@ -167,10 +167,10 @@ htmlhelp_basename = 'trytonddoc'
# -- Options for LaTeX output -------------------------------------------------
# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+# latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+# latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual])
@@ -181,17 +181,17 @@ latex_documents = [
# The name of an image file (relative to this directory) to place at the top of
# the title page.
-#latex_logo = None
+# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
+# latex_preamble = ''
# Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
# If false, no module index is generated.
-#latex_use_modindex = True
+# latex_use_modindex = True
diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst
index 82b4b1e..18aa231 100644
--- a/doc/ref/models/fields.rst
+++ b/doc/ref/models/fields.rst
@@ -80,7 +80,7 @@ method signature is::
on_change_<field name>()
-This method must return a dictionary with the values of fields to be updated.
+This method must change the value of the fields to be updated.
.. note::
@@ -269,7 +269,7 @@ A single line string field.
method ``autocomplete_<field name>`` of the model when the user changes one
of those field value. The method signature is::
- autocomplete_<field name>(values)
+ autocomplete_<field name>()
This method must return a list of string that will populate the
ComboboxEntry in the client.
@@ -336,6 +336,9 @@ DateTime
.. class:: DateTime(string[, format, \**options])
A date and time, represented in Python by a ``datetime.datetime`` instance.
+It is stored in `UTC`_ while displayed in the user timezone.
+
+.. _`UTC`: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
.. attribute:: DateTime.format
@@ -361,12 +364,26 @@ A time, represented in Python by a ``datetime.time`` instance.
Same as :attr:`DateTime.format`
+TimeDelta
+---------
+
+.. class:: TimeDelta(string[, converter[, \**options]])
+
+An interval, represented in Python by a ``datetime.timedelta`` instance.
+
+.. attribute:: TimeDelta.converter
+
+ The name of the context key containing the time converter.
+ A time converter is a dictionary with the keys: ``s`` (second), ``m``
+ (minute), ``h`` (hour), ``d`` (day), ``w`` (week), ``M`` (month), ``Y``
+ (year) and the value in second.
+
Binary
------
.. class:: Binary(string[, \**options])
-A binary field. It will be represented in Python by a ``str`` instance.
+A binary field. It will be represented in Python by a ``bytes`` instance.
:class:`Binary` has one extra optional argument:
@@ -770,3 +787,11 @@ A dictionary field with predefined keys.
The name of the :class:`DictSchemaMixin` model that stores the definition
of keys.
+
+Instance methods:
+
+.. method:: Dict.translated([name[, type_]])
+
+ Returns a descriptor for the translated `values` or `keys` of the field
+ following `type_`. The descriptor must be used on the same class as the
+ field. Default `type_` is `values`.
diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst
index 110636e..26722d4 100644
--- a/doc/ref/models/models.rst
+++ b/doc/ref/models/models.rst
@@ -129,6 +129,15 @@ Static methods:
Same as :meth:`ModelView.button` but return the action id of the XML `id`
action.
+.. staticmethod:: ModelView.button_change([\*fields])
+
+ Same as :meth:`ModelView.button` but for button that change values of the
+ fields on client side (similar to :ref:`on_change
+ <ref-models-fields-on_change>`).
+
+ .. warning::
+ Only on instance methods.
+
Class methods:
.. classmethod:: ModelView.fields_view_get([view_id[, view_type[, toolbar]]])
@@ -137,23 +146,15 @@ Class methods:
{
'model': model name,
+ 'type': view type,
+ 'view_id': view id,
'arch': XML description,
'fields': {
field name: {
...
},
},
- 'toolbar': {
- 'print': [
- ...
- ],
- 'action': [
- ...
- ],
- 'relate': [
- ...
- ],
- },
+ 'field_childs': field for tree,
}
.. classmethod:: ModelView.view_toolbar_get()
@@ -167,6 +168,12 @@ Class methods:
Returns the window title used by the client for the specific view type.
+.. classmethod:: ModelView.view_attributes()
+
+ Returns a list of XPath, attribute and value.
+ Each element from the XPath will get the attribute set with the JSON
+ encoded value.
+
============
ModelStorage
============
@@ -230,7 +237,7 @@ Static methods:
Return the default value for :attr:`create_date`.
-CLass methods:
+Class methods:
.. classmethod:: ModelStorage.create(vlist)
@@ -297,7 +304,7 @@ CLass methods:
.. classmethod:: ModelStorage.search_global(cls, text)
- Yield tuples (id, rec_name, icon) for records matching text.
+ Yield tuples (record, name, icon) for records matching text.
It is used for the global search.
.. classmethod:: ModelStorage.browse(ids)
@@ -336,6 +343,12 @@ CLass methods:
method must be overridden to add validation and must raise an exception if
validation fails.
+Dual methods:
+
+.. classmethod:: ModelStorage.save(records)
+
+ Save the modification made on the records.
+
Instance methods:
.. method:: ModelStorage.get_rec_name(name)
@@ -343,10 +356,6 @@ Instance methods:
Getter for the :class:`trytond.model.fields.Function` field
:attr:`rec_name`.
-.. method:: ModelStorage.save()
-
- Save the modification made on the record instance.
-
========
ModelSQL
========
@@ -373,6 +382,10 @@ Class attributes are:
second is the sort ordering as `ASC` for ascending or `DESC` for
descending.
+ In case the field used for the first element is a :class:`fields.Many2One`,
+ it is also possible to use the dotted notation to sort on a specific field
+ from the target record.
+
.. attribute:: ModelSQL._order_name
The name of the field (or an SQL statement) on which the records must be
@@ -425,6 +438,15 @@ Class methods:
No access rights are verified and the records are not validated.
..
+.. classmethod:: ModelSQL.restore_history_before(ids, datetime)
+
+ Restore the record ids from history before 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/pool.rst b/doc/ref/pool.rst
index 911a6fe..0ca5638 100644
--- a/doc/ref/pool.rst
+++ b/doc/ref/pool.rst
@@ -35,10 +35,6 @@ Instance methods:
Return the named instance of type from the pool.
-.. method:: Pool.object_name_list([type])
-
- Return the list of instances names.
-
.. method:: Pool.iterobject([type])
Return an interator over instances names.
diff --git a/doc/ref/pyson.rst b/doc/ref/pyson.rst
index fd1640b..0f3b3a2 100644
--- a/doc/ref/pyson.rst
+++ b/doc/ref/pyson.rst
@@ -50,15 +50,15 @@ Instance method:
Returns a string representation of a given PYSON statement.
``object`` contains a PYSON statement.
-.. class:: PYSONDecoder()
+.. class:: PYSONDecoder([context[, noeval]])
-Decoder for string into PYSON statement representation.
+Decoder for string into the evaluated or not PYSON statement.
Instance method:
.. method:: PYSONDecoder.decode(object)
- Returns a PYSON statement representation of a given string.
+ Returns a PYSON statement evaluated or not of a given string.
``object`` contains a string.
Statements
diff --git a/doc/ref/wizard.rst b/doc/ref/wizard.rst
index 62267e4..a8944ee 100644
--- a/doc/ref/wizard.rst
+++ b/doc/ref/wizard.rst
@@ -99,6 +99,8 @@ StateView
A :class:`StateView` is a state that will display a form in the client.
The form is defined by the :class:`~trytond.model.ModelView` with the name
`model_name`, the `XML` id in `view` and the `buttons`.
+ The default value of the view can be set with a method on wizard having the
+ same name as the state but starting with `default_`.
Instance attributes are:
@@ -116,11 +118,14 @@ Instance attributes are:
Instance methods are:
-.. method:: StateView.get_view()
+.. method:: StateView.get_view(wizard, state_name)
Returns the view definition like
:meth:`~trytond.model.ModelView.fields_view_get`.
+ * wizard is a :class:`Wizard` instance
+ * state_name is the name of the :class:`StateView` instance
+
.. method:: StateView.get_defaults(wizard, state_name, fields)
Return default values for the fields.
diff --git a/doc/topics/access_rights.rst b/doc/topics/access_rights.rst
index 96c0d22..41ce7c1 100644
--- a/doc/topics/access_rights.rst
+++ b/doc/topics/access_rights.rst
@@ -7,7 +7,7 @@ Access Rights
There are 3 levels of access rights: model, field, button and record.
Every access right is based on the groups of the user.
The model and field access rights are checked for every RPC call for which
-:attrs:`RPC.check_access` is set. The others are always enforced.
+:attr:`trytond.rpc.RPC.check_access` is set. The others are always enforced.
Model Access
============
diff --git a/doc/topics/configuration.rst b/doc/topics/configuration.rst
index 3ca33b1..662450c 100644
--- a/doc/topics/configuration.rst
+++ b/doc/topics/configuration.rst
@@ -4,7 +4,7 @@
Configuration file for Tryton
=============================
-The configuration file control some aspects of the behavior of Tryton.
+The configuration file controls some aspects of the behavior of Tryton.
The file uses a simple ini-file format. It consists of sections, led by a
`[section]` header and followed by `name = value` entries:
@@ -37,9 +37,10 @@ Defines the behavior of the JSON-RPC_ network interface.
listen
~~~~~~
-Defines a comma separated list of couple of host (or IP address) and port numer
-separeted by a colon to listen on.
-The default value is `localhost:8000`.
+Defines a comma separated list of couples of host (or IP address) and port
+number separated by a colon to listen on.
+
+Default `localhost:8000`
hostname
~~~~~~~~
@@ -49,7 +50,9 @@ Defines the hostname for this network interface.
data
~~~~
-Defines the root path to retrieve data for `GET` request.
+Defines the root path to retrieve data for `GET` requests.
+
+Default: `/var/www/localhost/tryton`
xmlrpc
------
@@ -59,7 +62,7 @@ Defines the behavior of the XML-RPC_ network interface.
listen
~~~~~~
-Same as for `jsonrpc` except it have no default value.
+Same as for `jsonrpc` except it has no default value.
webdav
------
@@ -69,12 +72,12 @@ Define the behavior of the WebDAV_ network interface.
listen
~~~~~~
-Same as for `jsonrpc` except it have no default value.
+Same as for `jsonrpc` except it has no default value.
database
--------
-Defines how database is managed.
+Defines how the database is managed.
uri
~~~
@@ -84,7 +87,9 @@ The typical form is:
database://username:password@host:port/
-The default available databases are:
+Default: `sqlite://`
+
+The available databases are:
PostgreSQL
**********
@@ -107,30 +112,68 @@ Same as for PostgreSQL.
path
~~~~
-The directory where Tryton should store files and so the user running `trytond`
+The directory where Tryton stores files and so the user running `trytond`
must have write access on this directory.
-The default value is `/var/lib/trytond/`.
+
+Default: `/var/lib/trytond/`
list
~~~~
-A boolean value (default: `True`) to list available databases.
+A boolean value to list available databases.
+
+Default: `True`
retry
~~~~~
-The number of retries when a database operation error occurs during a request.
+The number of retries when a database operational error occurs during a request.
+
+Default: `5`
language
~~~~~~~~
-The main language (default: `en_US`) of the database that will be stored in the
-main table for translatable fields.
+The main language of the database that will be used for storage in the main
+table for translations.
+
+Default: `en_US`
+
+cache
+-----
+
+Defines size of various cache.
+
+model
+~~~~~
+
+The number of different model kept in the cache per transaction.
+
+Default: `200`
+
+record
+~~~~~~
+
+The number of record loaded kept in the cache of the list.
+It can be changed locally using the `_record_cache_size` key in
+:attr:`Transaction.context`.
+
+Default: `2000`
+
+field
+~~~~~
+
+The number of field to load with an `eager` :attr:`Field.loading`.
+
+Default: `100`
ssl
---
-Activates the SSL_ on all network protocol.
+Activates SSL_ on all network protocols.
+
+.. note:: SSL_ is activated by defining privatekey.
+ Please refer to SSL-CERT_ on how to use private keys and certficates.
privatekey
~~~~~~~~~~
@@ -156,12 +199,12 @@ The available protocols are:
- `smtp+tls`: SMTP with STARTTLS
- `smtps`: SMTP with SSL
-The default value is: `smtp://localhost:25`
+Default: `smtp://localhost:25`
from
~~~~
-Defines the default `From` address when Tryton send emails.
+Defines the default `From` address for emails sent by Tryton.
session
-------
@@ -169,14 +212,16 @@ session
timeout
~~~~~~~
-The time in second before a session expires.
+The time in seconds until a session expires.
+
+Default: `600`
super_pwd
~~~~~~~~~
-Theserver password uses to authenticate database management from the client.
-It is encrypted using using the Unix `crypt(3)` routine.
-Such password can be generated using this command line::
+The server password used to authenticate from the client for database
+management tasks. It is encrypted using using the Unix `crypt(3)` routine.
+A password can be generated using this command line::
python -c 'import getpass,crypt,random,string; print crypt.crypt(getpass.getpass(), "".join(random.sample(string.ascii_letters + string.digits, 8)))'
@@ -188,10 +233,14 @@ unoconv
The parameters for `unoconv`.
+Default: `pipe,name=trytond;urp;StarOffice.ComponentContext`
+
+
.. _JSON-RPC: http://en.wikipedia.org/wiki/JSON-RPC
.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC
.. _WebDAV: http://en.wikipedia.org/wiki/WebDAV
.. _RFC-3986: http://tools.ietf.org/html/rfc3986
.. _SMTP-URL: http://tools.ietf.org/html/draft-earhart-url-smtp-00
.. _SSL: http://en.wikipedia.org/wiki/Secure_Sockets_Layer
+.. _SSL-CERT: https://docs.python.org/library/ssl.html#ssl.wrap_socket
.. _STARTTLS: http://en.wikipedia.org/wiki/STARTTLS
diff --git a/doc/topics/install.rst b/doc/topics/install.rst
index c9f4f1e..889e360 100644
--- a/doc/topics/install.rst
+++ b/doc/topics/install.rst
@@ -15,6 +15,8 @@ Prerequisites
* polib (https://bitbucket.org/izi/polib/wiki/Home)
* python-sql 0.2 or later (http://code.google.com/p/python-sql/)
* Optional: psycopg 2 or later (http://www.initd.org/)
+ * Optional: psycopg2cffi 2.5.0 or later
+ (http://github.com/chtd/psycopg2cffi)
* 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/)
diff --git a/doc/topics/logs.rst b/doc/topics/logs.rst
index ccd4e6d..ea90b17 100644
--- a/doc/topics/logs.rst
+++ b/doc/topics/logs.rst
@@ -4,7 +4,9 @@
Logging configuration
=====================
-Without any configuration, trytond write INFO messages to standard output.
+Without any configuration, trytond writes ERROR messages to standard output.
+With the verbose flag set, it writes INFO message.
+And with the verbose and development flags set, it write DEBUG message.
Logs can be configured using a `configparser-format`_ file. The filename can
be specified using trytond ``logconf`` parameter.
diff --git a/doc/topics/models/fields_on_change.rst b/doc/topics/models/fields_on_change.rst
index e47c787..6a77f7c 100644
--- a/doc/topics/models/fields_on_change.rst
+++ b/doc/topics/models/fields_on_change.rst
@@ -10,9 +10,10 @@ 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.
+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. Any change made on the instance will be pushed back to the
+client-side record.
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
@@ -23,43 +24,5 @@ that will be called has the following 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.
-
+``on_change_with`` attribute. The method must return the new value of the field
+to push back to the client-side record.
diff --git a/doc/topics/modules/index.rst b/doc/topics/modules/index.rst
index fa79017..f1694ed 100644
--- a/doc/topics/modules/index.rst
+++ b/doc/topics/modules/index.rst
@@ -78,8 +78,10 @@ The following snippet gives a first idea of what an xml file looks:
<record model="res.group" id="group_party_admin">
<field name="name">Party Administration</field>
</record>
- <record model="res.user" id="res.user_admin">
- <field name="groups" eval="[('add', ref('group_party_admin'))]"/>
+ <record model="res.user-res.group"
+ id="user_admin_group_party_admin">
+ <field name="user" ref="res.user_admin"/>
+ <field name="group" ref="group_party_admin"/>
</record>
<menuitem name="Party Management" sequence="0" id="menu_party"
@@ -88,7 +90,7 @@ The following snippet gives a first idea of what an xml file looks:
<record model="ir.ui.view" id="party_view_tree">
<field name="model">party.party</field>
<field name="type">tree</field>
- <field name="arch" type="xml">
+ <field name="arch">
<![CDATA[
<tree string="Parties">
<field name="code"/>
@@ -137,7 +139,8 @@ Here is the list of the tags:
* ``eval``: Python code to evaluate and use result as value.
- * ``type``: If set to xml, it will use the CDATA content as value.
+ * ``pyson``: convert the evaluated value into :ref:`PYSON <ref-pyson>`
+ string.
.. note::
Field content is considered as a string. So for fields that require
diff --git a/doc/topics/reports/index.rst b/doc/topics/reports/index.rst
index 633820e..a63c3dc 100644
--- a/doc/topics/reports/index.rst
+++ b/doc/topics/reports/index.rst
@@ -221,8 +221,9 @@ Passing custom data to a report
TODO: Examples of overriding Report.execute.
-In this example `Report.parse` is overridden and an employee object is set into
-context. Now the invoice report will be able to access the employee object.
+In this example `Report.get_context` is overridden and an employee
+object is set into context. Now the invoice report will be able to access the
+employee object.
.. highlight:: python
@@ -234,14 +235,18 @@ context. Now the invoice report will be able to access the employee object.
class InvoiceReport(Report):
__name__ = 'account.invoice'
- def parse(self, report, objects, datas, localcontext):
- employee_obj = Pool().get('company.employee')
- employee = False
- if Transaction().context.get('employee'):
- employee = employee_obj.browse(Transaction().context['employee'])
- localcontext['employee'] = employee
- return super(InvoiceReport, self).parse(report, objects, datas,
- localcontext)
+ @classmethod
+ def get_context(cls, records, data):
+ pool = Pool()
+ Employee = pool.get('company.employee')
+
+ context = super(InvoiceReport, cls).get_context(records, data)
+ employee_id = Transaction().context.get('employee')
+ employee = Employee(employee_id) if employee_id else None
+ context['employee'] = employee
+
+ return context
+
Pool.register(InvoiceReport, type_='report')
diff --git a/doc/topics/views/index.rst b/doc/topics/views/index.rst
index 0f79504..479670c 100644
--- a/doc/topics/views/index.rst
+++ b/doc/topics/views/index.rst
@@ -153,11 +153,19 @@ Each form view must start with this tag.
* ``string``: The text that will be used as default title for the tab or
the window.
- * ``on_write``: The name of a function that will be called when the record
- is saved. The function must have this syntax:
+ .. _form-attributes-on_write:
+
+ * ``on_write``: The name of a method on the Model of the view that will be
+ called when a record is saved. The method must return a list of record
+ ids that the client must reload if they are already loaded. The function
+ must have this syntax:
``on_write(self, ids)``
+ .. note::
+ The method must be registered in :attr:`trytond.model.Model.__rpc__`.
+ ..
+
* ``col``: The number of columns for the view.
* ``cursor``: The name of the field that must have the cursor by default.
@@ -224,6 +232,9 @@ Display a field of the object with the value of the current record.
* ``view_ids``: A comma separated list that specifies the view ids used to
display the relation.
+ * ``product``: Only for One2Many fields, a comma separated list of target
+ field name used to create records from the cartesian product.
+
* ``completion``: Only for Many2One fields, it is a boolean to set the
completion of the field.
@@ -327,6 +338,7 @@ Display a button.
* ``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``: to reload the current tab
* ``reload context``: to reload user context
* ``reload menu``: to reload menu
@@ -475,10 +487,7 @@ Each tree view must start with this tag.
* ``string``: The text that will be used as default title for the tab or
the window.
- * ``on_write``: The name of a function that will be called when a record is
- saved. The function must have this syntax:
-
- ``on_write(self, ids)``
+ * ``on_write``: see form-attributes-on_write_.
* ``editable``: If it is set to ``top`` or ``bottom``, the list becomes
editable and the new record will be added on ``top`` or ``bottom`` of the
diff --git a/setup.py b/setup.py
index 8fee48d..72ea1c3 100644
--- a/setup.py
+++ b/setup.py
@@ -1,35 +1,46 @@
#!/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.
+# 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 setuptools import setup, find_packages
import os
-
-PACKAGE, VERSION, LICENSE, WEBSITE = None, None, None, None
-execfile(os.path.join('trytond', 'version.py'))
+import re
+import platform
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
-major_version, minor_version, _ = VERSION.split('.', 2)
+
+def get_version():
+ init = read(os.path.join('trytond', '__init__.py'))
+ return re.search('__version__ = "([0-9.]*)"', init).group(1)
+
+version = get_version()
+major_version, minor_version, _ = version.split('.', 2)
major_version = int(major_version)
minor_version = int(minor_version)
+name = 'trytond'
download_url = 'http://downloads.tryton.org/%s.%s/' % (
major_version, minor_version)
if minor_version % 2:
- VERSION = '%s.%s.dev0' % (major_version, minor_version)
+ version = '%s.%s.dev0' % (major_version, minor_version)
download_url = 'hg+http://hg.tryton.org/%s#egg=%s-%s' % (
- PACKAGE, PACKAGE, VERSION)
+ name, name, version)
+
+if platform.python_implementation() == 'PyPy':
+ pg_require = ['psycopg2cffi >= 2.5']
+else:
+ pg_require = ['psycopg2 >= 2.0']
-setup(name=PACKAGE,
- version=VERSION,
+setup(name=name,
+ version=version,
description='Tryton server',
long_description=read('README'),
author='Tryton',
author_email='issue_tracker at tryton.org',
- url=WEBSITE,
+ url='http://www.tryton.org/',
download_url=download_url,
keywords='business application platform ERP',
packages=find_packages(exclude=['*.modules.*', 'modules.*', 'modules',
@@ -65,20 +76,22 @@ setup(name=PACKAGE,
'Natural Language :: Spanish',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Libraries :: Application Frameworks',
],
platforms='any',
- license=LICENSE,
+ license='GPL-3',
install_requires=[
'lxml >= 2.0',
'relatorio >= 0.2.0',
'Genshi',
'python-dateutil',
'polib',
- 'python-sql >= 0.2',
+ 'python-sql >= 0.4',
],
extras_require={
- 'PostgreSQL': ['psycopg2 >= 2.0'],
+ 'PostgreSQL': pg_require,
'MySQL': ['MySQL-python'],
'WebDAV': ['PyWebDAV >= 0.9.8'],
'unoconv': ['unoconv'],
diff --git a/trytond.egg-info/PKG-INFO b/trytond.egg-info/PKG-INFO
index b0a810f..a676571 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.4.3
+Version: 3.6.0
Summary: Tryton server
Home-page: http://www.tryton.org/
Author: Tryton
Author-email: issue_tracker at tryton.org
License: GPL-3
-Download-URL: http://downloads.tryton.org/3.4/
+Download-URL: http://downloads.tryton.org/3.6/
Description: trytond
=======
@@ -116,4 +116,6 @@ Classifier: Natural Language :: Slovenian
Classifier: Natural Language :: Spanish
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
diff --git a/trytond.egg-info/SOURCES.txt b/trytond.egg-info/SOURCES.txt
index f5a045e..8f46cc3 100644
--- a/trytond.egg-info/SOURCES.txt
+++ b/trytond.egg-info/SOURCES.txt
@@ -4,7 +4,6 @@ INSTALL
LICENSE
MANIFEST.in
README
-TODO
setup.py
bin/trytond
doc/Makefile
@@ -54,7 +53,6 @@ trytond/server.py
trytond/test_loader.py
trytond/transaction.py
trytond/url.py
-trytond/version.py
trytond.egg-info/PKG-INFO
trytond.egg-info/SOURCES.txt
trytond.egg-info/dependency_links.txt
@@ -235,6 +233,7 @@ trytond/ir/view/ui_view_tree_state_list.xml
trytond/ir/view/ui_view_tree_width_form.xml
trytond/ir/view/ui_view_tree_width_list.xml
trytond/model/__init__.py
+trytond/model/descriptors.py
trytond/model/dictschema.py
trytond/model/match.py
trytond/model/model.py
@@ -313,6 +312,7 @@ trytond/tests/history.py
trytond/tests/import_data.py
trytond/tests/import_data.xml
trytond/tests/model.py
+trytond/tests/modelview.py
trytond/tests/mptt.py
trytond/tests/run-tests.py
trytond/tests/sequence.xml
@@ -320,17 +320,22 @@ trytond/tests/test.py
trytond/tests/test_access.py
trytond/tests/test_cache.py
trytond/tests/test_copy.py
+trytond/tests/test_descriptors.py
trytond/tests/test_exportdata.py
trytond/tests/test_field_context.py
trytond/tests/test_fields.py
trytond/tests/test_history.py
trytond/tests/test_importdata.py
+trytond/tests/test_ir.py
trytond/tests/test_mixins.py
trytond/tests/test_modelsingleton.py
trytond/tests/test_modelsql.py
+trytond/tests/test_modelstorage.py
+trytond/tests/test_modelview.py
trytond/tests/test_mptt.py
trytond/tests/test_protocols.py
trytond/tests/test_pyson.py
+trytond/tests/test_res.py
trytond/tests/test_sequence.py
trytond/tests/test_tools.py
trytond/tests/test_transaction.py
@@ -338,6 +343,7 @@ trytond/tests/test_trigger.py
trytond/tests/test_tryton.py
trytond/tests/test_union.py
trytond/tests/test_user.py
+trytond/tests/test_webdav.py
trytond/tests/test_wizard.py
trytond/tests/test_workflow.py
trytond/tests/trigger.py
@@ -349,6 +355,7 @@ trytond/tests/workflow.xml
trytond/tools/StringMatcher.py
trytond/tools/__init__.py
trytond/tools/datetime_strftime.py
+trytond/tools/decimal_.py
trytond/tools/misc.py
trytond/tools/singleton.py
trytond/webdav/__init__.py
diff --git a/trytond.egg-info/requires.txt b/trytond.egg-info/requires.txt
index 25bd7b5..b1f81fe 100644
--- a/trytond.egg-info/requires.txt
+++ b/trytond.egg-info/requires.txt
@@ -3,7 +3,7 @@ relatorio >= 0.2.0
Genshi
python-dateutil
polib
-python-sql >= 0.2
+python-sql >= 0.4
[cdecimal]
cdecimal
diff --git a/trytond/__init__.py b/trytond/__init__.py
index 80f6ae6..99f7805 100644
--- a/trytond/__init__.py
+++ b/trytond/__init__.py
@@ -1,12 +1,14 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 email import charset
-from . import server
-
-__all__ = ['server']
+__version__ = "3.6.0"
os.environ['TZ'] = 'UTC'
if hasattr(time, 'tzset'):
time.tzset()
+
+# set email encoding for utf-8 to 'quoted-printable'
+charset.add_charset('utf-8', charset.QP, charset.QP)
diff --git a/trytond/backend/__init__.py b/trytond/backend/__init__.py
index 6b9abc4..44264ef 100644
--- a/trytond/backend/__init__.py
+++ b/trytond/backend/__init__.py
@@ -1,5 +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.
+# 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 sys
import urlparse
@@ -9,7 +9,7 @@ __all__ = ['name', 'get']
def name():
- return urlparse.urlparse(config.get('database', 'uri', '')).scheme
+ return urlparse.urlparse(config.get('database', 'uri', default='')).scheme
def get(prop):
diff --git a/trytond/backend/database.py b/trytond/backend/database.py
index dae7908..6168a03 100644
--- a/trytond/backend/database.py
+++ b/trytond/backend/database.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.
-from trytond.const import MODEL_CACHE_SIZE
+# 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.config import config
DatabaseIntegrityError = None
DatabaseOperationalError = None
@@ -117,7 +117,8 @@ class CursorInterface(object):
context = Transaction().context
keys = tuple(((key, context[key]) for key in sorted(self.cache_keys)
if key in context))
- return self.cache.setdefault((user, keys), LRUDict(MODEL_CACHE_SIZE))
+ return self.cache.setdefault((user, keys),
+ LRUDict(config.getint('cache', 'model')))
def execute(self, sql, params=None):
'''
diff --git a/trytond/backend/mysql/__init__.py b/trytond/backend/mysql/__init__.py
index 7f49e23..235b423 100644
--- a/trytond/backend/mysql/__init__.py
+++ b/trytond/backend/mysql/__init__.py
@@ -1,5 +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.
+# 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 .database import *
from .table import *
diff --git a/trytond/backend/mysql/database.py b/trytond/backend/mysql/database.py
index 4960cfc..9f55458 100644
--- a/trytond/backend/mysql/database.py
+++ b/trytond/backend/mysql/database.py
@@ -1,5 +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.
+# 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.backend.database import DatabaseInterface, CursorInterface
from trytond.config import config, parse_uri
import MySQLdb
diff --git a/trytond/backend/mysql/table.py b/trytond/backend/mysql/table.py
index 3fc878f..59d68a1 100644
--- a/trytond/backend/mysql/table.py
+++ b/trytond/backend/mysql/table.py
@@ -1,9 +1,11 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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.backend.table import TableHandlerInterface
import logging
+logger = logging.getLogger(__name__)
+
class TableHandler(TableHandlerInterface):
@@ -41,7 +43,7 @@ class TableHandler(TableHandlerInterface):
self.cursor.execute('ALTER TABLE `%s` '
'ADD COLUMN id BIGINT' % self.table_name)
self._update_definitions()
- if self.history and not '__id' in self._columns:
+ if self.history and '__id' not in self._columns:
self.cursor.execute('ALTER TABLE `%s` '
'ADD COLUMN __id BIGINT AUTO_INCREMENT '
'NOT NULL PRIMARY KEY' % self.table_name)
@@ -56,12 +58,12 @@ class TableHandler(TableHandlerInterface):
@staticmethod
def table_rename(cursor, old_name, new_name):
- #Rename table
+ # Rename table
if (TableHandler.table_exist(cursor, old_name)
and not TableHandler.table_exist(cursor, new_name)):
cursor.execute('ALTER TABLE `%s` RENAME TO `%s`'
% (old_name, new_name))
- #Rename history table
+ # Rename history table
old_history = old_name + '__history'
new_history = new_name + '__history'
if (TableHandler.table_exist(cursor, old_history)
@@ -183,11 +185,11 @@ class TableHandler(TableHandlerInterface):
):
self.alter_type(column_name, base_type)
else:
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to migrate column %s on table %s '
- 'from %s to %s.'
- % (column_name, self.table_name,
- self._columns[column_name]['typname'], base_type))
+ 'from %s to %s.',
+ column_name, self.table_name,
+ self._columns[column_name]['typname'], base_type)
if (base_type == 'varchar'
and self._columns[column_name]['typname'] == 'varchar'):
# Migrate size
@@ -197,13 +199,13 @@ class TableHandler(TableHandlerInterface):
elif self._columns[column_name]['size'] == field_size:
pass
else:
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to migrate column %s on table %s '
- 'from varchar(%s) to varchar(%s).'
- % (column_name, self.table_name,
- self._columns[column_name]['size'] > 0
- and self._columns[column_name]['size'] or 255,
- field_size))
+ 'from varchar(%s) to varchar(%s).',
+ column_name, self.table_name,
+ self._columns[column_name]['size'] > 0
+ and self._columns[column_name]['size'] or 255,
+ field_size)
return
column_type = column_type[1]
@@ -294,17 +296,17 @@ class TableHandler(TableHandlerInterface):
self._column_definition(column_name, nullable=False)))
self._update_definitions()
else:
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to set column %s '
'of table %s not null !\n'
'Try to re-run: '
'trytond.py --update=module\n'
'If it doesn\'t work, update records '
'and execute manually:\n'
- 'ALTER TABLE `%s` MODIFY COLUMN `%s` %s'
- % (column_name, self.table_name, self.table_name,
- column_name, self._column_definition(column_name,
- nullable=False)))
+ 'ALTER TABLE `%s` MODIFY COLUMN `%s` %s',
+ column_name, self.table_name, self.table_name,
+ column_name, self._column_definition(column_name,
+ nullable=False))
elif action == 'remove':
if self._columns[column_name]['nullable']:
return
@@ -331,13 +333,13 @@ class TableHandler(TableHandlerInterface):
except Exception:
if exception:
raise
- logging.getLogger('init').warning(
+ logger.warning(
'unable to add \'%s\' constraint on table %s !\n'
'If you want to have it, you should update the records '
'and execute manually:\n'
- 'ALTER table `%s` ADD CONSTRAINT `%s` %s'
- % (constraint, self.table_name, self.table_name, ident,
- constraint,))
+ 'ALTER table `%s` ADD CONSTRAINT `%s` %s',
+ constraint, self.table_name, self.table_name, ident,
+ constraint, exc_info=True)
self._update_definitions()
def drop_constraint(self, ident, exception=False, table=None):
@@ -351,9 +353,9 @@ class TableHandler(TableHandlerInterface):
except Exception:
if exception:
raise
- logging.getLogger('init').warning(
- 'unable to drop \'%s\' constraint on table %s!'
- % (ident, self.table_name))
+ logger.warning(
+ 'unable to drop \'%s\' constraint on table %s!',
+ ident, self.table_name)
self._update_definitions()
def drop_column(self, column_name, exception=False):
@@ -367,9 +369,9 @@ class TableHandler(TableHandlerInterface):
except Exception:
if exception:
raise
- logging.getLogger('init').warning(
- 'unable to drop \'%s\' column on table %s!'
- % (column_name, self.table_name))
+ logger.warning(
+ 'unable to drop \'%s\' column on table %s!',
+ column_name, self.table_name)
self._update_definitions()
@staticmethod
diff --git a/trytond/backend/postgresql/__init__.py b/trytond/backend/postgresql/__init__.py
index 7f49e23..235b423 100644
--- a/trytond/backend/postgresql/__init__.py
+++ b/trytond/backend/postgresql/__init__.py
@@ -1,5 +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.
+# 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 .database import *
from .table import *
diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py
index 8316f63..9529d5c 100644
--- a/trytond/backend/postgresql/database.py
+++ b/trytond/backend/postgresql/database.py
@@ -1,7 +1,18 @@
-#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.backend.database import DatabaseInterface, CursorInterface
-from trytond.config import config, parse_uri
+# 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 time
+import logging
+import re
+import os
+if os.name == 'posix':
+ import pwd
+from decimal import Decimal
+
+try:
+ from psycopg2cffi import compat
+ compat.register()
+except ImportError:
+ pass
from psycopg2.pool import ThreadedConnectionPool
from psycopg2.extensions import cursor as PsycopgCursor
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
@@ -9,23 +20,22 @@ from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from psycopg2.extensions import register_type, register_adapter
from psycopg2.extensions import UNICODE, AsIs
try:
- from psycopg2.extensions import PYDATE, PYDATETIME, PYTIME
+ from psycopg2.extensions import PYDATE, PYDATETIME, PYTIME, PYINTERVAL
except ImportError:
- PYDATE, PYDATETIME, PYTIME = None, None, None
+ PYDATE, PYDATETIME, PYTIME, PYINTERVAL = None, None, None, None
from psycopg2 import IntegrityError as DatabaseIntegrityError
from psycopg2 import OperationalError as DatabaseOperationalError
-import time
-import logging
-import re
-import os
-if os.name == 'posix':
- import pwd
-from decimal import Decimal
+
from sql import Flavor
+from trytond.backend.database import DatabaseInterface, CursorInterface
+from trytond.config import config, parse_uri
+
__all__ = ['Database', 'DatabaseIntegrityError', 'DatabaseOperationalError',
'Cursor']
+logger = logging.getLogger(__name__)
+
RE_VERSION = re.compile(r'\S+ (\d+)\.(\d+)')
os.environ['PGTZ'] = os.environ.get('TZ', '')
@@ -52,8 +62,7 @@ class Database(DatabaseInterface):
def connect(self):
if self._connpool is not None:
return self
- logger = logging.getLogger('database')
- logger.info('connect to "%s"' % self.database_name)
+ logger.info('connect to "%s"', self.database_name)
uri = parse_uri(config.get('database', 'uri'))
assert uri.scheme == 'postgresql'
host = uri.hostname and "host=%s" % uri.hostname or ''
@@ -61,8 +70,8 @@ class Database(DatabaseInterface):
name = "dbname=%s" % self.database_name
user = uri.username and "user=%s" % uri.username or ''
password = uri.password and "password=%s" % uri.password or ''
- minconn = config.getint('database', 'minconn', 1)
- maxconn = config.getint('database', 'maxconn', 64)
+ minconn = config.getint('database', 'minconn', default=1)
+ maxconn = config.getint('database', 'maxconn', default=64)
dsn = '%s %s %s %s %s' % (host, port, name, user, password)
self._connpool = ThreadedConnectionPool(minconn, maxconn, dsn)
return self
@@ -379,5 +388,7 @@ if PYDATETIME:
register_type(PYDATETIME)
if PYTIME:
register_type(PYTIME)
+if PYINTERVAL:
+ register_type(PYINTERVAL)
register_adapter(float, lambda value: AsIs(repr(value)))
register_adapter(Decimal, lambda value: AsIs(str(value)))
diff --git a/trytond/backend/postgresql/table.py b/trytond/backend/postgresql/table.py
index be4478f..3256fd9 100644
--- a/trytond/backend/postgresql/table.py
+++ b/trytond/backend/postgresql/table.py
@@ -1,11 +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.
+# 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.backend.table import TableHandlerInterface
import logging
__all__ = ['TableHandler']
+logger = logging.getLogger(__name__)
+
class TableHandler(TableHandlerInterface):
@@ -43,7 +45,7 @@ class TableHandler(TableHandlerInterface):
self.cursor.execute('ALTER TABLE "%s" '
'ADD COLUMN id INTEGER' % self.table_name)
self._update_definitions()
- if self.history and not '__id' in self._columns:
+ if self.history and '__id' not in self._columns:
self.cursor.execute('ALTER TABLE "%s" '
'ADD COLUMN __id INTEGER '
'DEFAULT nextval(\'"%s"\') NOT NULL' %
@@ -69,7 +71,7 @@ class TableHandler(TableHandlerInterface):
@staticmethod
def table_rename(cursor, old_name, new_name):
- #Rename table
+ # Rename table
if (TableHandler.table_exist(cursor, old_name)
and not TableHandler.table_exist(cursor, new_name)):
cursor.execute('ALTER TABLE "%s" RENAME TO "%s"'
@@ -78,7 +80,7 @@ class TableHandler(TableHandlerInterface):
old_sequence = old_name + '_id_seq'
new_sequence = new_name + '_id_seq'
TableHandler.sequence_rename(cursor, old_sequence, new_sequence)
- #Rename history table
+ # Rename history table
old_history = old_name + "__history"
new_history = new_name + "__history"
if (TableHandler.table_exist(cursor, old_history)
@@ -209,7 +211,7 @@ class TableHandler(TableHandlerInterface):
if self.column_exist(column_name):
if (column_name in ('create_date', 'write_date')
and column_type[1].lower() != 'timestamp(6)'):
- #Migrate dates from timestamp(0) to timestamp
+ # Migrate dates from timestamp(0) to timestamp
self.cursor.execute('ALTER TABLE "' + self.table_name + '" '
'ALTER COLUMN "' + column_name + '" TYPE timestamp')
comment()
@@ -225,11 +227,11 @@ class TableHandler(TableHandlerInterface):
]:
self.alter_type(column_name, base_type)
else:
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to migrate column %s on table %s '
- 'from %s to %s.'
- % (column_name, self.table_name,
- self._columns[column_name]['typname'], base_type))
+ 'from %s to %s.',
+ column_name, self.table_name,
+ self._columns[column_name]['typname'], base_type)
if (base_type == 'varchar'
and self._columns[column_name]['typname'] == 'varchar'):
@@ -243,13 +245,13 @@ class TableHandler(TableHandlerInterface):
and self._columns[column_name]['size'] < field_size):
self.alter_size(column_name, column_type[1])
else:
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to migrate column %s on table %s '
- 'from varchar(%s) to varchar(%s).'
- % (column_name, self.table_name,
- self._columns[column_name]['size'] > 0 and
- self._columns[column_name]['size'] or "",
- field_size))
+ 'from varchar(%s) to varchar(%s).',
+ column_name, self.table_name,
+ self._columns[column_name]['size'] > 0 and
+ self._columns[column_name]['size'] or "",
+ field_size)
return
column_type = column_type[1]
@@ -350,16 +352,15 @@ class TableHandler(TableHandlerInterface):
'ALTER COLUMN "' + column_name + '" SET NOT NULL')
self._update_definitions()
else:
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to set column %s '
'of table %s not null !\n'
'Try to re-run: '
'trytond.py --update=module\n'
'If it doesn\'t work, update records '
'and execute manually:\n'
- 'ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL'
- % (column_name, self.table_name, self.table_name,
- column_name))
+ 'ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL',
+ column_name, self.table_name, self.table_name, column_name)
elif action == 'remove':
if not self._columns[column_name]['notnull']:
return
@@ -385,13 +386,13 @@ class TableHandler(TableHandlerInterface):
except Exception:
if exception:
raise
- logging.getLogger('init').warning(
+ logger.warning(
'unable to add \'%s\' constraint on table %s !\n'
'If you want to have it, you should update the records '
'and execute manually:\n'
- 'ALTER table "%s" ADD CONSTRAINT "%s" %s'
- % (constraint, self.table_name, self.table_name, ident,
- constraint))
+ 'ALTER table "%s" ADD CONSTRAINT "%s" %s',
+ constraint, self.table_name, self.table_name, ident,
+ constraint)
self._update_definitions()
def drop_constraint(self, ident, exception=False, table=None):
@@ -405,9 +406,9 @@ class TableHandler(TableHandlerInterface):
except Exception:
if exception:
raise
- logging.getLogger('init').warning(
- 'unable to drop \'%s\' constraint on table %s!'
- % (ident, self.table_name))
+ logger.warning(
+ 'unable to drop \'%s\' constraint on table %s!',
+ ident, self.table_name)
self._update_definitions()
def drop_column(self, column_name, exception=False):
@@ -421,9 +422,9 @@ class TableHandler(TableHandlerInterface):
except Exception:
if exception:
raise
- logging.getLogger('init').warning(
- 'unable to drop \'%s\' column on table %s!'
- % (column_name, self.table_name))
+ logger.warning(
+ 'unable to drop \'%s\' column on table %s!',
+ column_name, self.table_name, exc_info=True)
self._update_definitions()
@staticmethod
diff --git a/trytond/backend/sqlite/__init__.py b/trytond/backend/sqlite/__init__.py
index 7f49e23..235b423 100644
--- a/trytond/backend/sqlite/__init__.py
+++ b/trytond/backend/sqlite/__init__.py
@@ -1,5 +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.
+# 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 .database import *
from .table import *
diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py
index dd9b2c8..13bcaf2 100644
--- a/trytond/backend/sqlite/database.py
+++ b/trytond/backend/sqlite/database.py
@@ -1,5 +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.
+# 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.backend.database import DatabaseInterface, CursorInterface
from trytond.config import config
import os
@@ -14,7 +14,7 @@ try:
from pysqlite2 import dbapi2 as sqlite
from pysqlite2.dbapi2 import IntegrityError as DatabaseIntegrityError
from pysqlite2.dbapi2 import OperationalError as DatabaseOperationalError
- #pysqlite2 < 2.5 doesn't return correct rowcount
+ # pysqlite2 < 2.5 doesn't return correct rowcount
_FIX_ROWCOUNT = sqlite.version_info < (2, 5, 0)
except ImportError:
import sqlite3 as sqlite
@@ -183,7 +183,8 @@ class Database(DatabaseInterface):
raise IOError('Database "%s" doesn\'t exist!' % db_filename)
if self._conn is not None:
return self
- self._conn = sqlite.connect(path, detect_types=sqlite.PARSE_DECLTYPES)
+ self._conn = sqlite.connect(path,
+ detect_types=sqlite.PARSE_DECLTYPES | sqlite.PARSE_COLNAMES)
self._conn.create_function('extract', 2, SQLiteExtract.extract)
self._conn.create_function('date_trunc', 2, date_trunc)
self._conn.create_function('split_part', 3, split_part)
@@ -194,6 +195,8 @@ class Database(DatabaseInterface):
self._conn.create_function('replace', 3, replace)
self._conn.create_function('now', 0, now)
self._conn.create_function('sign', 1, sign)
+ self._conn.create_function('greatest', -1, max)
+ self._conn.create_function('least', -1, min)
self._conn.execute('PRAGMA foreign_keys = ON')
return self
@@ -405,11 +408,12 @@ class Cursor(CursorInterface):
def has_constraint(self):
return False
-sqlite.register_converter('NUMERIC', lambda val: Decimal(val))
+sqlite.register_converter('NUMERIC', lambda val: Decimal(val.decode('utf-8')))
if sys.version_info[0] == 2:
sqlite.register_adapter(Decimal, lambda val: buffer(str(val)))
+ sqlite.register_adapter(bytearray, lambda val: buffer(val))
else:
- sqlite.register_adapter(Decimal, lambda val: bytes(str(val)))
+ sqlite.register_adapter(Decimal, lambda val: str(val).encode('utf-8'))
def adapt_datetime(val):
@@ -417,4 +421,18 @@ def adapt_datetime(val):
sqlite.register_adapter(datetime.datetime, adapt_datetime)
sqlite.register_adapter(datetime.time, lambda val: val.isoformat())
sqlite.register_converter('TIME', lambda val: datetime.time(*map(int,
- val.split(':'))))
+ val.decode('utf-8').split(':'))))
+sqlite.register_adapter(datetime.timedelta, lambda val: val.total_seconds())
+
+
+def convert_interval(value):
+ value = float(value)
+ # It is not allowed to instatiate timedelta with the min/max total seconds
+ if value >= _interval_max:
+ return datetime.timedelta.max
+ elif value <= _interval_min:
+ return datetime.timedelta.min
+ return datetime.timedelta(seconds=value)
+_interval_max = datetime.timedelta.max.total_seconds()
+_interval_min = datetime.timedelta.min.total_seconds()
+sqlite.register_converter('INTERVAL', convert_interval)
diff --git a/trytond/backend/sqlite/table.py b/trytond/backend/sqlite/table.py
index f2fafcb..0e7317b 100644
--- a/trytond/backend/sqlite/table.py
+++ b/trytond/backend/sqlite/table.py
@@ -1,5 +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.
+# 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.backend.table import TableHandlerInterface
import logging
@@ -7,6 +7,8 @@ import re
__all__ = ['TableHandler']
+logger = logging.getLogger(__name__)
+
class TableHandler(TableHandlerInterface):
def __init__(self, cursor, model, module_name=None, history=False):
@@ -44,7 +46,7 @@ class TableHandler(TableHandlerInterface):
# Migration from 1.6 add autoincrement
- if not 'AUTOINCREMENT' in sql.upper():
+ if 'AUTOINCREMENT' not in sql.upper():
temp_sql = sql.replace(table_name, '_temp_%s' % table_name)
cursor.execute(temp_sql)
cursor.execute('PRAGMA table_info("' + table_name + '")')
@@ -71,7 +73,7 @@ class TableHandler(TableHandlerInterface):
and not TableHandler.table_exist(cursor, new_name)):
cursor.execute('ALTER TABLE "%s" RENAME TO "%s"'
% (old_name, new_name))
- #Rename history table
+ # Rename history table
old_history = old_name + "__history"
new_history = new_name + "__history"
if (TableHandler.table_exist(cursor, old_history)
@@ -159,22 +161,22 @@ class TableHandler(TableHandlerInterface):
self._field2module[line[0]] = line[1]
def alter_size(self, column_name, column_type):
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to alter size of column %s '
- 'of table %s!'
- % (column_name, self.table_name))
+ 'of table %s!',
+ column_name, self.table_name)
def alter_type(self, column_name, column_type):
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to alter type of column %s '
- 'of table %s!'
- % (column_name, self.table_name))
+ 'of table %s!',
+ column_name, self.table_name)
def db_default(self, column_name, value):
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to set default on column %s '
- 'of table %s!'
- % (column_name, self.table_name))
+ 'of table %s!',
+ column_name, self.table_name)
def add_raw_column(self, column_name, column_type, column_format,
default_fun=None, field_size=None, migrate=True, string=''):
@@ -191,11 +193,11 @@ class TableHandler(TableHandlerInterface):
]:
self.alter_type(column_name, base_type)
else:
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to migrate column %s on table %s '
- 'from %s to %s.'
- % (column_name, self.table_name,
- self._columns[column_name]['typname'], base_type))
+ 'from %s to %s.',
+ column_name, self.table_name,
+ self._columns[column_name]['typname'], base_type)
if (base_type == 'VARCHAR'
and self._columns[column_name]['typname'] == 'VARCHAR'):
@@ -209,13 +211,13 @@ class TableHandler(TableHandlerInterface):
and self._columns[column_name]['size'] < field_size):
self.alter_size(column_name, column_type[1])
else:
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to migrate column %s on table %s '
- 'from varchar(%s) to varchar(%s).'
- % (column_name, self.table_name,
- self._columns[column_name]['size'] > 0
- and self._columns[column_name]['size'] or "",
- field_size))
+ 'from varchar(%s) to varchar(%s).',
+ column_name, self.table_name,
+ self._columns[column_name]['size'] > 0
+ and self._columns[column_name]['size'] or "",
+ field_size)
return
column_type = column_type[1]
@@ -238,14 +240,14 @@ class TableHandler(TableHandlerInterface):
self._update_definitions()
def add_fk(self, column_name, reference, on_delete=None):
- logging.getLogger('init').warning(
- 'Unable to add foreign key on table %s!'
- % (self.table_name,))
+ logger.warning(
+ 'Unable to add foreign key on table %s!',
+ self.table_name)
def drop_fk(self, column_name, table=None):
- logging.getLogger('init').warning(
- 'Unable to drop foreign key on table %s!'
- % (self.table_name,))
+ logger.warning(
+ 'Unable to drop foreign key on table %s!',
+ self.table_name)
def index_action(self, column_name, action='add', table=None):
if isinstance(column_name, basestring):
@@ -277,32 +279,32 @@ class TableHandler(TableHandlerInterface):
return
if action == 'add':
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to set not null on column %s '
- 'of table %s!'
- % (column_name, self.table_name))
+ 'of table %s!',
+ column_name, self.table_name)
elif action == 'remove':
- logging.getLogger('init').warning(
+ logger.warning(
'Unable to remove not null on column %s '
- 'of table %s!'
- % (column_name, self.table_name))
+ 'of table %s!',
+ column_name, self.table_name)
else:
raise Exception('Not null action not supported!')
def add_constraint(self, ident, constraint, exception=False):
- logging.getLogger('init').warning(
- 'Unable to add constraint on table %s!'
- % (self.table_name,))
+ logger.warning(
+ 'Unable to add constraint on table %s!',
+ self.table_name)
def drop_constraint(self, ident, exception=False, table=None):
- logging.getLogger('init').warning(
- 'Unable to drop constraint on table %s!'
- % (self.table_name,))
+ logger.warning(
+ 'Unable to drop constraint on table %s!',
+ self.table_name)
def drop_column(self, column_name, exception=False):
- logging.getLogger('init').warning(
- 'Unable to drop \'%s\' column on table %s!'
- % (column_name, self.table_name))
+ logger.warning(
+ 'Unable to drop "%s" column on table %s!',
+ column_name, self.table_name)
@staticmethod
def drop_table(cursor, model, table, cascade=False):
diff --git a/trytond/backend/table.py b/trytond/backend/table.py
index 6f2a9cd..afdeb6b 100644
--- a/trytond/backend/table.py
+++ b/trytond/backend/table.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
class TableHandlerInterface(object):
diff --git a/trytond/cache.py b/trytond/cache.py
index 6ccd039..2caf83f 100644
--- a/trytond/cache.py
+++ b/trytond/cache.py
@@ -1,5 +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.
+# 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 threading import Lock
from collections import OrderedDict
@@ -116,6 +116,11 @@ class Cache(object):
Cache._resets[dbname].clear()
cursor.commit()
+ @classmethod
+ def drop(cls, dbname):
+ for inst in cls._cache_instance:
+ inst._cache.pop(dbname, None)
+
class LRUDict(OrderedDict):
"""
diff --git a/trytond/config.py b/trytond/config.py
index 99e3057..46f5d1f 100644
--- a/trytond/config.py
+++ b/trytond/config.py
@@ -1,5 +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.
+# 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 ConfigParser
import urlparse
@@ -51,6 +51,10 @@ class TrytonConfigParser(ConfigParser.RawConfigParser):
self.set('database', 'list', 'True')
self.set('database', 'retry', 5)
self.set('database', 'language', 'en_US')
+ self.add_section('cache')
+ self.set('cache', 'model', 200)
+ self.set('cache', 'record', 2000)
+ self.set('cache', 'field', 100)
self.add_section('ssl')
self.add_section('email')
self.set('email', 'uri', 'smtp://localhost:25')
@@ -65,30 +69,37 @@ class TrytonConfigParser(ConfigParser.RawConfigParser):
return
self.read(configfile)
- def get(self, section, option, default=None):
+ def get(self, section, option, *args, **kwargs):
+ default = kwargs.pop('default', None)
try:
- return ConfigParser.RawConfigParser.get(self, section, option)
+ return ConfigParser.RawConfigParser.get(self, section, option,
+ *args, **kwargs)
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
return default
- def getint(self, section, option, default=None):
+ def getint(self, section, option, *args, **kwargs):
+ default = kwargs.pop('default', None)
try:
- return ConfigParser.RawConfigParser.getint(self, section, option)
+ return ConfigParser.RawConfigParser.getint(self, section, option,
+ *args, **kwargs)
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError,
TypeError):
return default
- def getfloat(self, section, option, default=None):
+ def getfloat(self, section, option, *args, **kwargs):
+ default = kwargs.pop('default', None)
try:
- return ConfigParser.RawConfigParser.getfloat(self, section, option)
+ return ConfigParser.RawConfigParser.getfloat(self, section, option,
+ *args, **kwargs)
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError,
TypeError):
return default
- def getboolean(self, section, option, default=None):
+ def getboolean(self, section, option, *args, **kwargs):
+ default = kwargs.pop('default', None)
try:
return ConfigParser.RawConfigParser.getboolean(
- self, section, option)
+ self, section, option, *args, **kwargs)
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError,
AttributeError):
return default
diff --git a/trytond/const.py b/trytond/const.py
index 13995b3..23503c0 100644
--- a/trytond/const.py
+++ b/trytond/const.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.
-
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
OPERATORS = (
'child_of',
'not child_of',
@@ -16,8 +15,4 @@ OPERATORS = (
'>=',
'<',
'>',
-)
-
-MODEL_CACHE_SIZE = 200
-RECORD_CACHE_SIZE = 2000
-BROWSE_FIELD_TRESHOLD = 100
+ )
diff --git a/trytond/convert.py b/trytond/convert.py
index 8d5515c..fea9638 100644
--- a/trytond/convert.py
+++ b/trytond/convert.py
@@ -1,18 +1,20 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 time
from xml import sax
import logging
-import traceback
-import sys
import re
from sql import Table
from itertools import izip
from collections import defaultdict
+from decimal import Decimal
-from .version import VERSION
-from .tools import safe_eval, grouped_slice
+from . import __version__
+from .tools import grouped_slice
from .transaction import Transaction
+from .pyson import PYSONEncoder, CONTEXT
+
+logger = logging.getLogger(__name__)
CDATA_START = re.compile('^\s*\<\!\[cdata\[', re.IGNORECASE)
CDATA_END = re.compile('\]\]\>\s*$', re.IGNORECASE)
@@ -53,7 +55,7 @@ class MenuitemTagHandler:
values[attr] = attributes.get(attr)
if attributes.get('active'):
- values['active'] = bool(safe_eval(attributes['active']))
+ values['active'] = bool(eval(attributes['active']))
if values.get('parent'):
values['parent'] = self.mh.get_id(values['parent'])
@@ -208,12 +210,13 @@ class RecordTagHandler:
search_attr = attributes.get('search', '')
ref_attr = attributes.get('ref', '')
eval_attr = attributes.get('eval', '')
+ pyson_attr = bool(int(attributes.get('pyson', '0')))
if search_attr:
search_model = self.model._fields[field_name].model_name
SearchModel = self.mh.pool.get(search_model)
with Transaction().set_context(active_test=False):
- found, = SearchModel.search(safe_eval(search_attr))
+ found, = SearchModel.search(eval(search_attr))
self.values[field_name] = found.id
elif ref_attr:
@@ -222,10 +225,16 @@ class RecordTagHandler:
elif eval_attr:
context = {}
context['time'] = time
- context['version'] = VERSION.rsplit('.', 1)[0]
+ context['version'] = __version__.rsplit('.', 1)[0]
context['ref'] = self.mh.get_id
context['obj'] = lambda *a: 1
- self.values[field_name] = safe_eval(eval_attr, context)
+ context['Decimal'] = Decimal
+ if pyson_attr:
+ context.update(CONTEXT)
+ value = eval(eval_attr, context)
+ if pyson_attr:
+ value = PYSONEncoder().encode(value)
+ self.values[field_name] = value
else:
raise Exception("Tags '%s' not supported inside tag record." %
@@ -325,7 +334,7 @@ class Fs2bdAccessor:
Whe call the prefetch function here to. Like that whe are sure
not to erase data when get is called.
"""
- if not module in self.fetched_modules:
+ if module not in self.fetched_modules:
self.fetch_new_module(module)
if fs_id not in self.fs2db[module]:
self.fs2db[module][fs_id] = {}
@@ -366,13 +375,12 @@ class Fs2bdAccessor:
record_ids.setdefault(rec.model, [])
record_ids[rec.model].append(rec.db_id)
- object_name_list = set(self.pool.object_name_list())
-
self.browserecord[module] = {}
for model_name in record_ids.keys():
- if model_name not in object_name_list:
+ try:
+ Model = self.pool.get(model_name)
+ except KeyError:
continue
- Model = self.pool.get(model_name)
self.browserecord[module][model_name] = {}
for sub_record_ids in grouped_slice(record_ids[model_name]):
with Transaction().set_context(active_test=False):
@@ -436,8 +444,9 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
try:
self.sax_parser.parse(source)
except Exception:
- logging.getLogger("convert").error(
- "Error while parsing xml file:\n" + self.current_state())
+ logger.error(
+ "Error while parsing xml file:\n" + self.current_state(),
+ exc_info=True)
raise
return self.to_delete
@@ -469,8 +478,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
pass
else:
- logging.getLogger("convert").info("Tag %s not supported" %
- name)
+ logger.info("Tag %s not supported", (name,))
return
elif not self.skip_data:
self.taghandler.startElement(name, attributes)
@@ -535,16 +543,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
elif field_type == 'reference':
if not getattr(record, key):
return None
- elif not isinstance(getattr(record, key), basestring):
- return str(getattr(record, key))
- ref_mode, ref_id = getattr(record, key).split(',', 1)
- try:
- ref_id = safe_eval(ref_id)
- except Exception:
- pass
- if isinstance(ref_id, (list, tuple)):
- ref_id = ref_id[0]
- return ref_mode + ',' + str(ref_id)
+ return str(getattr(record, key))
elif field_type in ['one2many', 'many2many']:
raise Unhandled_field("Unhandled field %s" % key)
else:
@@ -613,7 +612,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
% (fs_id, module))
record = self.fs2db.get_browserecord(module, Model.__name__, db_id)
- #Re-create record if it was deleted
+ # Re-create record if it was deleted
if not record:
with Transaction().set_context(
module=module, language='en_US'):
@@ -668,10 +667,10 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
# if they are not false in a boolean context (ie None,
# False, {} or [])
if db_field != expected_value and (db_field or expected_value):
- logging.getLogger("convert").warning(
+ logger.warning(
"Field %s of %s@%s not updated (id: %s), because "
- "it has changed since the last update"
- % (key, record.id, model, fs_id))
+ "it has changed since the last update",
+ key, record.id, model, fs_id)
continue
# so, the field in the fs and in the db are different,
@@ -792,34 +791,33 @@ def post_import(pool, module, to_delete):
('module', '=', module),
], order=[('id', 'DESC')])
- object_name_list = set(pool.object_name_list())
for mrec in mdata:
model, db_id = mrec.model, mrec.db_id
- logging.getLogger("convert").info(
- 'Deleting %s@%s' % (db_id, model))
+ logger.info('Deleting %s@%s', db_id, model)
try:
# Deletion of the record
- if model in object_name_list:
+ try:
Model = pool.get(model)
+ except KeyError:
+ Model = None
+ if Model:
Model.delete([Model(db_id)])
mdata_delete.append(mrec)
else:
- logging.getLogger("convert").warning(
- 'Could not delete id %d of model %s because model no '
- 'longer exists.' % (db_id, model))
+ logger.warning(
+ 'Could not delete id %d of model %s because model no '
+ 'longer exists.', db_id, model)
cursor.commit()
except Exception:
cursor.rollback()
- tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
- logging.getLogger("convert").error(
+ logger.error(
'Could not delete id: %d of model %s\n'
- 'There should be some relation '
- 'that points to this resource\n'
- 'You should manually fix this '
- 'and restart --update=module\n'
- 'Exception: %s' %
- (db_id, model, tb_s))
+ 'There should be some relation '
+ 'that points to this resource\n'
+ 'You should manually fix this '
+ 'and restart --update=module\n',
+ db_id, model, exc_info=True)
if 'active' in Model._fields:
Model.write([Model(db_id)], {
'active': False,
diff --git a/trytond/error.py b/trytond/error.py
index cb61f70..97a86bd 100644
--- a/trytond/error.py
+++ b/trytond/error.py
@@ -1,5 +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.
+# 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.transaction import Transaction
from trytond.pool import Pool
from trytond.exceptions import UserError, UserWarning
diff --git a/trytond/exceptions.py b/trytond/exceptions.py
index 5f3db66..dd086b1 100644
--- a/trytond/exceptions.py
+++ b/trytond/exceptions.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
class UserError(Exception):
diff --git a/trytond/ir/__init__.py b/trytond/ir/__init__.py
index 062897b..ab1e31e 100644
--- a/trytond/ir/__init__.py
+++ b/trytond/ir/__init__.py
@@ -1,5 +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.
+# 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 ..pool import Pool
from .configuration import *
from .translation import *
diff --git a/trytond/ir/action.py b/trytond/ir/action.py
index 606796a..2482c6b 100644
--- a/trytond/ir/action.py
+++ b/trytond/ir/action.py
@@ -1,5 +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.
+# 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 base64
import os
from operator import itemgetter
@@ -10,9 +10,9 @@ from sql import Table
from sql.aggregate import Count
from ..model import ModelView, ModelStorage, ModelSQL, fields
-from ..tools import file_open, safe_eval
+from ..tools import file_open
from .. import backend
-from ..pyson import PYSONEncoder, CONTEXT, PYSON
+from ..pyson import PYSONDecoder, PYSON
from ..transaction import Transaction
from ..pool import Pool
from ..cache import Cache
@@ -477,10 +477,6 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
@classmethod
def __setup__(cls):
super(ActionReport, cls).__setup__()
- cls._sql_constraints += [
- ('report_name_module_uniq', 'UNIQUE(report_name, module)',
- 'The internal name must be unique by module!'),
- ]
cls._error_messages.update({
'invalid_email': 'Invalid email definition on report "%s".',
})
@@ -537,13 +533,17 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
limit=limit, offset=offset))
for report_id, report in cursor.fetchall():
if report:
- report = buffer(base64.decodestring(str(report)))
+ report = fields.Binary.cast(
+ base64.decodestring(bytes(report)))
cursor.execute(*action_report.update(
[action_report.report_content_custom],
[report],
where=action_report.id == report_id))
table.drop_column('report_content_data')
+ # Migration from 3.4 remove report_name_module_uniq constraint
+ table.drop_constraint('report_name_module_uniq')
+
@staticmethod
def default_type():
return 'ir.action.report'
@@ -579,7 +579,7 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
for report in reports:
if report.email:
try:
- value = safe_eval(report.email, CONTEXT)
+ value = PYSONDecoder().decode(report.email)
except Exception:
value = None
if isinstance(value, dict):
@@ -593,7 +593,7 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
@classmethod
def get_report_content(cls, reports, name):
contents = {}
- converter = buffer
+ converter = fields.Binary.cast
default = None
format_ = Transaction().context.pop('%s.%s'
% (cls.__name__, name), '')
@@ -625,7 +625,7 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
@classmethod
def get_style_content(cls, reports, name):
contents = {}
- converter = buffer
+ converter = fields.Binary.cast
default = None
format_ = Transaction().context.pop('%s.%s'
% (cls.__name__, name), '')
@@ -645,14 +645,13 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
@classmethod
def get_pyson(cls, reports, name):
pysons = {}
- encoder = PYSONEncoder()
field = name[6:]
defaults = {
'email': '{}',
}
for report in reports:
- pysons[report.id] = encoder.encode(safe_eval(getattr(report, field)
- or defaults.get(field, 'None'), CONTEXT))
+ pysons[report.id] = (getattr(report, field)
+ or defaults.get(field, 'null'))
return pysons
@classmethod
@@ -812,7 +811,7 @@ class ActionActWindow(ActionMixin, ModelSQL, ModelView):
if not domain:
continue
try:
- value = safe_eval(domain, CONTEXT)
+ value = PYSONDecoder().decode(domain)
except Exception:
cls.raise_user_error('invalid_domain', {
'domain': domain,
@@ -844,7 +843,7 @@ class ActionActWindow(ActionMixin, ModelSQL, ModelView):
for action in actions:
if action.context:
try:
- value = safe_eval(action.context, CONTEXT)
+ value = PYSONDecoder().decode(action.context)
except Exception:
cls.raise_user_error('invalid_context', {
'context': action.context,
@@ -875,15 +874,12 @@ class ActionActWindow(ActionMixin, ModelSQL, ModelView):
for view in self.act_window_views]
def get_domains(self, name):
- encoder = PYSONEncoder()
- return [(domain.name,
- encoder.encode(safe_eval(domain.domain or '[]', CONTEXT)))
+ return [(domain.name, domain.domain or '[]')
for domain in self.act_window_domains]
@classmethod
def get_pyson(cls, windows, name):
pysons = {}
- encoder = PYSONEncoder()
field = name[6:]
defaults = {
'domain': '[]',
@@ -891,8 +887,8 @@ class ActionActWindow(ActionMixin, ModelSQL, ModelView):
'search_value': '{}',
}
for window in windows:
- pysons[window.id] = encoder.encode(safe_eval(getattr(window, field)
- or defaults.get(field, 'None'), CONTEXT))
+ pysons[window.id] = (getattr(window, field)
+ or defaults.get(field, 'null'))
return pysons
@classmethod
@@ -971,11 +967,45 @@ class ActionActWindowDomain(ModelSQL, ModelView):
def __setup__(cls):
super(ActionActWindowDomain, cls).__setup__()
cls._order.insert(0, ('sequence', 'ASC'))
+ cls._error_messages.update({
+ 'invalid_domain': ('Invalid domain or search criteria '
+ '"%(domain)s" on action "%(action)s".'),
+ })
@staticmethod
def default_active():
return True
+ @classmethod
+ def validate(cls, actions):
+ super(ActionActWindowDomain, cls).validate(actions)
+ cls.check_domain(actions)
+
+ @classmethod
+ def check_domain(cls, actions):
+ for action in actions:
+ if not action.domain:
+ continue
+ try:
+ value = PYSONDecoder().decode(action.domain)
+ except Exception:
+ value = None
+ if isinstance(value, PYSON):
+ if not value.types() == set([list]):
+ value = None
+ elif not isinstance(value, list):
+ value = None
+ else:
+ try:
+ fields.domain_validate(value)
+ except Exception:
+ value = None
+ if value is None:
+ cls.raise_user_error('invalid_domain', {
+ 'domain': action.domain,
+ 'action': action.rec_name,
+ })
+
class ActionWizard(ActionMixin, ModelSQL, ModelView):
"Action wizard"
diff --git a/trytond/ir/attachment.py b/trytond/ir/attachment.py
index 0195c70..51f6097 100644
--- a/trytond/ir/attachment.py
+++ b/trytond/ir/attachment.py
@@ -1,8 +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.
+# 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 hashlib
from sql.operators import Concat
+from sql.conditionals import Coalesce
from ..model import ModelView, ModelSQL, fields
from ..config import config
@@ -54,6 +55,7 @@ class Attachment(ModelSQL, ModelView):
@classmethod
def __setup__(cls):
super(Attachment, cls).__setup__()
+ cls._order.insert(0, ('last_modification', 'DESC'))
cls._sql_constraints += [
('resource_name', 'UNIQUE(resource, name)',
'The names of attachments must be unique by resource!'),
@@ -128,7 +130,7 @@ class Attachment(ModelSQL, ModelView):
else:
try:
with open(filename, 'rb') as file_p:
- value = buffer(file_p.read())
+ value = fields.Binary.cast(file_p.read())
except IOError:
pass
return value
@@ -191,6 +193,11 @@ class Attachment(ModelSQL, ModelView):
return (self.write_date if self.write_date else self.create_date
).replace(microsecond=0)
+ @staticmethod
+ def order_last_modification(tables):
+ table, _ = tables[None]
+ return [Coalesce(table.write_date, table.create_date)]
+
def get_last_user(self, name):
return (self.write_uid.rec_name if self.write_uid
else self.create_uid.rec_name)
diff --git a/trytond/ir/cache.py b/trytond/ir/cache.py
index 5c83ceb..b38a6b7 100644
--- a/trytond/ir/cache.py
+++ b/trytond/ir/cache.py
@@ -1,5 +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.
+# 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 ..model import ModelSQL, fields
__all__ = [
diff --git a/trytond/ir/configuration.py b/trytond/ir/configuration.py
index 4ee7307..ed1b09e 100644
--- a/trytond/ir/configuration.py
+++ b/trytond/ir/configuration.py
@@ -1,5 +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.
+# 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 ..model import ModelSQL, ModelSingleton, fields
from ..cache import Cache
from ..config import config
diff --git a/trytond/ir/cron.py b/trytond/ir/cron.py
index 438a0d2..336def8 100644
--- a/trytond/ir/cron.py
+++ b/trytond/ir/cron.py
@@ -1,5 +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.
+# 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 datetime
from dateutil.relativedelta import relativedelta
import traceback
@@ -20,6 +20,8 @@ __all__ = [
'Cron',
]
+logger = logging.getLogger(__name__)
+
_INTERVALTYPES = {
'days': lambda interval: relativedelta(days=interval),
'hours': lambda interval: relativedelta(hours=interval),
@@ -140,7 +142,6 @@ class Cron(ModelSQL, ModelView):
msg['To'] = to_addr
msg['From'] = from_addr
msg['Subject'] = Header(subject, 'utf-8')
- logger = logging.getLogger(__name__)
if not to_addr:
logger.error(msg.as_string())
else:
@@ -148,9 +149,9 @@ class Cron(ModelSQL, ModelView):
server = get_smtp_server()
server.sendmail(from_addr, to_addr, msg.as_string())
server.quit()
- except Exception, exception:
- logger.error('Unable to deliver email (%s):\n %s'
- % (exception, msg.as_string()))
+ except Exception:
+ logger.error('Unable to deliver email:\n %s',
+ msg.as_string(), exc_info=True)
@classmethod
def _callback(cls, cron):
@@ -202,8 +203,4 @@ class Cron(ModelSQL, ModelView):
transaction.cursor.commit()
except Exception:
transaction.cursor.rollback()
- tb_s = reduce(lambda x, y: x + y,
- traceback.format_exception(*sys.exc_info()))
- tb_s = tb_s.decode('utf-8', 'ignore')
- logger = logging.getLogger('cron')
- logger.error('Exception:\n%s' % tb_s)
+ logger.error('Running cron %s', cron.id, exc_info=True)
diff --git a/trytond/ir/date.py b/trytond/ir/date.py
index fabf04a..41624ce 100644
--- a/trytond/ir/date.py
+++ b/trytond/ir/date.py
@@ -1,5 +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.
+# 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 datetime
from ..model import Model
diff --git a/trytond/ir/export.py b/trytond/ir/export.py
index c37b8bc..9210f39 100644
--- a/trytond/ir/export.py
+++ b/trytond/ir/export.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
"Exports"
from ..model import ModelView, ModelSQL, fields
diff --git a/trytond/ir/gen_time_locale.py b/trytond/ir/gen_time_locale.py
index 9f78290..4d153a2 100644
--- a/trytond/ir/gen_time_locale.py
+++ b/trytond/ir/gen_time_locale.py
@@ -1,5 +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.
+# 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 time
import locale
import os
@@ -183,8 +183,8 @@ if __name__ == '__main__':
with open(os.path.join(os.path.dirname(__file__), 'time_locale.py'),
'w') as fp:
fp.write('''# -*- 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.')
+# This file is part of Tryton. The COPYRIGHT file at the top level of')
+# this repository contains the full copyright notices and license terms.')
''')
time_locale = {}
fp.write('TIME_LOCALE = \\\n')
@@ -201,9 +201,8 @@ if __name__ == '__main__':
'fr_FR',
'nl_NL',
'ru_RU',
+ 'sl_SI',
]:
time_locale[lang] = locale_strftime(lang)
- #fp.write('"' + lang + '": ' + repr(time_locale) + ',\n')
- #fp.write('}\n')
pp = pprint.PrettyPrinter(stream=fp)
pp.pprint(time_locale)
diff --git a/trytond/ir/lang.py b/trytond/ir/lang.py
index c0e175b..3210fd6 100644
--- a/trytond/ir/lang.py
+++ b/trytond/ir/lang.py
@@ -1,5 +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.
+# 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 datetime
import warnings
from ast import literal_eval
@@ -36,10 +36,10 @@ class Lang(ModelSQL, ModelView):
('rtl', 'Right-to-left'),
], 'Direction', required=True)
- #date
+ # date
date = fields.Char('Date', required=True)
- #number
+ # number
grouping = fields.Char('Grouping', required=True)
decimal_point = fields.Char('Decimal Separator', required=True)
thousands_sep = fields.Char('Thousands Separator')
diff --git a/trytond/ir/lang.xml b/trytond/ir/lang.xml
index 00b333d..f6a953f 100644
--- a/trytond/ir/lang.xml
+++ b/trytond/ir/lang.xml
@@ -126,7 +126,7 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.action.act_window" id="act_lang_form">
<field name="name">Languages</field>
<field name="res_model">ir.lang</field>
- <field name="context">{'active_test': False}</field>
+ <field name="context" eval="{'active_test': False}" pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_lang_form_view1">
diff --git a/trytond/ir/locale/bg_BG.po b/trytond/ir/locale/bg_BG.po
index 4209638..0a8f43f 100644
--- a/trytond/ir/locale/bg_BG.po
+++ b/trytond/ir/locale/bg_BG.po
@@ -42,6 +42,10 @@ msgctxt "error:foreign_model_missing:"
msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
msgstr ""
+msgctxt "error:ir.action.act_window.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr ""
@@ -62,10 +66,6 @@ msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
msgstr ""
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "Вътрепното име трябва да е уникално в модула!"
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr "Имената на прикачените файлове трябва да са уникални по ресурс!"
@@ -2241,9 +2241,9 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Макс брой редове"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
-msgstr "Минимално забавяне"
+msgstr ""
msgctxt "field:ir.trigger,model:"
msgid "Model"
@@ -2768,7 +2768,9 @@ msgstr ""
"диаграмата."
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
msgstr ""
msgctxt "help:ir.rule.group,default_p:"
@@ -2803,13 +2805,11 @@ msgstr ""
"Ограничаване на броя извиквания на \"Действие на фунция\" по записи.\n"
"0 - няма ограничение"
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Минимално забавяне в минути между извикването на \"Действие на функция\" за този запис.\n"
-"0 за да няма забавяне"
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -4094,11 +4094,11 @@ msgstr "Печат"
#, fuzzy
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Добре"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr "Добре"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -4114,7 +4114,7 @@ msgid "Cancel"
msgstr "Отказ"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr "Добре"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
@@ -4134,7 +4134,7 @@ msgid "Cancel"
msgstr "Отказ"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Добре"
msgctxt "wizard_button:ir.translation.export,result,end:"
@@ -4160,7 +4160,7 @@ msgstr ""
#, fuzzy
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Добре"
msgctxt "wizard_button:ir.translation.update,start,end:"
diff --git a/trytond/ir/locale/ca_ES.po b/trytond/ir/locale/ca_ES.po
index 9e9483f..bb4e662 100644
--- a/trytond/ir/locale/ca_ES.po
+++ b/trytond/ir/locale/ca_ES.po
@@ -27,8 +27,8 @@ msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
"exceeds its limit."
msgstr ""
-"El nombre de dígits \"%(digits)s\" del camp \"%(field)s\" de \"%(value)s\" "
-"és superior al seu límit."
+"El nombre de decimals \"%(digits)s\" del camp \"%(field)s\" de \"%(value)s\""
+" és superior al seu límit."
msgctxt "error:domain_validation_record:"
msgid ""
@@ -50,6 +50,12 @@ msgctxt "error:foreign_model_missing:"
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.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+"El domini o el criteri de cerca \"%(domain)s\" de l'acció \"%(action)s\" no "
+"és correcte."
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr "El context \"%(context)s\" de l'acció \"%(action)s\" no és correcte."
@@ -66,17 +72,13 @@ msgstr "La vista \"%(view)s\" per l'acció \"%(action)s\" no és correcte."
msgctxt "error:ir.action.keyword:"
msgid "Wrong wizard model in keyword action \"%s\"."
-msgstr "Assistent incorrecte en l'acció de teclat \"%s\"."
+msgstr "El model d'assistent és incorrecte en l'acció de teclat \"%s\"."
msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
msgstr ""
"La definició del correu electrònic sobre l'informe \"%s\", no és correcta."
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "El nom intern ha de ser únic per mòdul."
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr "El nom dels adjunts ha de ser únic per recurs."
@@ -921,7 +923,7 @@ msgstr "Data"
msgctxt "field:ir.attachment,data_size:"
msgid "Data size"
-msgstr "Camp data"
+msgstr "Mida de les dades"
msgctxt "field:ir.attachment,description:"
msgid "Description"
@@ -1209,7 +1211,7 @@ msgstr "Direcció"
msgctxt "field:ir.lang,grouping:"
msgid "Grouping"
-msgstr "Agrupant"
+msgstr "Agrupació"
msgctxt "field:ir.lang,id:"
msgid "ID"
@@ -1265,7 +1267,7 @@ msgstr "Informació"
msgctxt "field:ir.model,model:"
msgid "Model Name"
-msgstr "Nom del model"
+msgstr "Model"
msgctxt "field:ir.model,module:"
msgid "Module"
@@ -2189,7 +2191,7 @@ msgstr "Idioma"
msgctxt "field:ir.trigger,action_function:"
msgid "Action Function"
-msgstr "Acció funció"
+msgstr "Funció de l'acció"
msgctxt "field:ir.trigger,action_model:"
msgid "Action Model"
@@ -2219,7 +2221,7 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Número límit"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
msgstr "Retard mínim"
@@ -2237,7 +2239,7 @@ msgstr "Al crear"
msgctxt "field:ir.trigger,on_delete:"
msgid "On Delete"
-msgstr "Al eliminar"
+msgstr "A l'eliminar"
msgctxt "field:ir.trigger,on_time:"
msgid "On Time"
@@ -2660,7 +2662,7 @@ msgid ""
"Leave empty for the same as template, see unoconv documentation for "
"compatible format"
msgstr ""
-"Deixeu-ho en blanc per mantenir el mateix que a la plantilla, veieu la "
+"Deixeu-lo buit per mantenir el mateix que a la plantilla, consulteu la "
"documentació de l'unoconv per conèixer formats compatibles."
msgctxt "help:ir.action.report,style:"
@@ -2714,8 +2716,12 @@ msgid ""
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 s'avalua amb \"user\" com l'usuari actual."
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
+msgstr ""
+"El domini s'avalua amb PYSON on el context conté:\n"
+"- \"user\" és l'usuari actual"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
@@ -2746,16 +2752,16 @@ msgid ""
"Limit the number of call to \"Action Function\" by records.\n"
"0 for no limit."
msgstr ""
-"Número límit de crides \"Acció de funcions\".\n"
-"0 per no límit"
+"Número límit de crides a la \"Funció de l'acció\".\n"
+"Poseu un 0 per indicar no límit."
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Marge mínim en minuts entre les crides \"Acció de funció\" pel mateix registre.\n"
-"0 per no deixar marge."
+"Indica un temps mínim de retard entre crides a la \"Funció de l'acció\" per al mateix registre.\n"
+"Deixeu-ho buit si no hi ha retard."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -3093,7 +3099,7 @@ msgstr "Mòdul"
msgctxt "model:ir.module.module.config_wizard.done,name:"
msgid "Module Config Wizard Done"
-msgstr "Assistent de configuració del modul finalitzat"
+msgstr "Finalització assistent de configuració de mòduls"
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
@@ -3113,7 +3119,7 @@ msgstr "Dependències del mòdul"
msgctxt "model:ir.module.module.install_upgrade.done,name:"
msgid "Module Install Upgrade Done"
-msgstr "Finalització instal·lació/actualització mòdul"
+msgstr "Finalització instal·lació/actualització de mòduls"
msgctxt "model:ir.module.module.install_upgrade.start,name:"
msgid "Module Install Upgrade Start"
@@ -3687,6 +3693,10 @@ msgctxt "view:ir.attachment:"
msgid "Attachments"
msgstr "Adjunts"
+msgctxt "view:ir.attachment:"
+msgid "Last Modification Time"
+msgstr "Hora última modificació"
+
msgctxt "view:ir.cron:"
msgid "Action to trigger"
msgstr "Acció de disparador"
@@ -3765,7 +3775,7 @@ 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."
+msgstr "La configuració s'ha realitzat correctament."
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
@@ -3931,7 +3941,7 @@ msgstr "Neteja traduccions"
msgctxt "view:ir.translation.clean.succeed:"
msgid "Clean Translations Succeed!"
-msgstr "La neteja de les traduccions s'ha realitzat."
+msgstr "La neteja de les traduccions s'ha realitzat correctament."
msgctxt "view:ir.translation.export.result:"
msgid "Export Translation"
@@ -4034,11 +4044,11 @@ msgid "Print"
msgstr "Imprimir"
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr "D'acord"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr "D'acord"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -4054,7 +4064,7 @@ msgid "Cancel"
msgstr "Cancel·la"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr "D'acord"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
@@ -4074,7 +4084,7 @@ msgid "Cancel"
msgstr "Cancel·la"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "D'acord"
msgctxt "wizard_button:ir.translation.export,result,end:"
@@ -4098,7 +4108,7 @@ msgid "Set"
msgstr "Defineix"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "D'acord"
msgctxt "wizard_button:ir.translation.update,start,end:"
diff --git a/trytond/ir/locale/cs_CZ.po b/trytond/ir/locale/cs_CZ.po
index a149887..dbedf88 100644
--- a/trytond/ir/locale/cs_CZ.po
+++ b/trytond/ir/locale/cs_CZ.po
@@ -40,6 +40,10 @@ msgctxt "error:foreign_model_missing:"
msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
msgstr ""
+msgctxt "error:ir.action.act_window.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr ""
@@ -60,10 +64,6 @@ msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
msgstr ""
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr ""
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr ""
@@ -2181,7 +2181,7 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr ""
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
msgstr ""
@@ -2670,7 +2670,9 @@ msgid ""
msgstr ""
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
msgstr ""
msgctxt "help:ir.rule.group,default_p:"
@@ -2699,10 +2701,10 @@ msgid ""
"0 for no limit."
msgstr ""
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
msgctxt "help:ir.ui.view_search,domain:"
@@ -3979,11 +3981,11 @@ msgid "Print"
msgstr ""
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr ""
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr ""
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -3999,7 +4001,7 @@ msgid "Cancel"
msgstr ""
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr ""
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
@@ -4019,7 +4021,7 @@ msgid "Cancel"
msgstr ""
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr ""
msgctxt "wizard_button:ir.translation.export,result,end:"
@@ -4043,7 +4045,7 @@ msgid "Set"
msgstr ""
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr ""
msgctxt "wizard_button:ir.translation.update,start,end:"
diff --git a/trytond/ir/locale/de_DE.po b/trytond/ir/locale/de_DE.po
index 7009184..cbcdb44 100644
--- a/trytond/ir/locale/de_DE.po
+++ b/trytond/ir/locale/de_DE.po
@@ -50,6 +50,12 @@ msgctxt "error:foreign_model_missing:"
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.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+"Ungültiger Wertebereich (Domain) oder Suchkriterien \"%(domain)s\" in Aktion"
+" \"%(action)s\"."
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr "Ungültiger Kontext \"%(context)s\" in Aktion \"%(action)s\"."
@@ -72,10 +78,6 @@ msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
msgstr "Ungültige E-Mailadresse in Bericht \"%s\"."
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "Der interne Name kann pro Modul nur einmal vergeben werden!"
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr ""
@@ -2197,7 +2199,7 @@ msgstr "Sprache"
msgctxt "field:ir.trigger,action_function:"
msgid "Action Function"
-msgstr "Aktion Funktion"
+msgstr "Aktionsfunktion"
msgctxt "field:ir.trigger,action_model:"
msgid "Action Model"
@@ -2227,9 +2229,9 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Limit Durchläufe"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
-msgstr "Minimale Verzögerung"
+msgstr "Minimalverzögerung"
msgctxt "field:ir.trigger,model:"
msgid "Model"
@@ -2381,7 +2383,7 @@ msgstr "Gruppen"
msgctxt "field:ir.ui.menu,icon:"
msgid "Icon"
-msgstr "Icon"
+msgstr "Symbol"
msgctxt "field:ir.ui.menu,id:"
msgid "ID"
@@ -2724,10 +2726,12 @@ msgstr ""
"Graphen ausschließen."
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
msgstr ""
-"Wertebereich (Domain) wird ausgewertet mit \"user\" als der aktuelle "
-"Benutzer"
+"Wertebereich (Domain) wird ausgewertet mit einem PYSON Kontext, der enthält:\n"
+"- \"user\" als der aktuelle Benutzer"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
@@ -2762,13 +2766,13 @@ msgstr ""
"Begrenzung der Anzahl von Aufrufen von \"Aktion Funktion\" durch Datensätze.\n"
"0 bedeutet kein Limit."
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Minimale Verzögerung in Minuten zwischen Aufrufen von \"Aktion Funktion\" für denselben Datensatz.\n"
-"0 bedeutet keine Verzögerung."
+"Einen Minimalzeitraum setzen für den Aufruf von 'Aktionsfunktion' für denselben Datensatz.\n"
+"Leer bedeutet keine Verzögerung."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -3700,6 +3704,10 @@ msgctxt "view:ir.attachment:"
msgid "Attachments"
msgstr "Anhänge"
+msgctxt "view:ir.attachment:"
+msgid "Last Modification Time"
+msgstr "Letzte Änderung"
+
msgctxt "view:ir.cron:"
msgid "Action to trigger"
msgstr "Auszuführende Aktion"
@@ -4080,11 +4088,11 @@ msgid "Print"
msgstr "Drucken"
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr "OK"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr "OK"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -4100,7 +4108,7 @@ msgid "Cancel"
msgstr "Abbrechen"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr "OK"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
@@ -4120,7 +4128,7 @@ msgid "Cancel"
msgstr "Abbrechen"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "OK"
msgctxt "wizard_button:ir.translation.export,result,end:"
@@ -4144,7 +4152,7 @@ msgid "Set"
msgstr "Aktualisieren"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "OK"
msgctxt "wizard_button:ir.translation.update,start,end:"
diff --git a/trytond/ir/locale/es_AR.po b/trytond/ir/locale/es_AR.po
index d59aedf..1f8b326 100644
--- a/trytond/ir/locale/es_AR.po
+++ b/trytond/ir/locale/es_AR.po
@@ -4,14 +4,6 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:access_error:"
msgid ""
-"You try to bypass an access rule!\n"
-"(Document type: %s)"
-msgstr ""
-"¡Está intentando evitar una regla de acceso!\n"
-"(Tipo de documento: %s)"
-
-msgctxt "error:access_error:"
-msgid ""
"You try to bypass an access rule.\n"
"(Document type: %s)"
msgstr ""
@@ -49,6 +41,12 @@ msgctxt "error:foreign_model_missing:"
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.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+"El dominio o el criterio de búsqueda «%(domain)s» en la acción «%(action)s» "
+"no es correcto."
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr "El contexto «%(context)s» en la acción «%(action)s» no es correcto."
@@ -72,10 +70,6 @@ msgid "Invalid email definition on report \"%s\"."
msgstr ""
"La definición de correo electrónico en el informe «%s» no es correcta."
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "¡El nombre interno de los módulos debe ser único!"
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr "¡Los nombres de los archivos adjuntos deben ser únicos por recurso!"
@@ -274,14 +268,6 @@ msgstr "XML de la vista «%s» no es correcto."
msgctxt "error:read_error:"
msgid ""
-"You try to read records that don't exist anymore!\n"
-"(Document type: %s)"
-msgstr ""
-"¡Está intentando leer registros que ya no existen!\n"
-"(Tipo de documento: %s)"
-
-msgctxt "error:read_error:"
-msgid ""
"You try to read records that don't exist anymore.\n"
"(Document type: %s)"
msgstr ""
@@ -346,14 +332,6 @@ msgstr "Demasiadas relaciones encontradas: %r en %s"
msgctxt "error:write_error:"
msgid ""
-"You try to write on records that don't exist anymore!\n"
-"(Document type: %s)"
-msgstr ""
-"¡Está tratando de escribir en registros que ya no existen!\n"
-"(Tipo de documento: %s)"
-
-msgctxt "error:write_error:"
-msgid ""
"You try to write on records that don't exist anymore.\n"
"(Document type: %s)"
msgstr ""
@@ -2224,7 +2202,7 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Número Límite"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
msgstr "Retardo Mínimo"
@@ -2721,8 +2699,12 @@ msgstr ""
"la gráfica."
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
-msgstr "El dominio es evaluado con «user» que es el usuario actual."
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
+msgstr ""
+"El dominio es evaluado con un contexto PYSON que contiene:\n"
+"- «user» como el usuario actual"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
@@ -2756,13 +2738,13 @@ msgstr ""
"Limitar el número de llamada a la \"Función de Acción\" por registros.\n"
"0 para ningún límite."
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Establecer un retraso mínimo en minutos entre llamadas a la «Función de la acción» para el mismo registro.\n"
-"0 para ningún retraso."
+"Definir un retraso mínimo entre llamadas a la «Función de la acción» para el mismo registro.\n"
+"Vacío para ningún retraso."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -3108,8 +3090,7 @@ msgstr "Asistente de configuración inicial del módulo"
msgctxt "model:ir.module.module.config_wizard.item,name:"
msgid "Config wizard to run after installing module"
-msgstr ""
-"El asistente de configuración a ejecutar después de instalar un módulo"
+msgstr "Asistente de configuración después de instalar un módulo"
msgctxt "model:ir.module.module.config_wizard.other,name:"
msgid "Module Config Wizard Other"
@@ -3493,7 +3474,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"
@@ -3605,7 +3586,7 @@ msgstr "Tablón"
msgctxt "selection:ir.ui.view,type:"
msgid "Calendar"
-msgstr "Calendarios"
+msgstr "Calendario"
msgctxt "selection:ir.ui.view,type:"
msgid "Form"
@@ -3695,6 +3676,10 @@ msgctxt "view:ir.attachment:"
msgid "Attachments"
msgstr "Adjuntos"
+msgctxt "view:ir.attachment:"
+msgid "Last Modification Time"
+msgstr "Última Modificación"
+
msgctxt "view:ir.cron:"
msgid "Action to trigger"
msgstr "Acción a disparar"
@@ -3881,38 +3866,6 @@ msgctxt "view:ir.rule:"
msgid "Test"
msgstr "Prueba"
-msgctxt "view:ir.sequence.strict:"
-msgid "${day}"
-msgstr "${day}"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "${month}"
-msgstr "${month}"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "${year}"
-msgstr "${year}"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Day:"
-msgstr "Día:"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Legend (for prefix, suffix)"
-msgstr "Leyenda (variables para usar en prefijo y/o sufijo)"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Month:"
-msgstr "Mes:"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Sequences Strict"
-msgstr "Secuencias estrictas"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Year:"
-msgstr "Año:"
-
msgctxt "view:ir.sequence.type:"
msgid "Sequence Type"
msgstr "Tipo de secuencia"
@@ -4074,11 +4027,11 @@ msgid "Print"
msgstr "Imprimir"
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -4094,7 +4047,7 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
@@ -4114,7 +4067,7 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.translation.export,result,end:"
@@ -4138,7 +4091,7 @@ msgid "Set"
msgstr "Definir"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.translation.update,start,end:"
diff --git a/trytond/ir/locale/es_CO.po b/trytond/ir/locale/es_CO.po
index 7b69425..f8cf8d3 100644
--- a/trytond/ir/locale/es_CO.po
+++ b/trytond/ir/locale/es_CO.po
@@ -50,6 +50,12 @@ msgstr ""
"El valor \"%(value)s\" en el campo \"%(field)s\" en el modelo \"%(model)s\" "
"no existe."
+msgctxt "error:ir.action.act_window.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+"Dominio inválido o criterio de busqueda \"%(domain)s\" en acción "
+"\"%(action)s\"."
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr "Contexto inválido \"%(context)s\" en action \"%(action)s\"."
@@ -72,10 +78,6 @@ msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%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!"
-msgstr "¡El nombre interno de los módulos debe ser único!"
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr "¡Los nombres de los archivos adjuntos deben ser únicos por recurso!"
@@ -2222,9 +2224,9 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Número Límite"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
-msgstr "Retardo Mínimo"
+msgstr ""
msgctxt "field:ir.trigger,model:"
msgid "Model"
@@ -2719,8 +2721,12 @@ msgstr ""
"concuerden con el gráfico."
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
-msgstr "El dominio es evaluado con el \"usuario\" como el usuario actual"
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
+msgstr ""
+"El dominio es evaluado en un contexto PYSON conteniendo: - \"usuario\" como "
+"el usuario actual"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
@@ -2754,13 +2760,13 @@ msgstr ""
"Limitar el número de llamada a la \"Función de Acción\" por registros.\n"
"0 para ningún límite."
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Establecer un retraso mínimo en minutos entre llamadas a la \"Función de Acción\" para el mismo registro.\n"
-"0 para ningún retraso."
+"Fijar un tiempo mínimo de retraso entre la llamada a la \"Función Acción\" "
+"para el mismo registro. Dejar vacío para no retraso."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -3693,6 +3699,10 @@ msgctxt "view:ir.attachment:"
msgid "Attachments"
msgstr "Adjuntos"
+msgctxt "view:ir.attachment:"
+msgid "Last Modification Time"
+msgstr ""
+
msgctxt "view:ir.cron:"
msgid "Action to trigger"
msgstr "Acción a disparar"
@@ -4073,11 +4083,11 @@ msgid "Print"
msgstr "Imprimir"
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -4093,7 +4103,7 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
@@ -4113,7 +4123,7 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.translation.export,result,end:"
@@ -4137,7 +4147,7 @@ msgid "Set"
msgstr "Establecer"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.translation.update,start,end:"
diff --git a/trytond/ir/locale/es_EC.po b/trytond/ir/locale/es_EC.po
index de8595c..e79ca9b 100644
--- a/trytond/ir/locale/es_EC.po
+++ b/trytond/ir/locale/es_EC.po
@@ -4,14 +4,6 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:access_error:"
msgid ""
-"You try to bypass an access rule!\n"
-"(Document type: %s)"
-msgstr ""
-"¡Está intentando evitar una regla de acceso!\n"
-"(Tipo de documento: %s)"
-
-msgctxt "error:access_error:"
-msgid ""
"You try to bypass an access rule.\n"
"(Document type: %s)"
msgstr ""
@@ -27,8 +19,8 @@ msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
"exceeds its limit."
msgstr ""
-"El número de decimales \"%(digits)s\" del campo \"%(field)s\" en el "
-"valor\"%(value)s\" excede su limite."
+"El número de decimales \"%(digits)s\" del campo \"%(field)s\" en "
+"\"%(value)s\" excede su limite."
msgctxt "error:domain_validation_record:"
msgid ""
@@ -50,6 +42,12 @@ msgctxt "error:foreign_model_missing:"
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.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+"El dominio o el criterio de búsqueda \"%(domain)s\" en la acción "
+"«%(action)s» no es válido."
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr "El contexto \"%(context)s\" en la acción \"%(action)s\" no es válido."
@@ -70,11 +68,7 @@ msgstr "Modelo de asistente incorrecto en la acción de teclado \"%s\"."
msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
-msgstr "La definición de correo electrónico sobre el informe \"%s\" no es válido."
-
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "¡El nombre interno debe ser único por módulo!"
+msgstr "La definición de correo electrónico sobre el informe \"%s\" no es válida."
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
@@ -92,7 +86,7 @@ msgid ""
"\n"
"%s\n"
msgstr ""
-"La siguiente acción falló cuando se ejecutaba: \"%s\"\n"
+"Ha fallado la acción cuando se ejecutaba: \"%s\"\n"
"%s\n"
" Traza del programa:\n"
"\n"
@@ -105,12 +99,12 @@ msgstr "El idioma por defecto no puede ser eliminado."
msgctxt "error:ir.lang:"
msgid "Invalid date format \"%(format)s\" on \"%(language)s\" language."
msgstr ""
-"El formato de la fecha \"%(format)s\" para el idioma \"%(languages)s\" no es"
-" válido."
+"El formato de la fecha \"%(format)s\" del idioma \"%(languages)s\" no es "
+"válido."
msgctxt "error:ir.lang:"
msgid "Invalid grouping \"%(grouping)s\" on \"%(language)s\" language."
-msgstr "Agrupamiento no válido \"%(grouping)s\" en idioma \"%(languages)s\"."
+msgstr "La agrupación \"%(grouping)s\" del idioma \"%(languages)s\" no es válida."
msgctxt "error:ir.lang:"
msgid "The default language must be translatable."
@@ -142,7 +136,7 @@ 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, modulo, modelo) 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)"
@@ -154,7 +148,8 @@ 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 nombre del Campo Modelo \"%s\" no es un identificador python válido."
+msgstr ""
+"El nombre del Campo Modelo \"%s\" no es un identificador de Python válido."
msgctxt "error:ir.model.field:"
msgid "The field name in model must be unique!"
@@ -162,7 +157,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 modulo \"%s\" no es un identificador python válido."
+msgstr "El nombre del modulo \"%s\" no es un identificador de Python válido."
msgctxt "error:ir.model:"
msgid "The model must be unique!"
@@ -170,7 +165,7 @@ msgstr "¡El modelo debe ser único!"
msgctxt "error:ir.module.module.dependency:"
msgid "Dependency must be unique by module!"
-msgstr "¡Las dependencias por módulo deben ser únicas!"
+msgstr "¡Las dependencias deben ser únicas por módulo!"
msgctxt "error:ir.module.module:"
msgid "Missing dependencies %s for module \"%s\""
@@ -187,28 +182,27 @@ msgstr "¡El nombre del módulo debe ser único!"
msgctxt "error:ir.module.module:"
msgid "You can not remove a module that is installed or will be installed"
-msgstr ""
-"No puede eliminar un módulo que está instalado o que va a ser instalado"
+msgstr "No puede eliminar un módulo que está instalado o que será instalado"
msgctxt "error:ir.rule.group:"
msgid "Global and Default are mutually exclusive!"
-msgstr "Global y Predeterminado son mutuamente excluyentes"
+msgstr "¡\"Global\" y \"Por Defecto\" son mutuamente excluyentes!"
msgctxt "error:ir.rule:"
msgid "Invalid domain in rule \"%s\"."
-msgstr "Dominio no válido en la regla \"%s\"."
+msgstr "El dominio en la regla \"%s\" no es válido."
msgctxt "error:ir.sequence.strict:"
msgid "Invalid prefix \"%(prefix)s\" on sequence \"%(sequence)s\"."
-msgstr "El prefijo \"%(prefix)s\" en la secuencia \"%(sequence)s\" no es válido."
+msgstr "El prefijo \"%(prefix)s\" de la secuencia \"%(sequence)s\" no es válido."
msgctxt "error:ir.sequence.strict:"
msgid "Invalid suffix \"%(suffix)s\" on sequence \"%(sequence)s\"."
-msgstr "El sufijo \"%(sufix)s\" de la secuencia \"%(sequence)s\" no es válido."
+msgstr "El sufijo \"%(suffix)s\" de la secuencia \"%(sequence)s\" no es válido."
msgctxt "error:ir.sequence.strict:"
msgid "Last Timestamp cannot be in the future on sequence \"%s\"."
-msgstr "La ultima fecha-hora no puede ser del futuro para la secuencia \"%s\"."
+msgstr "La ultima fecha-hora no puede ser del futuro en la secuencia \"%s\"."
msgctxt "error:ir.sequence.strict:"
msgid "Missing sequence."
@@ -220,16 +214,15 @@ msgstr "El redondeo de la fecha-hora debe ser mayor que 0"
msgctxt "error:ir.sequence:"
msgid "Invalid prefix \"%(prefix)s\" on sequence \"%(sequence)s\"."
-msgstr "El prefijo \"%(prefix)s\" en la secuencia \"%(sequence)s\" no es válido."
+msgstr "El prefijo \"%(prefix)s\" de la secuencia \"%(sequence)s\" no es válido."
msgctxt "error:ir.sequence:"
msgid "Invalid suffix \"%(suffix)s\" on sequence \"%(sequence)s\"."
-msgstr "El sufijo \"%(sufix)s\" de la secuencia \"%(sequence)s\" no es válido."
+msgstr "El sufijo \"%(suffix)s\" de la secuencia \"%(sequence)s\" no es válido."
msgctxt "error:ir.sequence:"
msgid "Last Timestamp cannot be in the future on sequence \"%s\"."
-msgstr ""
-"La ultima fecha-hora no puede estar en el futuro para la secuencia \"%s\"."
+msgstr "La ultima fecha-hora no puede ser del futuro en la secuencia \"%s\"."
msgctxt "error:ir.sequence:"
msgid "Missing sequence."
@@ -248,7 +241,7 @@ msgid ""
"You can not export translation %(name)s because it is an overridden "
"translation by module %(overriding_module)s"
msgstr ""
-"No puede exportar la traduccion %(name)s porque es una traducción "
+"No puede exportar la traducción %(name)s porque es una traducción "
"sobreescrita por el módulo %(overrinding_module)s"
msgctxt "error:ir.trigger:"
@@ -265,19 +258,11 @@ msgstr ""
msgctxt "error:ir.ui.menu:"
msgid "\"%s\" is not a valid menu name because it is not allowed to contain \" / \"."
-msgstr "\"%s\" no es un nombre válido de menú porque no esta permitido contener \"/\""
+msgstr "\"%s\" no es un nombre de menú válido porque no puede contener \"/\""
msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
-msgstr "XML no válido para la vista \"%s\"."
-
-msgctxt "error:read_error:"
-msgid ""
-"You try to read records that don't exist anymore!\n"
-"(Document type: %s)"
-msgstr ""
-"¡Está intentando leer registros que ya no existen!\n"
-"(Tipo de documento: %s)"
+msgstr "XML de la vista \"%s\" no es válido."
msgctxt "error:read_error:"
msgid ""
@@ -297,7 +282,7 @@ msgstr ""
msgctxt "error:reference_syntax_error:"
msgid "Syntax error for reference %r in %s"
-msgstr "Error de sintaxis para referencia %r en %s"
+msgstr "Error de sintaxis para la referencia %r de %s"
msgctxt "error:relation_not_found:"
msgid "Relation not found: %r in %s"
@@ -305,15 +290,15 @@ msgstr "Relación no encontrada: %r en %s"
msgctxt "error:required_field:"
msgid "The field \"%(field)s\" on \"%(model)s\" is required."
-msgstr "El campo \"%(field)s\" en \"%(model)s\" es requerido."
+msgstr "El campo \"%(field)s\" de \"%(model)s\" es requerido."
msgctxt "error:required_validation_record:"
msgid "The field \"%(field)s\" on \"%(model)s\" is required."
-msgstr "El campo \"%(field)s\" en \"%(model)s\" es requerido."
+msgstr "El campo \"%(field)s\" de \"%(model)s\" es requerido."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
-msgstr "Falta la función de búsqueda en el campo \"%s\"."
+msgstr "Falta la función de búsqueda del campo \"%s\"."
msgctxt "error:selection_validation_record:"
msgid ""
@@ -321,7 +306,7 @@ msgid ""
"the selection."
msgstr ""
"El valor \"%(value)s\" del campo \"%(field)s\" en el modelo \"%(model)s\" no"
-" esta en la selección."
+" está en la selección."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
@@ -330,7 +315,7 @@ msgstr "El valor no se encuentra en la selección para el campo \"%s\"."
msgctxt "error:size_validation_record:"
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\" is demasiado"
+"El tamaño \"%(size)s\" del campo \"%(field)s\" en \"%(model)s\" es demasiado"
" largo."
msgctxt "error:time_format_validation_record:"
@@ -341,15 +326,7 @@ msgstr ""
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
-msgstr "Demasiadas relaciones encontradas: %r en %s"
-
-msgctxt "error:write_error:"
-msgid ""
-"You try to write on records that don't exist anymore!\n"
-"(Document type: %s)"
-msgstr ""
-"¡Está tratando de escribir en registros que ya no existen!\n"
-"(Tipo de documento: %s)"
+msgstr "Se han encontrado demasiadas relaciones: %r en %s"
msgctxt "error:write_error:"
msgid ""
@@ -365,7 +342,7 @@ msgstr "No está autorizado para modificar este registro."
msgctxt "error:xml_id_syntax_error:"
msgid "Syntax error for XML id %r in %s"
-msgstr "Error de sintaxis para XML id %r en %s"
+msgstr "Error de sintaxis para el XML id %r de %s"
msgctxt "error:xml_record_desc:"
msgid "This record is part of the base configuration."
@@ -377,11 +354,11 @@ msgstr "Activo"
msgctxt "field:ir.action,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.action,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.action,groups:"
msgid "Groups"
@@ -417,11 +394,11 @@ msgstr "Uso"
msgctxt "field:ir.action,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.action,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.action.act_window,act_window_domains:"
msgid "Domains"
@@ -441,19 +418,19 @@ msgstr "Activo"
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"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.action.act_window,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+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"
@@ -485,7 +462,7 @@ msgstr "Nombre"
msgctxt "field:ir.action.act_window,order:"
msgid "Order Value"
-msgstr "Valor Ordenado"
+msgstr "Valor del orden"
msgctxt "field:ir.action.act_window,pyson_context:"
msgid "PySON Context"
@@ -493,7 +470,7 @@ msgstr "Contexto PySON"
msgctxt "field:ir.action.act_window,pyson_domain:"
msgid "PySON Domain"
-msgstr "Dominio de PySON"
+msgstr "Dominio PySON"
msgctxt "field:ir.action.act_window,pyson_order:"
msgid "PySON Order"
@@ -501,7 +478,7 @@ msgstr "Orden PySON"
msgctxt "field:ir.action.act_window,pyson_search_value:"
msgid "PySON Search Criteria"
-msgstr "Criterio de Búsqueda de PySON"
+msgstr "Criterio de búsqueda PySON"
msgctxt "field:ir.action.act_window,rec_name:"
msgid "Name"
@@ -513,7 +490,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"
@@ -529,15 +506,15 @@ msgstr "Vistas"
msgctxt "field:ir.action.act_window,window_name:"
msgid "Window Name"
-msgstr "Nombre de Ventana"
+msgstr "Nombre de la ventana"
msgctxt "field:ir.action.act_window,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.action.act_window,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.action.act_window.domain,act_window:"
msgid "Action"
@@ -549,11 +526,11 @@ msgstr "Activo"
msgctxt "field:ir.action.act_window.domain,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.action.act_window.domain,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.action.act_window.domain,domain:"
msgid "Domain"
@@ -577,11 +554,11 @@ msgstr "Secuencia"
msgctxt "field:ir.action.act_window.domain,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.action.act_window.domain,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
@@ -593,11 +570,11 @@ msgstr "Activo"
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.action.act_window.view,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.action.act_window.view,id:"
msgid "ID"
@@ -617,11 +594,11 @@ msgstr "Vista"
msgctxt "field:ir.action.act_window.view,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.action.act_window.view,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.action.keyword,action:"
msgid "Action"
@@ -629,11 +606,11 @@ msgstr "Acción"
msgctxt "field:ir.action.keyword,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.action.keyword,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.action.keyword,groups:"
msgid "Groups"
@@ -645,7 +622,7 @@ msgstr "ID"
msgctxt "field:ir.action.keyword,keyword:"
msgid "Keyword"
-msgstr "Tecla"
+msgstr "Acción de teclado"
msgctxt "field:ir.action.keyword,model:"
msgid "Model"
@@ -657,11 +634,11 @@ msgstr "Nombre"
msgctxt "field:ir.action.keyword,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.action.keyword,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.action.report,action:"
msgid "Action"
@@ -673,11 +650,11 @@ msgstr "Activo"
msgctxt "field:ir.action.report,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.action.report,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.action.report,direct_print:"
msgid "Direct Print"
@@ -769,11 +746,11 @@ msgstr "Uso"
msgctxt "field:ir.action.report,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.action.report,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.action.url,action:"
msgid "Action"
@@ -785,11 +762,11 @@ msgstr "Activo"
msgctxt "field:ir.action.url,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.action.url,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.action.url,groups:"
msgid "Groups"
@@ -805,7 +782,7 @@ msgstr "ID"
msgctxt "field:ir.action.url,keywords:"
msgid "Keywords"
-msgstr "Teclas"
+msgstr "Acción de teclado"
msgctxt "field:ir.action.url,name:"
msgid "Name"
@@ -821,7 +798,7 @@ msgstr "Tipo"
msgctxt "field:ir.action.url,url:"
msgid "Action Url"
-msgstr "URL de la acción"
+msgstr "Acción de URL"
msgctxt "field:ir.action.url,usage:"
msgid "Usage"
@@ -829,11 +806,11 @@ msgstr "Uso"
msgctxt "field:ir.action.url,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.action.url,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.action.wizard,action:"
msgid "Action"
@@ -845,11 +822,11 @@ msgstr "Activo"
msgctxt "field:ir.action.wizard,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.action.wizard,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.action.wizard,email:"
msgid "Email"
@@ -869,7 +846,7 @@ msgstr "ID"
msgctxt "field:ir.action.wizard,keywords:"
msgid "Keywords"
-msgstr "Teclas"
+msgstr "Acción de teclado"
msgctxt "field:ir.action.wizard,model:"
msgid "Model"
@@ -901,11 +878,11 @@ msgstr "Nombre del asistente"
msgctxt "field:ir.action.wizard,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.action.wizard,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.attachment,collision:"
msgid "Collision"
@@ -913,11 +890,11 @@ msgstr "Colisión"
msgctxt "field:ir.attachment,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.attachment,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.attachment,data:"
msgid "Data"
@@ -925,7 +902,7 @@ msgstr "Datos"
msgctxt "field:ir.attachment,data_size:"
msgid "Data size"
-msgstr "Tamaño de datos"
+msgstr "Tamaño de los datos"
msgctxt "field:ir.attachment,description:"
msgid "Description"
@@ -973,19 +950,19 @@ msgstr "Tipo"
msgctxt "field:ir.attachment,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.attachment,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.cache,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.cache,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.cache,id:"
msgid "ID"
@@ -1001,23 +978,23 @@ msgstr "Nombre"
msgctxt "field:ir.cache,timestamp:"
msgid "Timestamp"
-msgstr "Marca de tiempo"
+msgstr "Fecha-hora"
msgctxt "field:ir.cache,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.cache,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.configuration,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.configuration,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.configuration,id:"
msgid "ID"
@@ -1025,7 +1002,7 @@ msgstr "ID"
msgctxt "field:ir.configuration,language:"
msgid "language"
-msgstr "Lenguaje"
+msgstr "Idioma"
msgctxt "field:ir.configuration,rec_name:"
msgid "Name"
@@ -1033,11 +1010,11 @@ msgstr "Nombre"
msgctxt "field:ir.configuration,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.configuration,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.cron,active:"
msgid "Active"
@@ -1049,11 +1026,11 @@ msgstr "Argumentos"
msgctxt "field:ir.cron,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.cron,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.cron,function:"
msgid "Function"
@@ -1065,11 +1042,11 @@ msgstr "ID"
msgctxt "field:ir.cron,interval_number:"
msgid "Interval Number"
-msgstr "Número de Intervalos"
+msgstr "Número de intervalos"
msgctxt "field:ir.cron,interval_type:"
msgid "Interval Unit"
-msgstr "Unidad de Intervalo"
+msgstr "Unidad de intervalo"
msgctxt "field:ir.cron,model:"
msgid "Model"
@@ -1081,11 +1058,11 @@ msgstr "Nombre"
msgctxt "field:ir.cron,next_call:"
msgid "Next Call"
-msgstr "Siguiente Llamada"
+msgstr "Siguiente llamada"
msgctxt "field:ir.cron,number_calls:"
msgid "Number of Calls"
-msgstr "Número de Llamadas"
+msgstr "Número de llamadas"
msgctxt "field:ir.cron,rec_name:"
msgid "Name"
@@ -1093,23 +1070,23 @@ msgstr "Nombre"
msgctxt "field:ir.cron,repeat_missed:"
msgid "Repeat Missed"
-msgstr "Repetir Fallidos"
+msgstr "Repetir fallidos"
msgctxt "field:ir.cron,request_user:"
msgid "Request User"
-msgstr "Solicitud de Usuario"
+msgstr "Usuario solicitante"
msgctxt "field:ir.cron,user:"
msgid "Execution User"
-msgstr "Ejecutado por"
+msgstr "Usuario para la ejecución"
msgctxt "field:ir.cron,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.cron,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.date,id:"
msgid "ID"
@@ -1117,11 +1094,11 @@ msgstr "ID"
msgctxt "field:ir.export,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.export,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.export,export_fields:"
msgid "Fields"
@@ -1145,19 +1122,19 @@ msgstr "Recurso"
msgctxt "field:ir.export,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.export,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.export.line,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.export.line,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.export.line,export:"
msgid "Export"
@@ -1177,11 +1154,11 @@ msgstr "Nombre"
msgctxt "field:ir.export.line,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.export.line,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.lang,active:"
msgid "Active"
@@ -1193,11 +1170,11 @@ msgstr "Código"
msgctxt "field:ir.lang,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.lang,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.lang,date:"
msgid "Date"
@@ -1229,7 +1206,7 @@ msgstr "Nombre"
msgctxt "field:ir.lang,thousands_sep:"
msgid "Thousands Separator"
-msgstr "Separador de Miles"
+msgstr "Separador de miles"
msgctxt "field:ir.lang,translatable:"
msgid "Translatable"
@@ -1237,19 +1214,19 @@ msgstr "Traducible"
msgctxt "field:ir.lang,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.lang,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.model,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.model,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.model,fields:"
msgid "Fields"
@@ -1257,7 +1234,7 @@ msgstr "Campos"
msgctxt "field:ir.model,global_search_p:"
msgid "Global Search"
-msgstr "Busqueda Global"
+msgstr "Busqueda global"
msgctxt "field:ir.model,id:"
msgid "ID"
@@ -1269,7 +1246,7 @@ msgstr "Información"
msgctxt "field:ir.model,model:"
msgid "Model Name"
-msgstr "Nombre del Modelo"
+msgstr "Nombre del modelo"
msgctxt "field:ir.model,module:"
msgid "Module"
@@ -1285,19 +1262,19 @@ msgstr "Nombre"
msgctxt "field:ir.model,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.model,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.model.access,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.model.access,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.model.access,description:"
msgid "Description"
@@ -1317,19 +1294,19 @@ msgstr "Modelo"
msgctxt "field:ir.model.access,perm_create:"
msgid "Create Access"
-msgstr "Permiso para Crear"
+msgstr "Permiso para crear"
msgctxt "field:ir.model.access,perm_delete:"
msgid "Delete Access"
-msgstr "Permiso para Borrar"
+msgstr "Permiso para eliminar"
msgctxt "field:ir.model.access,perm_read:"
msgid "Read Access"
-msgstr "Permiso para Leer"
+msgstr "Permiso para leer"
msgctxt "field:ir.model.access,perm_write:"
msgid "Write Access"
-msgstr "Permiso para Modificar"
+msgstr "Permiso para modificar"
msgctxt "field:ir.model.access,rec_name:"
msgid "Name"
@@ -1337,19 +1314,19 @@ msgstr "Nombre"
msgctxt "field:ir.model.access,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.model.access,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.model.button,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.model.button,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.model.button,groups:"
msgid "Groups"
@@ -1373,23 +1350,23 @@ msgstr "Nombre"
msgctxt "field:ir.model.button,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.model.button,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.model.data,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
-msgstr "ID del recurso"
+msgstr "ID del Recurso"
msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
@@ -1413,11 +1390,11 @@ msgstr "Módulo"
msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
-msgstr "No Actualizar"
+msgstr "Sin Actualizar"
msgctxt "field:ir.model.data,out_of_sync:"
msgid "Out of Sync"
-msgstr "Fuera de Sincronización"
+msgstr "Fuera de sincronización"
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
@@ -1429,23 +1406,23 @@ msgstr "Valores"
msgctxt "field:ir.model.data,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.model.data,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.model.field,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.model.field,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+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"
@@ -1477,27 +1454,27 @@ msgstr "Nombre"
msgctxt "field:ir.model.field,relation:"
msgid "Model Relation"
-msgstr "Relación del Modelo"
+msgstr "Relación del modelo"
msgctxt "field:ir.model.field,ttype:"
msgid "Field Type"
-msgstr "Tipo de Campo"
+msgstr "Tipo de campo"
msgctxt "field:ir.model.field,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.model.field,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.model.field.access,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.model.field.access,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.model.field.access,description:"
msgid "Description"
@@ -1517,19 +1494,19 @@ msgstr "ID"
msgctxt "field:ir.model.field.access,perm_create:"
msgid "Create Access"
-msgstr "Permiso para Crear"
+msgstr "Permiso para crear"
msgctxt "field:ir.model.field.access,perm_delete:"
msgid "Delete Access"
-msgstr "Permiso para Eliminar"
+msgstr "Permiso para eliminar"
msgctxt "field:ir.model.field.access,perm_read:"
msgid "Read Access"
-msgstr "Permiso para Leer"
+msgstr "Permiso para leer"
msgctxt "field:ir.model.field.access,perm_write:"
msgid "Write Access"
-msgstr "Permiso para Modificar"
+msgstr "Permiso para modificar"
msgctxt "field:ir.model.field.access,rec_name:"
msgid "Name"
@@ -1537,11 +1514,11 @@ msgstr "Nombre"
msgctxt "field:ir.model.field.access,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.model.field.access,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.model.print_model_graph.start,filter:"
msgid "Filter"
@@ -1561,11 +1538,11 @@ msgstr "Hijos"
msgctxt "field:ir.module.module,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.module.module,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.module.module,dependencies:"
msgid "Dependencies"
@@ -1597,11 +1574,11 @@ msgstr "Versión"
msgctxt "field:ir.module.module,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.module.module,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.module.module.config_wizard.done,id:"
msgid "ID"
@@ -1617,11 +1594,11 @@ msgstr "Acción"
msgctxt "field:ir.module.module.config_wizard.item,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.module.module.config_wizard.item,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.module.module.config_wizard.item,id:"
msgid "ID"
@@ -1641,11 +1618,11 @@ msgstr "Estado"
msgctxt "field:ir.module.module.config_wizard.item,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.module.module.config_wizard.item,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.module.module.config_wizard.other,id:"
msgid "ID"
@@ -1657,11 +1634,11 @@ msgstr "Porcentaje"
msgctxt "field:ir.module.module.dependency,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.module.module.dependency,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.module.module.dependency,id:"
msgid "ID"
@@ -1685,11 +1662,11 @@ msgstr "Estado"
msgctxt "field:ir.module.module.dependency,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.module.module.dependency,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.module.module.install_upgrade.done,id:"
msgid "ID"
@@ -1705,11 +1682,11 @@ msgstr "Módulos a actualizar"
msgctxt "field:ir.property,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.property,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.property,field:"
msgid "Field"
@@ -1733,19 +1710,19 @@ msgstr "Valor"
msgctxt "field:ir.property,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.property,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.rule,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.rule,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.rule,domain:"
msgid "Domain"
@@ -1765,23 +1742,23 @@ msgstr "Grupo"
msgctxt "field:ir.rule,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.rule,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.rule.group,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.rule.group,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.rule.group,default_p:"
msgid "Default"
-msgstr "Predeterminado"
+msgstr "Por defecto"
msgctxt "field:ir.rule.group,global_p:"
msgid "Global"
@@ -1805,19 +1782,19 @@ msgstr "Nombre"
msgctxt "field:ir.rule.group,perm_create:"
msgid "Create Access"
-msgstr "Permiso para Crear"
+msgstr "Permiso para crear"
msgctxt "field:ir.rule.group,perm_delete:"
msgid "Delete Access"
-msgstr "Permiso para Eliminar"
+msgstr "Permiso para eliminar"
msgctxt "field:ir.rule.group,perm_read:"
msgid "Read Access"
-msgstr "Permiso para Leer"
+msgstr "Permiso para leer"
msgctxt "field:ir.rule.group,perm_write:"
msgid "Write Access"
-msgstr "Permiso para Modificar"
+msgstr "Permiso para modificar"
msgctxt "field:ir.rule.group,rec_name:"
msgid "Name"
@@ -1833,11 +1810,11 @@ msgstr "Usuarios"
msgctxt "field:ir.rule.group,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.rule.group,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.sequence,active:"
msgid "Active"
@@ -1845,15 +1822,15 @@ msgstr "Activo"
msgctxt "field:ir.sequence,code:"
msgid "Sequence Code"
-msgstr "Código de Secuencia"
+msgstr "Código de secuencia"
msgctxt "field:ir.sequence,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.sequence,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.sequence,id:"
msgid "ID"
@@ -1861,23 +1838,23 @@ msgstr "ID"
msgctxt "field:ir.sequence,last_timestamp:"
msgid "Last Timestamp"
-msgstr "Última Fecha-hora"
+msgstr "Última fecha-hora"
msgctxt "field:ir.sequence,name:"
msgid "Sequence Name"
-msgstr "Nombre de Secuencia"
+msgstr "Nombre de secuencia"
msgctxt "field:ir.sequence,number_increment:"
msgid "Increment Number"
-msgstr "Número de Incremento"
+msgstr "Número de incremento"
msgctxt "field:ir.sequence,number_next:"
msgid "Next Number"
-msgstr "Siguiente Número"
+msgstr "Siguiente número"
msgctxt "field:ir.sequence,number_next_internal:"
msgid "Next Number"
-msgstr "Siguiente Número"
+msgstr "Siguiente número"
msgctxt "field:ir.sequence,padding:"
msgid "Number padding"
@@ -1897,11 +1874,11 @@ msgstr "Sufijo"
msgctxt "field:ir.sequence,timestamp_offset:"
msgid "Timestamp Offset"
-msgstr "Desfase de Fecha-hora"
+msgstr "Desfase de fecha-hora"
msgctxt "field:ir.sequence,timestamp_rounding:"
msgid "Timestamp Rounding"
-msgstr "Redondeo de Fecha-hora"
+msgstr "Redondeo de fecha-hora"
msgctxt "field:ir.sequence,type:"
msgid "Type"
@@ -1909,11 +1886,11 @@ msgstr "Tipo"
msgctxt "field:ir.sequence,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.sequence,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.sequence.strict,active:"
msgid "Active"
@@ -1921,15 +1898,15 @@ msgstr "Activo"
msgctxt "field:ir.sequence.strict,code:"
msgid "Sequence Code"
-msgstr "Código de Secuencia"
+msgstr "Código de secuencia"
msgctxt "field:ir.sequence.strict,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.sequence.strict,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.sequence.strict,id:"
msgid "ID"
@@ -1937,23 +1914,23 @@ msgstr "ID"
msgctxt "field:ir.sequence.strict,last_timestamp:"
msgid "Last Timestamp"
-msgstr "Última Fecha-hora"
+msgstr "Última fecha-hora"
msgctxt "field:ir.sequence.strict,name:"
msgid "Sequence Name"
-msgstr "Nombre de Secuencia"
+msgstr "Nombre de secuencia"
msgctxt "field:ir.sequence.strict,number_increment:"
msgid "Increment Number"
-msgstr "Número de Incremento"
+msgstr "Número de incremento"
msgctxt "field:ir.sequence.strict,number_next:"
msgid "Next Number"
-msgstr "Siguiente Número"
+msgstr "Siguiente número"
msgctxt "field:ir.sequence.strict,number_next_internal:"
msgid "Next Number"
-msgstr "Siguiente Número"
+msgstr "Siguiente número"
msgctxt "field:ir.sequence.strict,padding:"
msgid "Number padding"
@@ -1973,11 +1950,11 @@ msgstr "Sufijo"
msgctxt "field:ir.sequence.strict,timestamp_offset:"
msgid "Timestamp Offset"
-msgstr "Desfase de Fecha-hora"
+msgstr "Desfase de fecha-hora"
msgctxt "field:ir.sequence.strict,timestamp_rounding:"
msgid "Timestamp Rounding"
-msgstr "Redondeo de Fecha-hora"
+msgstr "Redondeo de fecha-hora"
msgctxt "field:ir.sequence.strict,type:"
msgid "Type"
@@ -1985,23 +1962,23 @@ msgstr "Tipo"
msgctxt "field:ir.sequence.strict,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.sequence.strict,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.sequence.type,code:"
msgid "Sequence Code"
-msgstr "Código de Secuencia"
+msgstr "Código de la secuencia"
msgctxt "field:ir.sequence.type,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.sequence.type,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.sequence.type,id:"
msgid "ID"
@@ -2009,7 +1986,7 @@ msgstr "ID"
msgctxt "field:ir.sequence.type,name:"
msgid "Sequence Name"
-msgstr "Nombre de Secuencia"
+msgstr "Nombre de la secuencia"
msgctxt "field:ir.sequence.type,rec_name:"
msgid "Name"
@@ -2017,19 +1994,19 @@ msgstr "Nombre"
msgctxt "field:ir.sequence.type,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.sequence.type,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.session,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.session,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.session,id:"
msgid "ID"
@@ -2045,19 +2022,19 @@ msgstr "Nombre"
msgctxt "field:ir.session,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.session,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.session.wizard,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.session.wizard,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.session.wizard,data:"
msgid "Data"
@@ -2073,19 +2050,19 @@ msgstr "Nombre"
msgctxt "field:ir.session.wizard,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.session.wizard,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.translation,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.translation,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.translation,fuzzy:"
msgid "Fuzzy"
@@ -2109,11 +2086,11 @@ msgstr "Módulo"
msgctxt "field:ir.translation,name:"
msgid "Field Name"
-msgstr "Nombre de Campo"
+msgstr "Nombre del campo"
msgctxt "field:ir.translation,overriding_module:"
msgid "Overriding Module"
-msgstr "Sobreescritura del Módulo"
+msgstr "Sobreescritura del módulo"
msgctxt "field:ir.translation,rec_name:"
msgid "Name"
@@ -2137,15 +2114,15 @@ msgstr "Tipo"
msgctxt "field:ir.translation,value:"
msgid "Translation Value"
-msgstr "Valor de la Traducción"
+msgstr "Valor de la traducción"
msgctxt "field:ir.translation,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.translation,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.translation.clean.start,id:"
msgid "ID"
@@ -2197,7 +2174,7 @@ msgstr "Función de la Acción"
msgctxt "field:ir.trigger,action_model:"
msgid "Action Model"
-msgstr "Acción de Modelo"
+msgstr "Modelo de la Acción"
msgctxt "field:ir.trigger,active:"
msgid "Active"
@@ -2209,11 +2186,11 @@ msgstr "Condición"
msgctxt "field:ir.trigger,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.trigger,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.trigger,id:"
msgid "ID"
@@ -2221,11 +2198,11 @@ msgstr "ID"
msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
-msgstr "Número Límite"
+msgstr "Número límite"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
-msgstr "Retardo Mínimo"
+msgstr "Retardo mínimo"
msgctxt "field:ir.trigger,model:"
msgid "Model"
@@ -2237,19 +2214,19 @@ msgstr "Nombre"
msgctxt "field:ir.trigger,on_create:"
msgid "On Create"
-msgstr "Al Crearse"
+msgstr "Al crear"
msgctxt "field:ir.trigger,on_delete:"
msgid "On Delete"
-msgstr "Al Borrar"
+msgstr "Al Eliminar"
msgctxt "field:ir.trigger,on_time:"
msgid "On Time"
-msgstr "Al Tiempo"
+msgstr "A Tiempo"
msgctxt "field:ir.trigger,on_write:"
msgid "On Write"
-msgstr "Al Modificar"
+msgstr "Al modificar"
msgctxt "field:ir.trigger,rec_name:"
msgid "Name"
@@ -2257,19 +2234,19 @@ msgstr "Nombre"
msgctxt "field:ir.trigger,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.trigger,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.trigger.log,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.trigger.log,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.trigger.log,id:"
msgid "ID"
@@ -2289,19 +2266,19 @@ msgstr "Disparador"
msgctxt "field:ir.trigger.log,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.trigger.log,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.ui.icon,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.ui.icon,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.ui.icon,icon:"
msgid "Icon"
@@ -2333,11 +2310,11 @@ msgstr "Secuencia"
msgctxt "field:ir.ui.icon,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.ui.icon,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.ui.menu,action:"
msgid "Action"
@@ -2345,7 +2322,7 @@ msgstr "Acción"
msgctxt "field:ir.ui.menu,action_keywords:"
msgid "Action Keywords"
-msgstr "Teclas de Acción"
+msgstr "Acciones de teclado"
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
@@ -2357,15 +2334,15 @@ msgstr "Hijos"
msgctxt "field:ir.ui.menu,complete_name:"
msgid "Complete Name"
-msgstr "Nombre Completo"
+msgstr "Nombre completo"
msgctxt "field:ir.ui.menu,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.ui.menu,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.ui.menu,favorite:"
msgid "Favorite"
@@ -2401,19 +2378,19 @@ msgstr "Secuencia"
msgctxt "field:ir.ui.menu,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.ui.menu,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.ui.menu.favorite,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.ui.menu.favorite,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.ui.menu.favorite,id:"
msgid "ID"
@@ -2437,23 +2414,23 @@ msgstr "Usuario"
msgctxt "field:ir.ui.menu.favorite,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.ui.menu.favorite,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.ui.view,arch:"
msgid "View Architecture"
-msgstr "Ver Arquitectura"
+msgstr "Arquitectura de la vista"
msgctxt "field:ir.ui.view,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.ui.view,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.ui.view,data:"
msgid "Data"
@@ -2473,7 +2450,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"
@@ -2497,15 +2474,15 @@ 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"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.ui.view,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.ui.view.show.start,id:"
msgid "ID"
@@ -2513,11 +2490,11 @@ msgstr "ID"
msgctxt "field:ir.ui.view_search,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.ui.view_search,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.ui.view_search,domain:"
msgid "Domain"
@@ -2545,23 +2522,23 @@ msgstr "Usuario"
msgctxt "field:ir.ui.view_search,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.ui.view_search,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.ui.view_tree_state,child_name:"
msgid "Child Name"
-msgstr "Nombre del Hijo"
+msgstr "Nombre del hijo"
msgctxt "field:ir.ui.view_tree_state,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.ui.view_tree_state,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.ui.view_tree_state,domain:"
msgid "Domain"
@@ -2577,7 +2554,7 @@ msgstr "Modelo"
msgctxt "field:ir.ui.view_tree_state,nodes:"
msgid "Expanded Nodes"
-msgstr "Nodos Expandidos"
+msgstr "Nodos expandidos"
msgctxt "field:ir.ui.view_tree_state,rec_name:"
msgid "Name"
@@ -2585,7 +2562,7 @@ msgstr "Nombre"
msgctxt "field:ir.ui.view_tree_state,selected_nodes:"
msgid "Selected Nodes"
-msgstr "Nodos Seleccionados"
+msgstr "Nodos seleccionados"
msgctxt "field:ir.ui.view_tree_state,user:"
msgid "User"
@@ -2593,19 +2570,19 @@ msgstr "Usuario"
msgctxt "field:ir.ui.view_tree_state,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.ui.view_tree_state,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.ui.view_tree_width,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.ui.view_tree_width,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.ui.view_tree_width,field:"
msgid "Field"
@@ -2633,15 +2610,15 @@ msgstr "Ancho"
msgctxt "field:ir.ui.view_tree_width,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.ui.view_tree_width,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "help:ir.action.act_window,limit:"
msgid "Default limit for the list view"
-msgstr "Límite predeterminado para la vista de lista"
+msgstr "Límite por defecto para la vista de lista"
msgctxt "help:ir.action.act_window,search_value:"
msgid "Default search criteria for the list view"
@@ -2649,7 +2626,7 @@ msgstr "Criterio de búsqueda por defecto para la vista de lista"
msgctxt "help:ir.action.act_window,window_name:"
msgid "Use the action name as window name"
-msgstr "Usar el nombre de la acción como el nombre de la ventana"
+msgstr "Utilizar el nombre de la acción como el nombre de la ventana"
msgctxt "help:ir.action.report,email:"
msgid ""
@@ -2664,7 +2641,7 @@ msgid ""
"Leave empty for the same as template, see unoconv documentation for "
"compatible format"
msgstr ""
-"Dejar vacío para el mismo como plantilla, consulte la documentación de "
+"Dejar vacío para el mismo tipo como plantilla, consulte la documentación de "
"unoconv sobre formatos compatibles."
msgctxt "help:ir.action.report,style:"
@@ -2680,12 +2657,12 @@ msgid ""
"Number of times the function is called, a negative number indicates that the"
" function will always be called"
msgstr ""
-"Número de veces que se llama la función, un número negativo indica que la "
+"Número de veces que se llama a la función, un número negativo indica que la "
"función siempre se llamará"
msgctxt "help:ir.cron,request_user:"
msgid "The user who will receive requests in case of failure"
-msgstr "El usuario que recibirá peticiones en caso de error"
+msgstr "El usuario que recibirá solicitudes en caso de error"
msgctxt "help:ir.cron,user:"
msgid "The user used to execute this action"
@@ -2697,7 +2674,7 @@ msgstr "Etiqueta RFC 4646: http://tools.ietf.org/html/rfc4646"
msgctxt "help:ir.model,module:"
msgid "Module in which this model is defined"
-msgstr "El módulo en el que este modelo está definido"
+msgstr "Módulo en el que se define este modelo"
msgctxt "help:ir.model.data,db_id:"
msgid "The id of the record in the database."
@@ -2709,7 +2686,7 @@ msgstr "El id del registro tal y como se conoce en el sistema de archivos."
msgctxt "help:ir.model.field,module:"
msgid "Module in which this field is defined"
-msgstr "El módulo en el que este campo está definido"
+msgstr "Módulo en el que se define este campo"
msgctxt "help:ir.model.print_model_graph.start,filter:"
msgid ""
@@ -2720,8 +2697,12 @@ msgstr ""
"que concuerden."
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
-msgstr "El dominio es evaluado con el \"usuario\" como el usuario actual"
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
+msgstr ""
+"El dominio es evaluado con un contexto PYSON que contiene:\n"
+"- \"user\" como el usuario actual"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
@@ -2745,23 +2726,23 @@ msgid ""
"It triggers the action if true."
msgstr ""
"Una instrucción Python evaluada con el registro representado por \"self\"\n"
-"Se dispara la acción si es verdadera."
+"Dispara la acción si es verdadera."
msgctxt "help:ir.trigger,limit_number:"
msgid ""
"Limit the number of call to \"Action Function\" by records.\n"
"0 for no limit."
msgstr ""
-"Limitar el número de llamada a la \"Función de Acción\" por registros.\n"
+"Limitar el número de llamadas a la \"Función de la Acción\" por registros.\n"
"0 para ningún límite."
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Establecer un retraso mínimo en minutos entre llamadas a la \"Función de Acción\" para el mismo registro.\n"
-"0 para ningún retraso."
+"Definir un retraso mínimo entre llamadas a la \"Función de la Acción\" para el mismo registro.\n"
+"Vacío para ningún retraso."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -2773,7 +2754,7 @@ msgstr "Acción"
msgctxt "model:ir.action,name:act_action_act_window_form"
msgid "Window Actions"
-msgstr "Ventana de Acciones"
+msgstr "Acciones de ventana"
msgctxt "model:ir.action,name:act_action_form"
msgid "Actions"
@@ -2797,11 +2778,11 @@ msgstr "Adjuntos"
msgctxt "model:ir.action,name:act_config_wizard_item_form"
msgid "Config Wizard Items"
-msgstr "Elementos del Asistente de Configuración"
+msgstr "Elementos del asistente de configuración"
msgctxt "model:ir.action,name:act_cron_form"
msgid "Scheduled Actions"
-msgstr "Acciones Programadas"
+msgstr "Acciones programadas"
msgctxt "model:ir.action,name:act_export_form"
msgid "Exports"
@@ -2825,20 +2806,19 @@ msgstr "Menú"
msgctxt "model:ir.action,name:act_model_access_form"
msgid "Models Access"
-msgstr "Permisos de Modelos"
+msgstr "Permisos de accesso a modelos"
msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Botones"
-#, fuzzy
msgctxt "model:ir.action,name:act_model_data_form"
msgid "Data"
msgstr "Datos"
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
-msgstr "Permisos de los Campos"
+msgstr "Permisos de acceso a campos"
msgctxt "model:ir.action,name:act_model_fields_form"
msgid "Fields"
@@ -2850,11 +2830,11 @@ msgstr "Modelos"
msgctxt "model:ir.action,name:act_module_config"
msgid "Configure Modules"
-msgstr "Configuración de Módulos"
+msgstr "Configurar los módulos"
msgctxt "model:ir.action,name:act_module_config_wizard"
msgid "Module Configuration"
-msgstr "Configuración de Módulo"
+msgstr "Configuración del módulo"
msgctxt "model:ir.action,name:act_module_form"
msgid "Modules"
@@ -2862,7 +2842,7 @@ msgstr "Módulos"
msgctxt "model:ir.action,name:act_module_install_upgrade"
msgid "Perform Pending Installation/Upgrade"
-msgstr "Realizar Instalaciones/Actualizaciones Pendientes"
+msgstr "Realizar instalaciones/actualizaciones pendientes"
msgctxt "model:ir.action,name:act_property_form"
msgid "Properties"
@@ -2870,11 +2850,11 @@ msgstr "Propiedades"
msgctxt "model:ir.action,name:act_property_form_default"
msgid "Default Properties"
-msgstr "Propiedades Predeterminadas"
+msgstr "Propiedades por defecto"
msgctxt "model:ir.action,name:act_rule_group_form"
msgid "Record Rules"
-msgstr "Reglas de Registros"
+msgstr "Reglas de registros"
msgctxt "model:ir.action,name:act_sequence_form"
msgid "Sequences"
@@ -2882,19 +2862,19 @@ msgstr "Secuencias"
msgctxt "model:ir.action,name:act_sequence_strict_form"
msgid "Sequences Strict"
-msgstr "Secuencias Estrictas"
+msgstr "Secuencias estrictas"
msgctxt "model:ir.action,name:act_sequence_type_form"
msgid "Sequence Types"
-msgstr "Tipos de Secuencia"
+msgstr "Tipos de secuencia"
msgctxt "model:ir.action,name:act_translation_clean"
msgid "Clean Translations"
-msgstr "Limpiar Traducciones"
+msgstr "Limpiar traducciones"
msgctxt "model:ir.action,name:act_translation_export"
msgid "Export Translations"
-msgstr "Exportar Traducciones"
+msgstr "Exportar traducciones"
msgctxt "model:ir.action,name:act_translation_form"
msgid "Translations"
@@ -2902,11 +2882,11 @@ msgstr "Traducciones"
msgctxt "model:ir.action,name:act_translation_set"
msgid "Set Report Translations"
-msgstr "Establecer Traducciones de los Informes"
+msgstr "Definir traducciones de los informes"
msgctxt "model:ir.action,name:act_translation_update"
msgid "Synchronize Translations"
-msgstr "Sincronizar Traducciones"
+msgstr "Sincronizar traducciones"
msgctxt "model:ir.action,name:act_trigger_form"
msgid "Triggers"
@@ -2918,19 +2898,19 @@ msgstr "Vistas"
msgctxt "model:ir.action,name:act_view_search"
msgid "View Search"
-msgstr "Buscar Vista"
+msgstr "Búsqueda favorita"
msgctxt "model:ir.action,name:act_view_show"
msgid "Show View"
-msgstr "Mostar Vista"
+msgstr "Mostrar vista"
msgctxt "model:ir.action,name:act_view_tree_state"
msgid "Tree State"
-msgstr "Estado de Arbol"
+msgstr "Estado de árbol"
msgctxt "model:ir.action,name:act_view_tree_width_form"
msgid "View Tree Width"
-msgstr "Ancho de la Vista de Arbol"
+msgstr "Ancho de la vista de árbol"
msgctxt "model:ir.action,name:print_model_graph"
msgid "Graph"
@@ -2942,13 +2922,12 @@ msgstr "Gráfico"
msgctxt "model:ir.action.act_window,name:"
msgid "Action act window"
-msgstr "Acción ventana acción"
+msgstr "Acción de ventana"
msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
-msgstr "Accion en dominio de ventana de acción"
+msgstr "Dominio de acción de ventana"
-#, fuzzy
msgctxt ""
"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
msgid "All"
@@ -2957,23 +2936,23 @@ msgstr "Todo"
msgctxt ""
"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
msgid "Out of Sync"
-msgstr "Fuera de Sincronización"
+msgstr "Fuera de sincronización"
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
-msgstr "Acción en vista de ventana de acción"
+msgstr "Acción de vista de ventana"
msgctxt "model:ir.action.keyword,name:"
msgid "Action keyword"
-msgstr "Tecla de acción "
+msgstr "Acción de teclado"
msgctxt "model:ir.action.report,name:"
msgid "Action report"
-msgstr "Acción en Reporte"
+msgstr "Informe de acción"
msgctxt "model:ir.action.url,name:"
msgid "Action URL"
-msgstr "URL de la acción"
+msgstr "Acción de URL"
msgctxt "model:ir.action.wizard,name:"
msgid "Action wizard"
@@ -3073,7 +3052,7 @@ msgstr "Modelo"
msgctxt "model:ir.model.access,name:"
msgid "Model access"
-msgstr "Permisos de modelo"
+msgstr "Permisos de acceso a modelo"
msgctxt "model:ir.model.button,name:"
msgid "Model Button"
@@ -3089,11 +3068,11 @@ msgstr "Campo de modelo"
msgctxt "model:ir.model.field.access,name:"
msgid "Model Field Access"
-msgstr "Permiso Campo de Modelo"
+msgstr "Permisos de acceso a campo de modelo"
msgctxt "model:ir.model.print_model_graph.start,name:"
msgid "Print Model Graph"
-msgstr "Imprimir Gráfico del Modelo"
+msgstr "Imprimir gráfico del modelo"
msgctxt "model:ir.module.module,name:"
msgid "Module"
@@ -3101,11 +3080,11 @@ msgstr "Módulo"
msgctxt "model:ir.module.module.config_wizard.done,name:"
msgid "Module Config Wizard Done"
-msgstr "Asitente de Configuración del Módulo - Realizado"
+msgstr "Asitente de configuración del módulo - Realizado"
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
-msgstr "Asistente de Configuración del Módulo - Primero"
+msgstr "Asistente de configuración inicial del módulo"
msgctxt "model:ir.module.module.config_wizard.item,name:"
msgid "Config wizard to run after installing module"
@@ -3113,7 +3092,7 @@ msgstr "Asistente de configuración a ejecutar después de instalar un módulo"
msgctxt "model:ir.module.module.config_wizard.other,name:"
msgid "Module Config Wizard Other"
-msgstr "Asistente de Configuración del Módulo - Otro"
+msgstr "Asistente de configuración del módulo - Otro"
msgctxt "model:ir.module.module.dependency,name:"
msgid "Module dependency"
@@ -3121,11 +3100,11 @@ msgstr "Dependencias del módulo"
msgctxt "model:ir.module.module.install_upgrade.done,name:"
msgid "Module Install Upgrade Done"
-msgstr "Instalación / Actualización del Módulo - Realizada"
+msgstr "Instalación o actualización de módulos realizada"
msgctxt "model:ir.module.module.install_upgrade.start,name:"
msgid "Module Install Upgrade Start"
-msgstr "Instalación / Actualización del Módulo - Iniciar"
+msgstr "Iniciar la instalación o actualización de módulos"
msgctxt "model:ir.property,name:"
msgid "Property"
@@ -3145,7 +3124,7 @@ msgstr "Secuencia"
msgctxt "model:ir.sequence.strict,name:"
msgid "Sequence Strict"
-msgstr "Secuencia Estricta"
+msgstr "Secuencia estricta"
msgctxt "model:ir.sequence.type,name:"
msgid "Sequence type"
@@ -3157,7 +3136,7 @@ msgstr "Sesión"
msgctxt "model:ir.session.wizard,name:"
msgid "Session Wizard"
-msgstr "Asistente de Sesión"
+msgstr "Asistente de sesión"
msgctxt "model:ir.translation,name:"
msgid "Translation"
@@ -3181,11 +3160,11 @@ msgstr "Exportar traducción - archivo"
msgctxt "model:ir.translation.set.start,name:"
msgid "Set Translation"
-msgstr "Establecer Traducción"
+msgstr "Definir traducción"
msgctxt "model:ir.translation.set.succeed,name:"
msgid "Set Translation"
-msgstr "Establecer Traducción"
+msgstr "Definir traducción"
msgctxt "model:ir.translation.update.start,name:"
msgid "Update translation"
@@ -3197,7 +3176,7 @@ msgstr "Disparador"
msgctxt "model:ir.trigger.log,name:"
msgid "Trigger Log"
-msgstr "Registro del Disparador"
+msgstr "Registro del disparador"
msgctxt "model:ir.ui.icon,name:"
msgid "Icon"
@@ -3217,7 +3196,7 @@ msgstr "Acciones"
msgctxt "model:ir.ui.menu,name:menu_action_act_window"
msgid "Window Actions"
-msgstr "Ventana de Acciones"
+msgstr "Acciones de ventana"
msgctxt "model:ir.ui.menu,name:menu_action_report_form"
msgid "Reports"
@@ -3241,11 +3220,11 @@ msgstr "Adjuntos"
msgctxt "model:ir.ui.menu,name:menu_config_wizard_item_form"
msgid "Config Wizard Items"
-msgstr "Elementos del Asistente de Configuración"
+msgstr "Elementos del asistente 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"
@@ -3257,7 +3236,7 @@ msgstr "Iconos"
msgctxt "model:ir.ui.menu,name:menu_ir_sequence_type"
msgid "Sequence Types"
-msgstr "Tipos de Secuencia"
+msgstr "Tipos de secuencia"
msgctxt "model:ir.ui.menu,name:menu_lang_form"
msgid "Languages"
@@ -3273,20 +3252,19 @@ msgstr "Menú"
msgctxt "model:ir.ui.menu,name:menu_model_access_form"
msgid "Models Access"
-msgstr "Permisos de Modelos"
+msgstr "Permisos de acceso a modelos"
msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Botones"
-#, fuzzy
msgctxt "model:ir.ui.menu,name:menu_model_data_form"
msgid "Data"
msgstr "Datos"
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
-msgstr "Permisos de Campos"
+msgstr "Permisos de acceso a campos"
msgctxt "model:ir.ui.menu,name:menu_model_form"
msgid "Models"
@@ -3302,7 +3280,7 @@ msgstr "Módulos"
msgctxt "model:ir.ui.menu,name:menu_module_install_upgrade"
msgid "Perform Pending Installation/Upgrade"
-msgstr "Realizar Instalaciones/Actualizaciones Pendientes"
+msgstr "Realizar instalaciones/actualizaciones pendientes"
msgctxt "model:ir.ui.menu,name:menu_modules"
msgid "Modules"
@@ -3314,15 +3292,15 @@ msgstr "Propiedades"
msgctxt "model:ir.ui.menu,name:menu_property_form_default"
msgid "Default Properties"
-msgstr "Propiedades por Defecto"
+msgstr "Propiedades por defecto"
msgctxt "model:ir.ui.menu,name:menu_rule_group_form"
msgid "Record Rules"
-msgstr "Reglas de Registros"
+msgstr "Reglas de registros"
msgctxt "model:ir.ui.menu,name:menu_scheduler"
msgid "Scheduler"
-msgstr "Programador de Tareas"
+msgstr "Programador de tareas"
msgctxt "model:ir.ui.menu,name:menu_sequence_form"
msgid "Sequences"
@@ -3330,7 +3308,7 @@ msgstr "Secuencias"
msgctxt "model:ir.ui.menu,name:menu_sequence_strict_form"
msgid "Sequences Strict"
-msgstr "Secuencias Estrictas"
+msgstr "Secuencias estrictas"
msgctxt "model:ir.ui.menu,name:menu_sequences"
msgid "Sequences"
@@ -3342,7 +3320,7 @@ msgstr "Limpiar Traducciones"
msgctxt "model:ir.ui.menu,name:menu_translation_export"
msgid "Export Translations"
-msgstr "Exportar Traducciones"
+msgstr "Exportar traducciones"
msgctxt "model:ir.ui.menu,name:menu_translation_form"
msgid "Translations"
@@ -3350,11 +3328,11 @@ msgstr "Traducciones"
msgctxt "model:ir.ui.menu,name:menu_translation_set"
msgid "Set Translations"
-msgstr "Establecer Traducciones"
+msgstr "Definir traducciones"
msgctxt "model:ir.ui.menu,name:menu_translation_update"
msgid "Synchronize Translations"
-msgstr "Sincronizar Traducciones"
+msgstr "Sincronizar traducciones"
msgctxt "model:ir.ui.menu,name:menu_trigger_form"
msgid "Triggers"
@@ -3362,7 +3340,7 @@ msgstr "Disparadores"
msgctxt "model:ir.ui.menu,name:menu_ui"
msgid "User Interface"
-msgstr "Interfaz de Usuario"
+msgstr "Interfaz de usuario"
msgctxt "model:ir.ui.menu,name:menu_view"
msgid "Views"
@@ -3370,15 +3348,15 @@ msgstr "Vistas"
msgctxt "model:ir.ui.menu,name:menu_view_search"
msgid "View Search"
-msgstr "Buscar Vista"
+msgstr "Búsquedas favoritas"
msgctxt "model:ir.ui.menu,name:menu_view_tree_state"
msgid "Tree State"
-msgstr "Estado de Arbol"
+msgstr "Estado de árbol"
msgctxt "model:ir.ui.menu,name:menu_view_tree_width"
msgid "View Tree Width"
-msgstr "Ancho de la Vista de Arbol"
+msgstr "Ancho de la vista de árbol"
msgctxt "model:ir.ui.menu,name:model_model_fields_form"
msgid "Fields"
@@ -3386,7 +3364,7 @@ msgstr "Campos"
msgctxt "model:ir.ui.menu.favorite,name:"
msgid "Menu Favorite"
-msgstr "Menú Favorito"
+msgstr "Menú favorito"
msgctxt "model:ir.ui.view,name:"
msgid "View"
@@ -3398,23 +3376,23 @@ msgstr "Mostrar vista"
msgctxt "model:ir.ui.view_search,name:"
msgid "View Search"
-msgstr "Buscar Vista"
+msgstr "Búsqueda favorita"
msgctxt "model:ir.ui.view_tree_state,name:"
msgid "View Tree State"
-msgstr "Estado de Vista de Arbol"
+msgstr "Estado de vista de árbol"
msgctxt "model:ir.ui.view_tree_width,name:"
msgid "View Tree Width"
-msgstr "Ancho de la Vista de Arbol"
+msgstr "Ancho de la vista de árbol"
msgctxt "selection:ir.action.keyword,keyword:"
msgid "Action form"
-msgstr "Acción formulario"
+msgstr "Acción de formulario"
msgctxt "selection:ir.action.keyword,keyword:"
msgid "Action tree"
-msgstr "Árbol de acciones"
+msgstr "Acción de árbol"
msgctxt "selection:ir.action.keyword,keyword:"
msgid "Form relate"
@@ -3426,7 +3404,7 @@ msgstr "Abrir Gráfico"
msgctxt "selection:ir.action.keyword,keyword:"
msgid "Open tree"
-msgstr "Desplegar árbol"
+msgstr "Abrir árbol"
msgctxt "selection:ir.action.keyword,keyword:"
msgid "Print form"
@@ -3478,19 +3456,19 @@ msgstr "No instalado"
msgctxt "selection:ir.module.module,state:"
msgid "To be installed"
-msgstr "Para ser instalado"
+msgstr "Para instalar"
msgctxt "selection:ir.module.module,state:"
msgid "To be removed"
-msgstr "Para ser eliminado"
+msgstr "Para eliminar"
msgctxt "selection:ir.module.module,state:"
msgid "To be upgraded"
-msgstr "Para ser actualizado"
+msgstr "Para actualizar"
msgctxt "selection:ir.module.module.config_wizard.item,state:"
msgid "Done"
-msgstr "Terminado"
+msgstr "Realizado"
msgctxt "selection:ir.module.module.config_wizard.item,state:"
msgid "Open"
@@ -3506,15 +3484,15 @@ msgstr "No instalado"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "To be installed"
-msgstr "Para ser instalado"
+msgstr "Para instalar"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "To be removed"
-msgstr "Para ser eliminado"
+msgstr "Para eliminar"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "To be upgraded"
-msgstr "Para ser actualizado"
+msgstr "Para actualizar"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "Unknown"
@@ -3522,11 +3500,11 @@ msgstr "Desconocido"
msgctxt "selection:ir.sequence,type:"
msgid "Decimal Timestamp"
-msgstr "Fecha-hora Decimal"
+msgstr "Fecha-hora decimal"
msgctxt "selection:ir.sequence,type:"
msgid "Hexadecimal Timestamp"
-msgstr "Fecha-hora Hexadecimal"
+msgstr "Fecha-hora hexadecimal"
msgctxt "selection:ir.sequence,type:"
msgid "Incremental"
@@ -3534,11 +3512,11 @@ msgstr "Incremental"
msgctxt "selection:ir.sequence.strict,type:"
msgid "Decimal Timestamp"
-msgstr "Fecha-hora Decimal"
+msgstr "Fecha-hora decimal"
msgctxt "selection:ir.sequence.strict,type:"
msgid "Hexadecimal Timestamp"
-msgstr "Fecha-hora Hexadecimal"
+msgstr "Fecha-hora hexadecimal"
msgctxt "selection:ir.sequence.strict,type:"
msgid "Incremental"
@@ -3574,7 +3552,7 @@ msgstr "Vista"
msgctxt "selection:ir.translation,type:"
msgid "Wizard Button"
-msgstr "Botón de Asistente"
+msgstr "Botón del asistente"
msgctxt "selection:ir.ui.menu,action:"
msgid ""
@@ -3642,11 +3620,11 @@ 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"
@@ -3696,17 +3674,21 @@ msgctxt "view:ir.attachment:"
msgid "Attachments"
msgstr "Adjuntos"
+msgctxt "view:ir.attachment:"
+msgid "Last Modification Time"
+msgstr "Hora de última modificación"
+
msgctxt "view:ir.cron:"
msgid "Action to trigger"
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"
@@ -3714,7 +3696,7 @@ msgstr "Exportaciones"
msgctxt "view:ir.lang:"
msgid "Date Formatting"
-msgstr "Formato de Fecha"
+msgstr "Formato de fecha"
msgctxt "view:ir.lang:"
msgid "Language"
@@ -3726,7 +3708,7 @@ msgstr "Idiomas"
msgctxt "view:ir.lang:"
msgid "Numbers Formatting"
-msgstr "Formato de Números"
+msgstr "Formato de números"
msgctxt "view:ir.model.access:"
msgid "Access controls"
@@ -3742,7 +3724,7 @@ msgstr "Botones"
msgctxt "view:ir.model.data:"
msgid "Model Data"
-msgstr "Datos de Modelo"
+msgstr "Datos del modelo"
msgctxt "view:ir.model.data:"
msgid "Sync"
@@ -3750,7 +3732,7 @@ msgstr "Sincronizar"
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
-msgstr "Permiso de Campo"
+msgstr "Permisos de acceso a campo"
msgctxt "view:ir.model.field:"
msgid "Field"
@@ -3762,7 +3744,7 @@ msgstr "Campos"
msgctxt "view:ir.model.print_model_graph.start:"
msgid "Print Model Graph"
-msgstr "Imprimir Gráfico del Modelo"
+msgstr "Imprimir gráfico del modelo"
msgctxt "view:ir.model:"
msgid "Model Description"
@@ -3778,23 +3760,23 @@ msgstr "La configuracion se ha realizado."
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!"
+msgstr "¡Bienvenido al asistente de configuración del módulo!"
msgctxt "view:ir.module.module.config_wizard.first:"
msgid ""
"You will be able to configure your installation depending on the modules you"
" have installed."
msgstr ""
-"Usted será capaz de configurar su instalación dependiendo de los módulos que"
-" ha instalado."
+"Usted podrá configurar su instalación dependiendo de los módulos que ha "
+"instalado."
msgctxt "view:ir.module.module.config_wizard.item:"
msgid "Config Wizard Items"
-msgstr "Elementos del Asistente de Configuración"
+msgstr "Elementos del asistente de configuración"
msgctxt "view:ir.module.module.config_wizard.other:"
msgid "Configuration Wizard Next Step!"
-msgstr "¡Siguiente Paso del Asistente de Configuración!"
+msgstr "¡Siguiente paso del asistente de configuración!"
msgctxt "view:ir.module.module.dependency:"
msgid "Dependencies"
@@ -3806,15 +3788,15 @@ msgstr "Dependencia"
msgctxt "view:ir.module.module.install_upgrade.done:"
msgid "System Upgrade Done"
-msgstr "Actualización del Sistema Realizada"
+msgstr "Actualización del sistema realizada"
msgctxt "view:ir.module.module.install_upgrade.done:"
msgid "The modules have been upgraded / installed !"
-msgstr "¡Los módulos se han actualizado / instalado!"
+msgstr "¡Los módulos han sido actualizados/instalados!"
msgctxt "view:ir.module.module.install_upgrade.start:"
msgid "Note that this operation may take a few minutes."
-msgstr "Tenga en cuenta que esta operación puede demorar varios minutos."
+msgstr "Tenga en cuenta que esta operación puede tardar unos minutos."
msgctxt "view:ir.module.module.install_upgrade.start:"
msgid "System Upgrade"
@@ -3822,31 +3804,31 @@ msgstr "Actualización del Sistema"
msgctxt "view:ir.module.module.install_upgrade.start:"
msgid "Your system will be upgraded."
-msgstr "Su sistema sera actualizado."
+msgstr "Su sistema será actualizado."
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 Desinstalación"
+msgstr "Cancelar 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 Instalar"
+msgstr "Marcar para instalar"
msgctxt "view:ir.module.module:"
msgid "Mark for Uninstallation (beta)"
-msgstr "Marcar para Desinstalar (beta)"
+msgstr "Marcar para desinstalar (beta)"
msgctxt "view:ir.module.module:"
msgid "Mark for Upgrade"
-msgstr "Marcar para Actualizar"
+msgstr "Marcar para actualizar"
msgctxt "view:ir.module.module:"
msgid "Module"
@@ -3868,7 +3850,7 @@ msgctxt "view:ir.rule.group:"
msgid ""
"If there is no test defined, the rule is always satisfied if not global"
msgstr ""
-"Si no hay ninguna prueba definida, la regla se aplica siempre que no sea "
+"Si no hay ninguna prueba definida, la regla se aplica siempre si no es "
"global"
msgctxt "view:ir.rule.group:"
@@ -3877,47 +3859,15 @@ msgstr "Reglas de Registros"
msgctxt "view:ir.rule.group:"
msgid "The rule is satisfied if at least one test is True"
-msgstr "La regla se satisface si al menos una condición es verdadera"
+msgstr "La regla se satisface si al menos una condición es cierta"
msgctxt "view:ir.rule:"
msgid "Test"
msgstr "Prueba"
-msgctxt "view:ir.sequence.strict:"
-msgid "${day}"
-msgstr "${day}"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "${month}"
-msgstr "${month}"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "${year}"
-msgstr "${year}"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Day:"
-msgstr "Día:"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Legend (for prefix, suffix)"
-msgstr "Leyenda (para prefijo y/o sufijo)"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Month:"
-msgstr "Mes:"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Sequences Strict"
-msgstr "Secuencias Estrictas"
-
-msgctxt "view:ir.sequence.strict:"
-msgid "Year:"
-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}"
@@ -3961,15 +3911,15 @@ msgstr "Año:"
msgctxt "view:ir.translation.clean.start:"
msgid "Clean Translations"
-msgstr "Limpiar Traducciones"
+msgstr "Limpiar las traducciones"
msgctxt "view:ir.translation.clean.start:"
msgid "Clean Translations?"
-msgstr "¿Limpiar las Traducciones?"
+msgstr "¿Limpiar las traducciones?"
msgctxt "view:ir.translation.clean.succeed:"
msgid "Clean Translations"
-msgstr "Limpiar Traducciones"
+msgstr "Limpiar traducciones"
msgctxt "view:ir.translation.clean.succeed:"
msgid "Clean Translations Succeed!"
@@ -3977,31 +3927,31 @@ msgstr "¡Las traducciones se limpiaron correctamente!"
msgctxt "view:ir.translation.export.result:"
msgid "Export Translation"
-msgstr "Exportar Traducción"
+msgstr "Exportar traducción"
msgctxt "view:ir.translation.export.start:"
msgid "Export Translation"
-msgstr "Exportar Traducción"
+msgstr "Exportar traducción"
msgctxt "view:ir.translation.set.start:"
msgid "Set Translations"
-msgstr "Establecer Traducciones"
+msgstr "Definir traducciones"
msgctxt "view:ir.translation.set.start:"
msgid "Synchronize Translations?"
-msgstr "¿Sincronizar las Traducciones?"
+msgstr "¿Sincronizar las traducciones?"
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Succeed!"
-msgstr "¡Realizado Exitosamente!"
+msgstr "¡Las traducciones se han realizado correctamente!"
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Translations"
-msgstr "Establecer Traducciones"
+msgstr "Definir traducciones"
msgctxt "view:ir.translation.update.start:"
msgid "Synchronize Translations"
-msgstr "Sincronizar Traducciones"
+msgstr "Sincronizar traducciones"
msgctxt "view:ir.translation:"
msgid "Translations"
@@ -4025,11 +3975,11 @@ msgstr "Iconos"
msgctxt "view:ir.ui.menu.favorite:"
msgid "Menu Favorite"
-msgstr "Menú Favorito"
+msgstr "Menú favorito"
msgctxt "view:ir.ui.menu.favorite:"
msgid "Menu Favorites"
-msgstr "Menu Favoritos"
+msgstr "Menu favoritos"
msgctxt "view:ir.ui.menu:"
msgid "Menu"
@@ -4045,27 +3995,27 @@ msgstr "_Mostrar"
msgctxt "view:ir.ui.view_search:"
msgid "View Search"
-msgstr "Buscar Vista"
+msgstr "Búsqueda favorita"
msgctxt "view:ir.ui.view_search:"
msgid "View Searches"
-msgstr "Vista Busquedas"
+msgstr "Búsquedas favoritas"
msgctxt "view:ir.ui.view_tree_state:"
msgid "View Tree State"
-msgstr "Estado de Vista de Arbol"
+msgstr "Estado de vista de árbol"
msgctxt "view:ir.ui.view_tree_state:"
msgid "Views Tree State"
-msgstr "Estado de la Vista de Arbol"
+msgstr "Estado de la vista de árbol"
msgctxt "view:ir.ui.view_tree_width:"
msgid "View Tree Width"
-msgstr "Ancho de la Vista de Arbol"
+msgstr "Ancho de la vista de árbol"
msgctxt "view:ir.ui.view_tree_width:"
msgid "Views Tree Width"
-msgstr "Ancho de la Vista de Arbol"
+msgstr "Ancho de la vista de árbol"
msgctxt "wizard_button:ir.model.print_model_graph,start,end:"
msgid "Cancel"
@@ -4076,12 +4026,12 @@ msgid "Print"
msgstr "Imprimir"
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
-msgstr "Aceptar"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
-msgstr "Aceptar"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
msgid "Cancel"
@@ -4096,8 +4046,8 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
-msgstr "Aceptar"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
msgid "Cancel"
@@ -4105,7 +4055,7 @@ msgstr "Cancelar"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,upgrade:"
msgid "Start Upgrade"
-msgstr "Iniciar Actualización"
+msgstr "Iniciar actualización"
msgctxt "wizard_button:ir.translation.clean,start,clean:"
msgid "Clean"
@@ -4116,8 +4066,8 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
-msgstr "Aceptar"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.translation.export,result,end:"
msgid "Close"
@@ -4137,11 +4087,11 @@ msgstr "Cancelar"
msgctxt "wizard_button:ir.translation.set,start,set_:"
msgid "Set"
-msgstr "Establecer"
+msgstr "Definir"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
-msgstr "Aceptar"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.translation.update,start,end:"
msgid "Cancel"
diff --git a/trytond/ir/locale/es_ES.po b/trytond/ir/locale/es_ES.po
index 987d042..2d297c8 100644
--- a/trytond/ir/locale/es_ES.po
+++ b/trytond/ir/locale/es_ES.po
@@ -27,8 +27,8 @@ msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
"exceeds its limit."
msgstr ""
-"El número de dígitos \"%(digits)s\" del campo \"%(field)s\" de \"%(value)s\""
-" es superior a su límite."
+"El número de decimales \"%(digits)s\" del campo \"%(field)s\" de "
+"\"%(value)s\" es superior a su límite."
msgctxt "error:domain_validation_record:"
msgid ""
@@ -50,6 +50,12 @@ msgctxt "error:foreign_model_missing:"
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.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+"El dominio o el criterio de búsqueda \"%(domain)s\" en la acción "
+"\"%(action)s\" no es correcto."
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr "El contexto \"%(context)s\" en la acción \"%(action)s\" no es correcto."
@@ -66,17 +72,13 @@ msgstr "La vista \"%(view)s\" para la acción \"%(action)s\" no es correcto."
msgctxt "error:ir.action.keyword:"
msgid "Wrong wizard model in keyword action \"%s\"."
-msgstr "Asistente incorrecto en la acción de teclado \"%s\"."
+msgstr "El modelo de asistente es incorrecto en la acción de teclado \"%s\"."
msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
msgstr ""
"La definición de correo electrónico sobre el informe \"%s\" no es correcta."
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "El nombre interno debe ser único por módulo."
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr "El nombre de los adjuntos debe ser único por registro."
@@ -1267,7 +1269,7 @@ msgstr "Información"
msgctxt "field:ir.model,model:"
msgid "Model Name"
-msgstr "Nombre del modelo"
+msgstr "Modelo"
msgctxt "field:ir.model,module:"
msgid "Module"
@@ -2221,9 +2223,9 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Número límite"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
-msgstr "Margen mínimo"
+msgstr "Retraso mínimo"
msgctxt "field:ir.trigger,model:"
msgid "Model"
@@ -2662,8 +2664,8 @@ msgid ""
"Leave empty for the same as template, see unoconv documentation for "
"compatible format"
msgstr ""
-"Dejarlo vacío para el mismo tipo que la plantilla. Ver la documentación "
-"unoconv para formatos compatibles."
+"Dejarlo vacío para el mismo tipo que la plantilla. Consultar la "
+"documentación unoconv para conocer formatos compatibles."
msgctxt "help:ir.action.report,style:"
msgid "Define the style to apply on the report."
@@ -2718,8 +2720,12 @@ msgstr ""
"concuerden."
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
-msgstr "El dominio es evaluado con el usuario \"user\" que es el usuario actual."
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
+msgstr ""
+"El dominio se evalua con PYSON dónde el contexto contiene:\n"
+"- \"user\" es el usuario actual"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
@@ -2753,13 +2759,13 @@ msgstr ""
"Número límite de llamadas a la \"Función de la acción\".\n"
"0 para no límite."
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Margen mínimo en minutos entre las llamadas a la \"Función de la acción\" para el mismo registro.\n"
-"0 para no dejar margen."
+"Indica un tiempo mínimo de retraso entre llamadas a la \"Función de la acción\" para el mismo registro.\n"
+"Dejarlo vacío si no hay retraso."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -3097,11 +3103,11 @@ 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"
+msgstr "Finalización asistente de configuración de módulos"
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
-msgstr "Inicio asistente de configuración del módulo"
+msgstr "Asistente de configuración del módulo - Primero"
msgctxt "model:ir.module.module.config_wizard.item,name:"
msgid "Config wizard to run after installing module"
@@ -3117,7 +3123,7 @@ msgstr "Dependencias del módulo"
msgctxt "model:ir.module.module.install_upgrade.done,name:"
msgid "Module Install Upgrade Done"
-msgstr "Finalizar la instalación o actualización de módulos"
+msgstr "Finalización instalación o actualización de módulos"
msgctxt "model:ir.module.module.install_upgrade.start,name:"
msgid "Module Install Upgrade Start"
@@ -3691,6 +3697,10 @@ msgctxt "view:ir.attachment:"
msgid "Attachments"
msgstr "Adjuntos"
+msgctxt "view:ir.attachment:"
+msgid "Last Modification Time"
+msgstr "Hora última modificación"
+
msgctxt "view:ir.cron:"
msgid "Action to trigger"
msgstr "Acción a disparar"
@@ -3935,7 +3945,7 @@ msgstr "Limpiar traducciones"
msgctxt "view:ir.translation.clean.succeed:"
msgid "Clean Translations Succeed!"
-msgstr "Las traducciones se han limpiado correctamente."
+msgstr "La limpieza de las traducciones se ha realizado correctamente."
msgctxt "view:ir.translation.export.result:"
msgid "Export Translation"
@@ -4038,11 +4048,11 @@ msgid "Print"
msgstr "Imprimir"
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -4058,7 +4068,7 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
@@ -4078,7 +4088,7 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.translation.export,result,end:"
@@ -4102,7 +4112,7 @@ msgid "Set"
msgstr "Definir"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:ir.translation.update,start,end:"
diff --git a/trytond/ir/locale/fr_FR.po b/trytond/ir/locale/fr_FR.po
index 44e6984..844d47d 100644
--- a/trytond/ir/locale/fr_FR.po
+++ b/trytond/ir/locale/fr_FR.po
@@ -52,6 +52,12 @@ msgstr ""
"La valeur « %(value)s » du champ « %(field)s » sur « %(model)s » n'existe "
"pas."
+msgctxt "error:ir.action.act_window.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+"Le domaine ou critère de recherche « %(domain)s » de l'action « %(action)s »"
+" est invalide."
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr "Le contexte « %(context)s » de l'action « %(action)s » est invalide."
@@ -74,10 +80,6 @@ msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
msgstr "Définition de mail incorrecte sur le rapport « %s »."
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "Le nom interne doit être unique par module !"
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr ""
@@ -2236,9 +2238,9 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Nombre limite"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
-msgstr "Délais minimal"
+msgstr "Délai minimal"
msgctxt "field:ir.trigger,model:"
msgid "Model"
@@ -2682,7 +2684,7 @@ msgstr ""
msgctxt "help:ir.action.report,style:"
msgid "Define the style to apply on the report."
-msgstr "Définit le style à appliquer sur le rapport."
+msgstr "Défini le style à appliquer sur le rapport."
msgctxt "help:ir.action.wizard,window:"
msgid "Run wizard in a new window"
@@ -2733,8 +2735,12 @@ msgstr ""
"graphe"
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
-msgstr "Le domaine est évalué avec « user » comme utilisateur courant"
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
+msgstr ""
+"Le domaine est évalué avec un context PYSON contenant:\n"
+"- « user » comme utilisateur courant"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
@@ -2767,13 +2773,13 @@ msgstr ""
"Limite le nombre d'appel aux « Fonction action » par enregistrement.\n"
"0 signifie pas de limite."
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Défini un délais minimum en minutes entre les appels aux « Fonctions actions » sur un même modèle.\n"
-"0 signifie pas de délais."
+"Défini un delai minimal entre l'appel à la « fonction de l'action » pour le même enregistrement.\n"
+"Vide pour aucun délai."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -3705,6 +3711,10 @@ msgctxt "view:ir.attachment:"
msgid "Attachments"
msgstr "Pièces jointes"
+msgctxt "view:ir.attachment:"
+msgid "Last Modification Time"
+msgstr "Heure de dernière modification"
+
msgctxt "view:ir.cron:"
msgid "Action to trigger"
msgstr "Action à déclencher"
@@ -4085,12 +4095,12 @@ msgid "Print"
msgstr "Imprimer"
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
-msgstr "Ok"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
-msgstr "Ok"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
msgid "Cancel"
@@ -4105,8 +4115,8 @@ msgid "Cancel"
msgstr "Annuler"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
-msgstr "Ok"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
msgid "Cancel"
@@ -4125,8 +4135,8 @@ msgid "Cancel"
msgstr "Annuler"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
-msgstr "Ok"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.translation.export,result,end:"
msgid "Close"
@@ -4149,8 +4159,8 @@ msgid "Set"
msgstr "Mise à jour"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
-msgstr "Ok"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:ir.translation.update,start,end:"
msgid "Cancel"
diff --git a/trytond/ir/locale/nl_NL.po b/trytond/ir/locale/nl_NL.po
index a34a383..f3a0e8c 100644
--- a/trytond/ir/locale/nl_NL.po
+++ b/trytond/ir/locale/nl_NL.po
@@ -42,6 +42,10 @@ msgctxt "error:foreign_model_missing:"
msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
msgstr ""
+msgctxt "error:ir.action.act_window.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr ""
@@ -62,10 +66,6 @@ msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
msgstr ""
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "De interne naam moet uniek zijn per module!"
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr "De namen van de bijlagen moeten uniek zijn per bron!"
@@ -2251,9 +2251,9 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Begrenzing nummer"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
-msgstr "Minimum vertraging"
+msgstr ""
msgctxt "field:ir.trigger,model:"
msgid "Model"
@@ -2759,7 +2759,9 @@ msgid ""
msgstr ""
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
msgstr ""
msgctxt "help:ir.rule.group,default_p:"
@@ -2792,14 +2794,11 @@ msgstr ""
"Beperk het aantal aanroepen van \"Uitvoerende functie\" door items.\n"
"Gebruik 0 voor geen beperking."
-#, fuzzy
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Stelt de minimum vertraging in tussen afzonderlijke aanroepen van \"Uitvoerende functie\" door hetzelfde item.\n"
-"Gebruik 0 voor geen vertraging."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -4100,12 +4099,12 @@ msgstr ""
#, fuzzy
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Oké"
#, fuzzy
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr "Oké"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -4123,7 +4122,7 @@ msgstr "Annuleren"
#, fuzzy
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr "Oké"
#, fuzzy
@@ -4146,7 +4145,7 @@ msgstr "Annuleren"
#, fuzzy
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Oké"
#, fuzzy
@@ -4175,7 +4174,7 @@ msgstr ""
#, fuzzy
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Oké"
#, fuzzy
diff --git a/trytond/ir/locale/ru_RU.po b/trytond/ir/locale/ru_RU.po
index 687f877..372924d 100644
--- a/trytond/ir/locale/ru_RU.po
+++ b/trytond/ir/locale/ru_RU.po
@@ -44,6 +44,13 @@ msgctxt "error:foreign_model_missing:"
msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
msgstr ""
+#, fuzzy
+msgctxt "error:ir.action.act_window.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+"Некорректный домен или условия поиска \"%(domain)s\" у действия "
+"\"%(action)s\"."
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr "Некорректный контекст \"%(context)s\" у действия \"%(action)s\"."
@@ -66,10 +73,6 @@ msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
msgstr "Некорректная эл.почта у отчета \"%s\"."
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "Наименование модуля должно быть уникальным!"
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr "Имена вложений должны быть уникальны для ресурса!"
@@ -2200,9 +2203,9 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Номер лимита"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
-msgstr "Минимальная задержка"
+msgstr ""
msgctxt "field:ir.trigger,model:"
msgid "Model"
@@ -2705,8 +2708,11 @@ msgstr ""
"Ввод регулярного выражения Python исключит соответствующие модели из "
"диаграммы."
+#, fuzzy
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
msgstr ""
"Домен расчитывается для пользователя \"user\" как для текущего пользователя"
@@ -2741,13 +2747,11 @@ msgstr ""
"Ограничение количества вызовов \"Действие функции\" от записей. \"0\" - нет "
"ограничений."
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Задайте минимальную задержку в минутах между вызовом \"Действие функции\" для одинаковых записей.\n"
-"\"0\" для отсутствия задержки."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -4033,11 +4037,11 @@ msgstr "Печать"
#, fuzzy
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Ок"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr "Ок"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -4053,7 +4057,7 @@ msgid "Cancel"
msgstr "Отменить"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr "Ок"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
@@ -4073,7 +4077,7 @@ msgid "Cancel"
msgstr "Отменить"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Ок"
msgctxt "wizard_button:ir.translation.export,result,end:"
@@ -4097,7 +4101,7 @@ msgid "Set"
msgstr "Применить"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "Ок"
msgctxt "wizard_button:ir.translation.update,start,end:"
diff --git a/trytond/ir/locale/sl_SI.po b/trytond/ir/locale/sl_SI.po
index 8b73500..ff1f95b 100644
--- a/trytond/ir/locale/sl_SI.po
+++ b/trytond/ir/locale/sl_SI.po
@@ -50,6 +50,12 @@ msgctxt "error:foreign_model_missing:"
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.domain:"
+msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
+msgstr ""
+"Neveljavna domena ali iskalni kriterij \"%(domain)s\" pri ukrepu "
+"\"%(action)s\"."
+
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
msgstr "Neveljaven kontekst \"%(context)s\" pri ukrepu \"%(action)s\"."
@@ -72,10 +78,6 @@ msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
msgstr "Neveljavna definicija epošte na izpisu \"%s\"."
-msgctxt "error:ir.action.report:"
-msgid "The internal name must be unique by module!"
-msgstr "Interni naziv mora biti enoličen med moduli."
-
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr "Imena priponk morajo biti unikatna med resursi."
@@ -401,7 +403,7 @@ msgstr "Ime"
msgctxt "field:ir.action,type:"
msgid "Type"
-msgstr "Vrsta"
+msgstr "Tip"
msgctxt "field:ir.action,usage:"
msgid "Usage"
@@ -961,7 +963,7 @@ msgstr "Povzetek"
msgctxt "field:ir.attachment,type:"
msgid "Type"
-msgstr "Vrsta"
+msgstr "Tip"
msgctxt "field:ir.attachment,write_date:"
msgid "Write Date"
@@ -1897,7 +1899,7 @@ msgstr "Zaokroževanje časovnega žiga"
msgctxt "field:ir.sequence,type:"
msgid "Type"
-msgstr "Vrsta"
+msgstr "Tip"
msgctxt "field:ir.sequence,write_date:"
msgid "Write Date"
@@ -1973,7 +1975,7 @@ msgstr "Zaokroževanje časovnega žiga"
msgctxt "field:ir.sequence.strict,type:"
msgid "Type"
-msgstr "Vrsta"
+msgstr "Tip"
msgctxt "field:ir.sequence.strict,write_date:"
msgid "Write Date"
@@ -2125,7 +2127,7 @@ msgstr "MD5 izvora"
msgctxt "field:ir.translation,type:"
msgid "Type"
-msgstr "Vrsta"
+msgstr "Tip"
msgctxt "field:ir.translation,value:"
msgid "Translation Value"
@@ -2215,7 +2217,7 @@ msgctxt "field:ir.trigger,limit_number:"
msgid "Limit Number"
msgstr "Število klicev"
-msgctxt "field:ir.trigger,minimum_delay:"
+msgctxt "field:ir.trigger,minimum_time_delay:"
msgid "Minimum Delay"
msgstr "Minimalna zakasnitev"
@@ -2710,8 +2712,12 @@ msgid ""
msgstr "Vpis Python regularnega izraza izloči najdene modele iz grafa."
msgctxt "help:ir.rule,domain:"
-msgid "Domain is evaluated with \"user\" as the current user"
-msgstr "Domena ima vrednost \"user\" za trenutnega uporabnika "
+msgid ""
+"Domain is evaluated with a PYSON context containing:\n"
+"- \"user\" as the current user"
+msgstr ""
+"Domena je ovrednotena s PYSON kontekstom, ki vsebuje:\n"
+"- \"user\" kot trenutni uporabnik"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
@@ -2745,13 +2751,13 @@ msgstr ""
"Omeji število klicev \"funkcije ukrepa\" po posameznem zapisu.\n"
"0 za neomejeno število."
-msgctxt "help:ir.trigger,minimum_delay:"
+msgctxt "help:ir.trigger,minimum_time_delay:"
msgid ""
-"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
-"0 for no delay."
+"Set a minimum time delay between call to \"Action Function\" for the same record.\n"
+"empty for no delay."
msgstr ""
-"Nastavi minimalno zakasnitev v minutah med klicema \"funkcije ukrepa\" istega zapisa.\n"
-"0 za brez zakasnitve."
+"Nastavi minimalno časovno zakasnitev za klic \"Funkcija ukrepa\" pri istem zapisu.\n"
+"Pusti prazno za brez zakasnitve."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -3683,6 +3689,10 @@ msgctxt "view:ir.attachment:"
msgid "Attachments"
msgstr "Priloge"
+msgctxt "view:ir.attachment:"
+msgid "Last Modification Time"
+msgstr "Zadnja sprememba"
+
msgctxt "view:ir.cron:"
msgid "Action to trigger"
msgstr "Sprožitev ukrepa"
@@ -4027,11 +4037,11 @@ msgid "Print"
msgstr "Natisni"
msgctxt "wizard_button:ir.module.module.config_wizard,done,end:"
-msgid "Ok"
+msgid "OK"
msgstr "V redu"
msgctxt "wizard_button:ir.module.module.config_wizard,first,action:"
-msgid "Ok"
+msgid "OK"
msgstr "V redu"
msgctxt "wizard_button:ir.module.module.config_wizard,first,end:"
@@ -4047,7 +4057,7 @@ msgid "Cancel"
msgstr "Prekliči"
msgctxt "wizard_button:ir.module.module.install_upgrade,done,config:"
-msgid "Ok"
+msgid "OK"
msgstr "V redu"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,end:"
@@ -4067,7 +4077,7 @@ msgid "Cancel"
msgstr "Prekliči"
msgctxt "wizard_button:ir.translation.clean,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "V redu"
msgctxt "wizard_button:ir.translation.export,result,end:"
@@ -4091,7 +4101,7 @@ msgid "Set"
msgstr "Nastavi"
msgctxt "wizard_button:ir.translation.set,succeed,end:"
-msgid "Ok"
+msgid "OK"
msgstr "V redu"
msgctxt "wizard_button:ir.translation.update,start,end:"
diff --git a/trytond/ir/model.py b/trytond/ir/model.py
index 2217dc8..c4152b2 100644
--- a/trytond/ir/model.py
+++ b/trytond/ir/model.py
@@ -1,7 +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.
+# 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 re
import heapq
+
+from sql import Null
from sql.aggregate import Max
from sql.conditionals import Case
from collections import defaultdict
@@ -20,6 +22,7 @@ from ..pyson import Bool, Eval
from ..rpc import RPC
from .. import backend
from ..protocols.jsonrpc import JSONDecoder, JSONEncoder
+from ..tools import is_instance_method
try:
from ..tools.StringMatcher import StringMatcher
except ImportError:
@@ -168,7 +171,7 @@ class Model(ModelSQL, ModelView):
def global_search(cls, text, limit, menu='ir.ui.menu'):
"""
Search on models for text including menu
- Returns a list of tuple (ratio, model, model_name, id, rec_name, icon)
+ Returns a list of tuple (ratio, model, model_name, id, name, icon)
The size of the list is limited to limit
"""
pool = Pool()
@@ -194,12 +197,12 @@ class Model(ModelSQL, ModelView):
Model = pool.get(model.model)
if not hasattr(Model, 'search_global'):
continue
- for id_, rec_name, icon in Model.search_global(text):
- if isinstance(rec_name, str):
- rec_name = rec_name.decode('utf-8')
- s.set_seq1(rec_name)
+ for record, name, icon in Model.search_global(text):
+ if isinstance(name, str):
+ name = name.decode('utf-8')
+ s.set_seq1(name)
yield (s.ratio(), model.model, model.rec_name,
- id_, rec_name, icon)
+ record.id, name, icon)
return heapq.nlargest(int(limit), generate())
@@ -309,7 +312,7 @@ class ModelField(ModelSQL, ModelView):
for field_name in model_fields:
if model_fields[field_name]['module'] == module_name \
and field_name not in model._fields:
- #XXX This delete field even when it is defined later
+ # XXX This delete field even when it is defined later
# in the module
cursor.execute(*ir_model_field.delete(
where=ir_model_field.id ==
@@ -511,7 +514,7 @@ class ModelAccess(ModelSQL, ModelView):
Max(Case((model_access.perm_create, 1), else_=0)),
Max(Case((model_access.perm_delete, 1), else_=0)),
where=ir_model.model.in_(models)
- & ((user_group.user == user) | (model_access.group == None)),
+ & ((user_group.user == user) | (model_access.group == Null)),
group_by=ir_model.model))
access.update(dict(
(m, {'read': r, 'write': w, 'create': c, 'delete': d})
@@ -562,8 +565,7 @@ class ModelAccess(ModelSQL, ModelView):
selection = field.selection
if isinstance(selection, basestring):
sel_func = getattr(Model, field.selection)
- if (not hasattr(sel_func, 'im_self')
- or sel_func.im_self):
+ if not is_instance_method(Model, field.selection):
selection = sel_func()
else:
# XXX Can not check access right on instance method
@@ -697,7 +699,7 @@ class ModelFieldAccess(ModelSQL, ModelView):
Max(Case((field_access.perm_create, 1), else_=0)),
Max(Case((field_access.perm_delete, 1), else_=0)),
where=ir_model.model.in_(models)
- & ((user_group.user == user) | (field_access.group == None)),
+ & ((user_group.user == user) | (field_access.group == Null)),
group_by=[ir_model.model, model_field.name]))
for m, f, r, w, c, d in cursor.fetchall():
accesses[m][f] = {'read': r, 'write': w, 'create': c, 'delete': d}
@@ -876,7 +878,7 @@ class ModelData(ModelSQL, ModelView):
Operator = fields.SQL_OPERATORS[operator]
query = table.select(table.id,
where=Operator(
- (table.fs_values != table.values) & (table.fs_values != None),
+ (table.fs_values != table.values) & (table.fs_values != Null),
value))
return [('id', 'in', query)]
@@ -916,10 +918,9 @@ class ModelData(ModelSQL, ModelView):
return dict(json.loads(values, object_hook=JSONDecoder()))
except ValueError:
# Migration from 3.2
- from ..tools import safe_eval
from decimal import Decimal
import datetime
- return safe_eval(values, {
+ return eval(values, {
'Decimal': Decimal,
'datetime': datetime,
})
@@ -1008,7 +1009,7 @@ class ModelGraph(Report):
graph.set('ratio', 'auto')
cls.fill_graph(models, graph, level=data['level'], filter=filter)
data = graph.create(prog='dot', format='png')
- return ('png', buffer(data), False, action_report.name)
+ return ('png', fields.Binary.cast(data), False, action_report.name)
@classmethod
def fill_graph(cls, models, graph, level=1, filter=None):
diff --git a/trytond/ir/model.xml b/trytond/ir/model.xml
index 9c5641a..ae4d69d 100644
--- a/trytond/ir/model.xml
+++ b/trytond/ir/model.xml
@@ -204,7 +204,7 @@ this repository contains the full copyright notices and license terms. -->
id="act_model_data_form_domain_out_of_sync">
<field name="name">Out of Sync</field>
<field name="sequence" eval="10"/>
- <field name="domain">[('out_of_sync', '=', True)]</field>
+ <field name="domain" eval="[('out_of_sync', '=', True)]" pyson="1"/>
<field name="act_window" ref="act_model_data_form"/>
</record>
<record model="ir.action.act_window.domain"
diff --git a/trytond/ir/module/__init__.py b/trytond/ir/module/__init__.py
index 9a8fa1a..98704bf 100644
--- a/trytond/ir/module/__init__.py
+++ b/trytond/ir/module/__init__.py
@@ -1,3 +1,3 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 .module import *
diff --git a/trytond/ir/module/module.py b/trytond/ir/module/module.py
index 8153bde..1dca76d 100644
--- a/trytond/ir/module/module.py
+++ b/trytond/ir/module/module.py
@@ -1,5 +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.
+# 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
@@ -9,7 +9,7 @@ from trytond.wizard import Wizard, StateView, Button, StateTransition, \
from trytond import backend
from trytond.pool import Pool
from trytond.transaction import Transaction
-from trytond.pyson import Eval
+from trytond.pyson import Eval, If
from trytond.rpc import RPC
__all__ = [
@@ -121,6 +121,15 @@ class Module(ModelSQL, ModelView):
return child_ids
@classmethod
+ def view_attributes(cls):
+ return [('/tree', 'colors',
+ If(Eval('state').in_(['to upgrade', 'to install']),
+ 'blue',
+ If(Eval('state') == 'uninstalled',
+ 'grey',
+ 'black')))]
+
+ @classmethod
def delete(cls, records):
for module in records:
if module.state in (
@@ -423,7 +432,7 @@ class ModuleConfigWizard(Wizard):
first = StateView('ir.module.module.config_wizard.first',
'ir.module_config_wizard_first_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
- Button('Ok', 'action', 'tryton-ok', default=True),
+ Button('OK', 'action', 'tryton-ok', default=True),
])
other = StateView('ir.module.module.config_wizard.other',
'ir.module_config_wizard_other_view_form', [
@@ -433,7 +442,7 @@ class ModuleConfigWizard(Wizard):
action = ConfigStateAction()
done = StateView('ir.module.module.config_wizard.done',
'ir.module_config_wizard_done_view_form', [
- Button('Ok', 'end', 'tryton-close', default=True),
+ Button('OK', 'end', 'tryton-close', default=True),
])
def transition_start(self):
@@ -479,7 +488,7 @@ class ModuleInstallUpgrade(Wizard):
upgrade = StateTransition()
done = StateView('ir.module.module.install_upgrade.done',
'ir.module_install_upgrade_done_view_form', [
- Button('Ok', 'config', 'tryton-ok', default=True),
+ Button('OK', 'config', 'tryton-ok', default=True),
])
config = StateAction('ir.act_module_config_wizard')
diff --git a/trytond/ir/module/module.xml b/trytond/ir/module/module.xml
index 8dc897f..530fc8d 100644
--- a/trytond/ir/module/module.xml
+++ b/trytond/ir/module/module.xml
@@ -18,7 +18,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">Modules</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.module.module</field>
- <field name="domain">[('name', '!=', 'tests')]</field>
+ <field name="domain" eval="[('name', '!=', 'tests')]" pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_module_form_view1">
diff --git a/trytond/ir/property.py b/trytond/ir/property.py
index f5c79ff..2e5db5d 100644
--- a/trytond/ir/property.py
+++ b/trytond/ir/property.py
@@ -1,5 +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.
+# 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 ..model import ModelView, ModelSQL, fields
from ..transaction import Transaction
diff --git a/trytond/ir/property.xml b/trytond/ir/property.xml
index a7e1718..2ea792e 100644
--- a/trytond/ir/property.xml
+++ b/trytond/ir/property.xml
@@ -17,7 +17,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">Default Properties</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.property</field>
- <field name="domain">[('res', '=', None)]</field>
+ <field name="domain" eval="[('res', '=', None)]" pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_property_form_view1_default">
diff --git a/trytond/ir/rule.py b/trytond/ir/rule.py
index 0c60fe7..351b577 100644
--- a/trytond/ir/rule.py
+++ b/trytond/ir/rule.py
@@ -1,11 +1,11 @@
-#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 ..model import ModelView, ModelSQL, fields
-from ..tools import safe_eval
+# 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 ..model import ModelView, ModelSQL, fields, EvalEnvironment
from ..transaction import Transaction
from ..cache import Cache
from ..pool import Pool
from .. import backend
+from ..pyson import PYSONDecoder
__all__ = [
'RuleGroup', 'Rule',
@@ -92,11 +92,11 @@ class RuleGroup(ModelSQL, ModelView):
class Rule(ModelSQL, ModelView):
"Rule"
__name__ = 'ir.rule'
- _rec_name = 'field'
rule_group = fields.Many2One('ir.rule.group', 'Group', select=True,
required=True, ondelete="CASCADE")
domain = fields.Char('Domain', required=True,
- help='Domain is evaluated with "user" as the current user')
+ help='Domain is evaluated with a PYSON context containing:\n'
+ '- "user" as the current user')
_domain_get_cache = Cache('ir_rule.domain_get', context=False)
@classmethod
@@ -127,7 +127,7 @@ class Rule(ModelSQL, ModelView):
ctx = cls._get_context()
for rule in rules:
try:
- value = safe_eval(rule.domain, ctx)
+ value = PYSONDecoder(ctx).decode(rule.domain)
except Exception:
cls.raise_user_error('invalid_domain', (rule.rec_name,))
if not isinstance(value, list):
@@ -143,7 +143,7 @@ class Rule(ModelSQL, ModelView):
User = Pool().get('res.user')
user_id = Transaction().user
with Transaction().set_context(_check_access=False):
- user = User(user_id)
+ user = EvalEnvironment(User(user_id), User)
return {
'user': user,
}
@@ -219,7 +219,7 @@ class Rule(ModelSQL, ModelView):
for rule in cls.browse(ids):
assert rule.domain, ('Rule domain empty,'
'check if migration was done')
- dom = safe_eval(rule.domain, ctx)
+ dom = PYSONDecoder(ctx).decode(rule.domain)
if rule.rule_group.global_p:
clause_global.setdefault(rule.rule_group.id, ['OR'])
clause_global[rule.rule_group.id].append(dom)
diff --git a/trytond/ir/sequence.py b/trytond/ir/sequence.py
index 93bae63..33d0c18 100644
--- a/trytond/ir/sequence.py
+++ b/trytond/ir/sequence.py
@@ -1,5 +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.
+# 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 string import Template
import time
from itertools import izip
@@ -174,6 +174,18 @@ class Sequence(ModelSQL, ModelView):
})
@classmethod
+ def view_attributes(cls):
+ return [
+ ('//group[@id="incremental"]', 'states', {
+ 'invisible': ~Eval('type').in_(['incremental']),
+ }),
+ ('//group[@id="timestamp"]', 'states', {
+ 'invisible': ~Eval('type').in_(
+ ['decimal timestamp', 'hexadecimal timestamp']),
+ }),
+ ]
+
+ @classmethod
def create(cls, vlist):
sequences = super(Sequence, cls).create(vlist)
for sequence, values in izip(sequences, vlist):
@@ -310,7 +322,7 @@ class Sequence(ModelSQL, ModelView):
% sequence._sql_sequence_name)
number_next, = cursor.fetchone()
else:
- #Pre-fetch number_next
+ # Pre-fetch number_next
number_next = sequence.number_next_internal
cls.write([sequence], {
diff --git a/trytond/ir/sequence.xml b/trytond/ir/sequence.xml
index be5d996..c9c3506 100644
--- a/trytond/ir/sequence.xml
+++ b/trytond/ir/sequence.xml
@@ -20,7 +20,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">Sequences</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.sequence</field>
- <field name="context">{'active_test': False}</field>
+ <field name="context" eval="{'active_test': False}" pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_sequence_form_view1">
@@ -51,7 +51,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">Sequences Strict</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.sequence.strict</field>
- <field name="context">{'active_test': False}</field>
+ <field name="context" eval="{'active_test': False}" pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_sequence_strict_form_view1">
diff --git a/trytond/ir/session.py b/trytond/ir/session.py
index d9beae9..99e8e8a 100644
--- a/trytond/ir/session.py
+++ b/trytond/ir/session.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
try:
import simplejson as json
except ImportError:
diff --git a/trytond/ir/time_locale.py b/trytond/ir/time_locale.py
index 1371e77..6077daf 100644
--- a/trytond/ir/time_locale.py
+++ b/trytond/ir/time_locale.py
@@ -1,6 +1,6 @@
# -*- 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.')
+# This file is part of Tryton. The COPYRIGHT file at the top level of')
+# this repository contains the full copyright notices and license terms.')
TIME_LOCALE = \
{'bg_BG': {'%A': [u'\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a',
u'\u0432\u0442\u043e\u0440\u043d\u0438\u043a',
@@ -305,7 +305,7 @@ TIME_LOCALE = \
u'oct',
u'nov',
u'dic'],
- '%p': [u'AM', u'PM']},
+ '%p': [u'', u'']},
'es_ES': {'%A': [u'lunes',
u'martes',
u'mi\xe9rcoles',
@@ -443,59 +443,65 @@ TIME_LOCALE = \
u'\u041e\u043a\u0442\u044f\u0431\u0440\u044c',
u'\u041d\u043e\u044f\u0431\u0440\u044c',
u'\u0414\u0435\u043a\u0430\u0431\u0440\u044c'],
- '%a': [u'\u041f\u043d.',
- u'\u0412\u0442.',
- u'\u0421\u0440.',
- u'\u0427\u0442.',
- u'\u041f\u0442.',
- u'\u0421\u0431.',
- u'\u0412\u0441.'],
+ '%a': [u'\u041f\u043d',
+ u'\u0412\u0442',
+ u'\u0421\u0440',
+ u'\u0427\u0442',
+ u'\u041f\u0442',
+ u'\u0421\u0431',
+ u'\u0412\u0441'],
'%b': [None,
- u'\u044f\u043d\u0432.',
- u'\u0444\u0435\u0432\u0440.',
- u'\u043c\u0430\u0440\u0442\u0430',
- u'\u0430\u043f\u0440.',
- u'\u043c\u0430\u044f',
- u'\u0438\u044e\u043d\u044f',
- u'\u0438\u044e\u043b\u044f',
- u'\u0430\u0432\u0433.',
- u'\u0441\u0435\u043d\u0442.',
- u'\u043e\u043a\u0442.',
- u'\u043d\u043e\u044f\u0431.',
- u'\u0434\u0435\u043a.'],
+ u'\u044f\u043d\u0432',
+ u'\u0444\u0435\u0432',
+ u'\u043c\u0430\u0440',
+ u'\u0430\u043f\u0440',
+ u'\u043c\u0430\u0439',
+ u'\u0438\u044e\u043d',
+ u'\u0438\u044e\u043b',
+ u'\u0430\u0432\u0433',
+ u'\u0441\u0435\u043d',
+ u'\u043e\u043a\u0442',
+ u'\u043d\u043e\u044f',
+ u'\u0434\u0435\u043a'],
'%p': [u'', u'']},
- 'sl_SI': {'%A': [u'ponedeljek',
- u'torek',
- u'sreda',
- u'\u010detrtek',
- u'petek',
- u'sobota',
- u'nedelja'],
- '%B': [None,
- u'januar',
- u'februar',
- u'marec',
- u'april',
- u'maj',
- u'junij',
- u'julij',
- u'avgust',
- u'september',
- u'oktober',
- u'november',
- u'december'],
- '%a': [u'pon', u'tor', u'sre', u'\u010det', u'pet', u'sob', u'ned'],
- '%b': [None,
- u'jan',
- u'feb',
- u'mar',
- u'apr',
- u'maj',
- u'jun',
- u'jul',
- u'avg',
- u'sep',
- u'okt',
- u'nov',
- u'dec'],
- '%p': [u'', u'']}}
+ 'sl_SI': {'%A': [u'ponedeljek',
+ u'torek',
+ u'sreda',
+ u'\u010detrtek',
+ u'petek',
+ u'sobota',
+ u'nedelja'],
+ '%B': [None,
+ u'januar',
+ u'februar',
+ u'marec',
+ u'april',
+ u'maj',
+ u'junij',
+ u'julij',
+ u'avgust',
+ u'september',
+ u'oktober',
+ u'november',
+ u'december'],
+ '%a': [u'pon',
+ u'tor',
+ u'sre',
+ u'\u010det',
+ u'pet',
+ u'sob',
+ u'ned'],
+ '%b': [None,
+ u'jan',
+ u'feb',
+ u'mar',
+ u'apr',
+ u'maj',
+ u'jun',
+ u'jul',
+ u'avg',
+ u'sep',
+ u'okt',
+ u'nov',
+ u'dec'],
+ '%p': [u'', u'']}}
diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py
index b0c8a54..17af7ff 100644
--- a/trytond/ir/translation.py
+++ b/trytond/ir/translation.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
try:
import cStringIO as StringIO
except ImportError:
@@ -12,7 +12,8 @@ import os
from hashlib import md5
from lxml import etree
from itertools import izip
-from sql import Column
+
+from sql import Column, Null
from sql.functions import Substring, Position
from sql.conditionals import Case
from sql.operators import Or, And
@@ -23,11 +24,11 @@ from ..wizard import Wizard, StateView, StateTransition, StateAction, \
Button
from ..tools import file_open, reduce_ids, grouped_slice
from .. import backend
-from ..pyson import PYSONEncoder
+from ..pyson import PYSONEncoder, Eval
from ..transaction import Transaction
from ..pool import Pool
from ..cache import Cache
-from ..const import RECORD_CACHE_SIZE
+from ..config import config
__all__ = ['Translation',
'TranslationSetStart', 'TranslationSetSucceed', 'TranslationSet',
@@ -126,7 +127,7 @@ class Translation(ModelSQL, ModelView):
# Migration from 2.2 and 2.8
cursor.execute(*ir_translation.update([ir_translation.res_id],
- [-1], where=(ir_translation.res_id == None)
+ [-1], where=(ir_translation.res_id == Null)
| (ir_translation.res_id == 0)))
table = TableHandler(Transaction().cursor, cls, module_name)
@@ -148,7 +149,7 @@ class Translation(ModelSQL, ModelView):
& (ir_translation.name == name)
# Keep searching on all values for migration
& ((ir_translation.res_id == -1)
- | (ir_translation.res_id == None)
+ | (ir_translation.res_id == Null)
| (ir_translation.res_id == 0))))
trans_id = None
if cursor.rowcount == -1 or cursor.rowcount is None:
@@ -336,13 +337,15 @@ class Translation(ModelSQL, ModelView):
@classmethod
def search_rec_name(cls, name, clause):
clause = tuple(clause)
- translations = cls.search(['OR',
- ('src',) + clause[1:],
- ('value',) + clause[1:],
- ])
- if translations:
- return [('id', 'in', [t.id for t in translations])]
- return [(cls._rec_name,) + clause[1:]]
+ if clause[1].startswith('!') or clause[1].startswith('not '):
+ bool_op = 'AND'
+ else:
+ bool_op = 'OR'
+ return [bool_op,
+ ('src',) + clause[1:],
+ ('value',) + clause[1:],
+ (cls._rec_name,) + clause[1:],
+ ]
@classmethod
def search_model(cls, name, clause):
@@ -372,6 +375,10 @@ class Translation(ModelSQL, ModelView):
return md5((src or '').encode('utf-8')).hexdigest()
@classmethod
+ def view_attributes(cls):
+ return [('/form//field[@name="value"]', 'spell', Eval('lang'))]
+
+ @classmethod
def get_ids(cls, name, ttype, lang, ids):
"Return translation for each id"
pool = Pool()
@@ -436,7 +443,7 @@ class Translation(ModelSQL, ModelView):
(table.type == ttype),
(table.name == name),
(table.value != ''),
- (table.value != None),
+ (table.value != Null),
red_sql,
))
if fuzzy_sql:
@@ -611,7 +618,7 @@ class Translation(ModelSQL, ModelView):
& (table.type == ttype)
& (table.name == name)
& (table.value != '')
- & (table.value != None)
+ & (table.value != Null)
& (table.fuzzy == False)
& (table.res_id == -1))
if source is not None:
@@ -656,7 +663,7 @@ class Translation(ModelSQL, ModelView):
(table.type == ttype),
(table.name == name),
(table.value != ''),
- (table.value != None),
+ (table.value != Null),
(table.fuzzy == False),
(table.res_id == -1),
))
@@ -780,7 +787,7 @@ class Translation(ModelSQL, ModelView):
(model_data.db_id, model_data.noupdate)
translations = set()
- to_create = []
+ to_save = []
pofile = polib.pofile(po_path)
id2translation = {}
@@ -795,7 +802,7 @@ class Translation(ModelSQL, ModelView):
raise ValueError('Unknow translation type: %s' %
translation.type)
key2ids.setdefault(key, []).append(translation.id)
- if len(module_translations) <= RECORD_CACHE_SIZE:
+ if len(module_translations) <= config.getint('cache', 'record'):
id2translation[translation.id] = translation
def override_translation(ressource_id, new_translation):
@@ -829,12 +836,14 @@ class Translation(ModelSQL, ModelView):
# Make a first loop to retreive translation ids in the right order to
# get better read locality and a full usage of the cache.
translation_ids = []
- if len(module_translations) <= RECORD_CACHE_SIZE:
+ if len(module_translations) <= config.getint('cache', 'record'):
processes = (True,)
else:
processes = (False, True)
for processing in processes:
- if processing and len(module_translations) > RECORD_CACHE_SIZE:
+ if (processing
+ and len(module_translations) > config.getint('cache',
+ 'record')):
id2translation = dict((t.id, t)
for t in cls.browse(translation_ids))
for entry in pofile:
@@ -872,26 +881,15 @@ class Translation(ModelSQL, ModelView):
continue
if not ids:
- to_create.append(translation._save_values)
- else:
- to_write = []
+ to_save.append(translation)
+ elif not noupdate:
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_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:
- with Transaction().set_context(module=module):
- translations |= set(cls.create(to_create))
+ old_translation.value = translation.value
+ old_translation.fuzzy = translation.fuzzy
+ to_save.append(old_translation)
+ cls.save(to_save)
+ translations |= set(to_save)
if translations:
all_translations = set(cls.search([
@@ -983,7 +981,7 @@ class TranslationSet(Wizard):
set_ = StateTransition()
succeed = StateView('ir.translation.set.succeed',
'ir.translation_set_succeed_view_form', [
- Button('Ok', 'end', 'tryton-ok', default=True),
+ Button('OK', 'end', 'tryton-ok', default=True),
])
def _translate_report(self, node):
@@ -1229,7 +1227,7 @@ class TranslationClean(Wizard):
clean = StateTransition()
succeed = StateView('ir.translation.clean.succeed',
'ir.translation_clean_succeed_view_form', [
- Button('Ok', 'end', 'tryton-ok', default=True),
+ Button('OK', 'end', 'tryton-ok', default=True),
])
@staticmethod
@@ -1239,9 +1237,10 @@ class TranslationClean(Wizard):
model_name, field_name = translation.name.split(',', 1)
except ValueError:
return True
- if model_name not in pool.object_name_list():
+ try:
+ Model = pool.get(model_name)
+ except KeyError:
return True
- Model = pool.get(model_name)
if field_name not in Model._fields:
return True
@@ -1252,10 +1251,11 @@ class TranslationClean(Wizard):
model_name, field_name = translation.name.split(',', 1)
except ValueError:
return True
- if model_name not in pool.object_name_list():
+ try:
+ Model = pool.get(model_name)
+ except KeyError:
return True
if translation.res_id >= 0:
- Model = pool.get(model_name)
if field_name not in Model._fields:
return True
field = Model._fields[field_name]
@@ -1282,9 +1282,10 @@ class TranslationClean(Wizard):
model_name, field_name = translation.name.split(',', 1)
except ValueError:
return True
- if model_name not in pool.object_name_list():
+ try:
+ Model = pool.get(model_name)
+ except KeyError:
return True
- Model = pool.get(model_name)
if field_name not in Model._fields:
return True
field = Model._fields[field_name]
@@ -1300,7 +1301,9 @@ class TranslationClean(Wizard):
def _clean_view(translation):
pool = Pool()
model_name = translation.name
- if model_name not in pool.object_name_list():
+ try:
+ pool.get(model_name)
+ except KeyError:
return True
@staticmethod
@@ -1311,10 +1314,10 @@ class TranslationClean(Wizard):
translation.name.split(',', 2)
except ValueError:
return True
- if (wizard_name not in
- pool.object_name_list(type='wizard')):
+ try:
+ Wizard = pool.get(wizard_name, type='wizard')
+ except KeyError:
return True
- Wizard = pool.get(wizard_name, type='wizard')
if not Wizard:
return True
state = Wizard.states.get(state_name)
@@ -1331,9 +1334,10 @@ class TranslationClean(Wizard):
model_name, field_name = translation.name.split(',', 1)
except ValueError:
return True
- if model_name not in pool.object_name_list():
+ try:
+ Model = pool.get(model_name)
+ except KeyError:
return True
- Model = pool.get(model_name)
if field_name not in Model._fields:
return True
field = Model._fields[field_name]
@@ -1368,8 +1372,15 @@ class TranslationClean(Wizard):
'recursion_error',
):
return False
- if model_name in pool.object_name_list():
+ Model, Wizard = None, None
+ try:
Model = pool.get(model_name)
+ except KeyError:
+ try:
+ Wizard = pool.get(model_name, type='wizard')
+ except KeyError:
+ pass
+ if Model:
errors = Model._error_messages.values()
if issubclass(Model, ModelSQL):
errors += Model._sql_error_messages.values()
@@ -1377,8 +1388,7 @@ class TranslationClean(Wizard):
errors.append(error)
if translation.src not in errors:
return True
- elif model_name in pool.object_name_list(type='wizard'):
- Wizard = pool.get(model_name, type='wizard')
+ elif Wizard:
errors = Wizard._error_messages.values()
if translation.src not in errors:
return True
@@ -1530,10 +1540,10 @@ class TranslationUpdate(Wizard):
& translation.src.in_(
translation.select(translation.src,
where=((translation.value == '')
- | (translation.value == None))
+ | (translation.value == Null))
& (translation.lang == lang)))
& (translation.value != '')
- & (translation.value != None),
+ & (translation.value != Null),
group_by=translation.src))
for row in cursor.dictfetchall():
@@ -1541,13 +1551,13 @@ class TranslationUpdate(Wizard):
[translation.fuzzy, translation.value],
[True, row['value']],
where=(translation.src == row['src'])
- & ((translation.value == '') | (translation.value == None))
+ & ((translation.value == '') | (translation.value == Null))
& (translation.lang == lang)))
cursor.execute(*translation.update(
[translation.fuzzy],
[False],
- where=((translation.value == '') | (translation.value == None))
+ where=((translation.value == '') | (translation.value == Null))
& (translation.lang == lang)))
action['pyson_domain'] = PYSONEncoder().encode([
@@ -1608,9 +1618,8 @@ class TranslationExport(Wizard):
def transition_export(self):
pool = Pool()
Translation = pool.get('ir.translation')
- file_data = Translation.translation_export(
+ self.result.file = Translation.translation_export(
self.start.language.code, self.start.module.name)
- self.result.file = buffer(file_data) if file_data else None
return 'result'
def default_result(self, fields):
diff --git a/trytond/ir/translation.xml b/trytond/ir/translation.xml
index e93b30b..fbb8424 100644
--- a/trytond/ir/translation.xml
+++ b/trytond/ir/translation.xml
@@ -16,7 +16,7 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.action.act_window" id="act_translation_form">
<field name="name">Translations</field>
<field name="res_model">ir.translation</field>
- <field name="domain">[('module', '!=', None)]</field>
+ <field name="domain" eval="[('module', '!=', None)]" pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_translation_form_view1">
diff --git a/trytond/ir/trigger.py b/trytond/ir/trigger.py
index 9b88d2f..704400d 100644
--- a/trytond/ir/trigger.py
+++ b/trytond/ir/trigger.py
@@ -1,13 +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.
+# 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 datetime
import time
-from sql import Literal
+from sql import Literal, Null
from sql.aggregate import Count, Max
-from ..model import ModelView, ModelSQL, fields
-from ..pyson import Eval
-from ..tools import safe_eval, grouped_slice
+from ..model import ModelView, ModelSQL, fields, EvalEnvironment
+from ..pyson import Eval, PYSONDecoder
+from ..tools import grouped_slice
from .. import backend
from ..tools import reduce_ids
from ..transaction import Transaction
@@ -29,7 +29,7 @@ class Trigger(ModelSQL, ModelView):
'invisible': (Eval('on_create', False)
| Eval('on_write', False)
| Eval('on_delete', False)),
- }, depends=['on_create', 'on_write', 'on_delete'])
+ }, depends=['on_create', 'on_write', 'on_delete'])
on_create = fields.Boolean('On Create', select=True, states={
'invisible': Eval('on_time', False),
}, depends=['on_time'])
@@ -45,9 +45,10 @@ class Trigger(ModelSQL, ModelView):
limit_number = fields.Integer('Limit Number', required=True,
help='Limit the number of call to "Action Function" by records.\n'
'0 for no limit.')
- minimum_delay = fields.Float('Minimum Delay', help='Set a minimum delay '
- 'in minutes between call to "Action Function" for the same record.\n'
- '0 for no delay.')
+ minimum_time_delay = fields.TimeDelta('Minimum Delay',
+ help='Set a minimum time delay between call to "Action Function" '
+ 'for the same record.\n'
+ 'empty for no delay.')
action_model = fields.Many2One('ir.model', 'Action Model', required=True)
action_function = fields.Char('Action Function', required=True)
_get_triggers_cache = Cache('ir_trigger.get_triggers')
@@ -67,6 +68,29 @@ class Trigger(ModelSQL, ModelView):
cls._order.insert(0, ('name', 'ASC'))
@classmethod
+ def __register__(cls, module_name):
+ TableHandler = backend.get('TableHandler')
+ cursor = Transaction().cursor
+ table = TableHandler(cursor, cls, module_name)
+ sql_table = cls.__table__()
+
+ super(Trigger, cls).__register__(module_name)
+
+ # Migration from 3.4:
+ # change minimum_delay into timedelta minimu_time_delay
+ if table.column_exist('minimum_delay'):
+ cursor.execute(*sql_table.select(
+ sql_table.id, sql_table.minimum_delay,
+ where=sql_table.minimum_delay != Null))
+ for id_, delay in cursor.fetchall():
+ delay = datetime.timedelta(hours=delay)
+ cursor.execute(*sql_table.update(
+ [sql_table.minimu_time_delay],
+ [delay],
+ where=sql_table.id == id_))
+ table.drop_column('minimum_delay')
+
+ @classmethod
def validate(cls, triggers):
super(Trigger, cls).validate(triggers)
cls.check_condition(triggers)
@@ -96,36 +120,24 @@ class Trigger(ModelSQL, ModelView):
@fields.depends('on_time')
def on_change_on_time(self):
if self.on_time:
- return {
- 'on_create': False,
- 'on_write': False,
- 'on_delete': False,
- }
- return {}
+ self.on_create = False
+ self.on_write = False
+ self.on_delete = False
@fields.depends('on_create')
def on_change_on_create(self):
if self.on_create:
- return {
- 'on_time': False,
- }
- return {}
+ self.on_time = False
@fields.depends('on_write')
def on_change_on_write(self):
if self.on_write:
- return {
- 'on_time': False,
- }
- return {}
+ self.on_time = False
@fields.depends('on_delete')
def on_change_on_delete(self):
if self.on_delete:
- return {
- 'on_time': False,
- }
- return {}
+ self.on_time = False
@classmethod
def get_triggers(cls, model_name, mode):
@@ -159,8 +171,8 @@ class Trigger(ModelSQL, ModelView):
env['current_date'] = datetime.datetime.today()
env['time'] = time
env['context'] = Transaction().context
- env['self'] = record
- return bool(safe_eval(trigger.condition, env))
+ env['self'] = EvalEnvironment(record, record.__class__)
+ return bool(PYSONDecoder(env).decode(trigger.condition))
@classmethod
def trigger_action(cls, records, trigger):
@@ -194,8 +206,8 @@ class Trigger(ModelSQL, ModelView):
new_ids.append(record_id)
ids = new_ids
- # Filter on minimum_delay
- if trigger.minimum_delay:
+ # Filter on minimum_time_delay
+ if trigger.minimum_time_delay:
new_ids = []
for sub_ids in grouped_slice(ids):
sub_ids = list(sub_ids)
@@ -223,15 +235,14 @@ class Trigger(ModelSQL, ModelView):
delay[record_id] = datetime.datetime(year, month, day,
hours, minutes, seconds, microseconds)
if (datetime.datetime.now() - delay[record_id]
- >= datetime.timedelta(
- minutes=trigger.minimum_delay)):
+ >= trigger.minimum_time_delay):
new_ids.append(record_id)
ids = new_ids
records = Model.browse(ids)
if records:
getattr(ActionModel, trigger.action_function)(records, trigger)
- if trigger.limit_number or trigger.minimum_delay:
+ if trigger.limit_number or trigger.minimum_time_delay:
to_create = []
for record in records:
to_create.append({
diff --git a/trytond/ir/ui/__init__.py b/trytond/ir/ui/__init__.py
index c86640b..4effdfa 100644
--- a/trytond/ir/ui/__init__.py
+++ b/trytond/ir/ui/__init__.py
@@ -1,2 +1,2 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
diff --git a/trytond/ir/ui/calendar.rnc b/trytond/ir/ui/calendar.rnc
index 0d3fcfe..75fb807 100644
--- a/trytond/ir/ui/calendar.rnc
+++ b/trytond/ir/ui/calendar.rnc
@@ -9,4 +9,19 @@ attlist.calendar &=
[ a:defaultValue = "Unknown" ] attribute string { text }?
field = element field { attlist.field, empty }
attlist.field &= attribute name { text }
-start = calendar
+data = element data { attlist.data, xpath* }
+attlist.data &= empty
+
+xpath = element xpath {
+ attlist.xpath, ( field )*
+ }
+attlist.xpath &= attribute expr { text }
+attlist.xpath &=
+ [ a:defaultValue = "inside" ]
+ attribute position { "inside"
+ | "replace"
+ | "replace_attributes"
+ | "after"
+ | "before" }?
+
+start = data | calendar
diff --git a/trytond/ir/ui/calendar.rng b/trytond/ir/ui/calendar.rng
index eee6466..9b756e4 100644
--- a/trytond/ir/ui/calendar.rng
+++ b/trytond/ir/ui/calendar.rng
@@ -30,7 +30,47 @@
<define name="attlist.field" combine="interleave">
<attribute name="name"/>
</define>
+ <define name="data">
+ <element name="data">
+ <ref name="attlist.data"/>
+ <zeroOrMore>
+ <ref name="xpath"/>
+ </zeroOrMore>
+ </element>
+ </define>
+ <define name="attlist.data" combine="interleave">
+ <empty/>
+ </define>
+ <define name="xpath">
+ <element name="xpath">
+ <ref name="attlist.xpath"/>
+ <zeroOrMore>
+ <choice>
+ <ref name="field"/>
+ </choice>
+ </zeroOrMore>
+ </element>
+ </define>
+ <define name="attlist.xpath" combine="interleave">
+ <attribute name="expr"/>
+ </define>
+ <define name="attlist.xpath" combine="interleave">
+ <optional>
+ <attribute name="position" a:defaultValue="inside">
+ <choice>
+ <value>inside</value>
+ <value>replace</value>
+ <value>replace_attributes</value>
+ <value>after</value>
+ <value>before</value>
+ </choice>
+ </attribute>
+ </optional>
+ </define>
<start>
- <ref name="calendar"/>
+ <choice>
+ <ref name="data"/>
+ <ref name="calendar"/>
+ </choice>
</start>
</grammar>
diff --git a/trytond/ir/ui/form.rnc b/trytond/ir/ui/form.rnc
index 5943d85..01ac6c4 100644
--- a/trytond/ir/ui/form.rnc
+++ b/trytond/ir/ui/form.rnc
@@ -49,7 +49,7 @@ attlist.field &=
| "selection"
| "char"
| "password"
- | "float_time"
+ | "timedelta"
| "boolean"
| "reference"
| "binary"
@@ -85,6 +85,7 @@ attlist.field &=
[ a:defaultValue = "0" ] attribute tree_invisible { "0" | "1" }?
attlist.field &= attribute mode { text }?
attlist.field &= attribute view_ids { text }?
+attlist.field &= attribute product { text }?
attlist.field &=
[ a:defaultValue = "0" ] attribute invisible { "0" | "1" }?
attlist.field &= attribute sum { text }?
@@ -98,9 +99,6 @@ attlist.field &=
| "top_to_bottom"
}?
attlist.field &= attribute spell { text }?
-attlist.field &= attribute float_time { text }?
-attlist.field &= attribute img_width { text }?
-attlist.field &= attribute img_height { text }?
attlist.field &=
[a:defaultValue = "0"] attribute filename_visible { "0" | "1" }?
attlist.field &= [a:defaultValue = "0"] attribute pre_validate { "0" | "1" }?
diff --git a/trytond/ir/ui/form.rng b/trytond/ir/ui/form.rng
index 87b446c..457ffa1 100644
--- a/trytond/ir/ui/form.rng
+++ b/trytond/ir/ui/form.rng
@@ -148,7 +148,7 @@
<value>selection</value>
<value>char</value>
<value>password</value>
- <value>float_time</value>
+ <value>timedelta</value>
<value>boolean</value>
<value>reference</value>
<value>binary</value>
@@ -287,6 +287,11 @@
</define>
<define name="attlist.field" combine="interleave">
<optional>
+ <attribute name="product"/>
+ </optional>
+ </define>
+ <define name="attlist.field" combine="interleave">
+ <optional>
<attribute name="invisible" a:defaultValue="0">
<choice>
<value>0</value>
@@ -329,21 +334,6 @@
</define>
<define name="attlist.field" combine="interleave">
<optional>
- <attribute name="float_time"/>
- </optional>
- </define>
- <define name="attlist.field" combine="interleave">
- <optional>
- <attribute name="img_width"/>
- </optional>
- </define>
- <define name="attlist.field" combine="interleave">
- <optional>
- <attribute name="img_height"/>
- </optional>
- </define>
- <define name="attlist.field" combine="interleave">
- <optional>
<attribute name="filename_visible" a:defaultValue="0">
<choice>
<value>0</value>
diff --git a/trytond/ir/ui/graph.rnc b/trytond/ir/ui/graph.rnc
index 3a74cfc..9373be3 100644
--- a/trytond/ir/ui/graph.rnc
+++ b/trytond/ir/ui/graph.rnc
@@ -23,8 +23,7 @@ attlist.field &= attribute domain { text }?
attlist.field &= attribute fill { "0" | "1" }?
attlist.field &= attribute empty { "0" | "1" }?
attlist.field &= attribute color { text }?
-attlist.field &= attribute widget { "float_time" }?
-attlist.field &= attribute float_time { text }?
+attlist.field &= attribute timedelta { text }?
attlist.field &=
[ a:defaultValue = "linear" ]
attribute interpolation { "constant-left" | "constant-right" | "constant-center" | "linear" }?
diff --git a/trytond/ir/ui/graph.rng b/trytond/ir/ui/graph.rng
index 125c49c..eb55457 100644
--- a/trytond/ir/ui/graph.rng
+++ b/trytond/ir/ui/graph.rng
@@ -115,14 +115,7 @@
</define>
<define name="attlist.field" combine="interleave">
<optional>
- <attribute name="widget">
- <value>float_time</value>
- </attribute>
- </optional>
- </define>
- <define name="attlist.field" combine="interleave">
- <optional>
- <attribute name="float_time"/>
+ <attribute name="timedelta"/>
</optional>
</define>
<define name="attlist.field" combine="interleave">
diff --git a/trytond/ir/ui/menu.py b/trytond/ir/ui/menu.py
index 94af4d8..d3d83c9 100644
--- a/trytond/ir/ui/menu.py
+++ b/trytond/ir/ui/menu.py
@@ -1,7 +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.
+# 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 groupby
+from sql import Null
+
from trytond.model import ModelView, ModelSQL, fields
from trytond.transaction import Transaction
from trytond.pool import Pool
@@ -169,7 +171,7 @@ class UIMenu(ModelSQL, ModelView):
('rec_name', 'ilike', '%%%s%%' % text),
]):
if record.action:
- yield record.id, record.rec_name, record.icon
+ yield record, record.rec_name, record.icon
@classmethod
def search(cls, domain, offset=0, limit=None, order=None, count=False,
@@ -294,7 +296,7 @@ class UIMenuFavorite(ModelSQL, ModelView):
@staticmethod
def order_sequence(tables):
table, _ = tables[None]
- return [table.sequence == None, table.sequence]
+ return [table.sequence == Null, table.sequence]
@staticmethod
def default_user():
diff --git a/trytond/ir/ui/menu.xml b/trytond/ir/ui/menu.xml
index 4a0a610..7b11a1d 100644
--- a/trytond/ir/ui/menu.xml
+++ b/trytond/ir/ui/menu.xml
@@ -28,7 +28,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">Menu</field>
<field name="res_model">ir.ui.menu</field>
<field name="usage">menu</field>
- <field name="domain">[('parent', '=', None)]</field>
+ <field name="domain" eval="[('parent', '=', None)]" pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_menu_tree_view1">
diff --git a/trytond/ir/ui/tree.rnc b/trytond/ir/ui/tree.rnc
index 925331e..e42ec0d 100644
--- a/trytond/ir/ui/tree.rnc
+++ b/trytond/ir/ui/tree.rnc
@@ -27,7 +27,7 @@ attlist.field &=
| "selection"
| "float"
| "numeric"
- | "float_time"
+ | "timedelta"
| "integer"
| "datetime"
| "time"
@@ -58,9 +58,9 @@ attlist.field &=
| "bottom_to_top"
| "top_to_bottom"
}?
-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 &= attribute string { text }?
attlist.field &= [a:defaultValue = "1"] attribute factor { text }?
attlist.field &= attribute filename { text }?
prefix = element prefix { attlist.affix, empty }
diff --git a/trytond/ir/ui/tree.rng b/trytond/ir/ui/tree.rng
index 0508c44..44a5d3c 100644
--- a/trytond/ir/ui/tree.rng
+++ b/trytond/ir/ui/tree.rng
@@ -97,7 +97,7 @@
<value>selection</value>
<value>float</value>
<value>numeric</value>
- <value>float_time</value>
+ <value>timedelta</value>
<value>integer</value>
<value>datetime</value>
<value>time</value>
@@ -170,11 +170,6 @@
</define>
<define name="attlist.field" combine="interleave">
<optional>
- <attribute name="float_time"/>
- </optional>
- </define>
- <define name="attlist.field" combine="interleave">
- <optional>
<attribute name="pre_validate" a:defaultValue="0">
<choice>
<value>0</value>
@@ -195,6 +190,11 @@
</define>
<define name="attlist.field" combine="interleave">
<optional>
+ <attribute name="string"/>
+ </optional>
+ </define>
+ <define name="attlist.field" combine="interleave">
+ <optional>
<attribute name="factor" a:defaultValue="1"/>
</optional>
</define>
diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py
index 2c3fb90..ccc469d 100644
--- a/trytond/ir/ui/view.py
+++ b/trytond/ir/ui/view.py
@@ -1,5 +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.
+# 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 sys
import logging
@@ -11,8 +11,8 @@ except ImportError:
from lxml import etree
from trytond.model import ModelView, ModelSQL, fields
from trytond import backend
-from trytond.pyson import CONTEXT, Eval, Bool, PYSONDecoder
-from trytond.tools import safe_eval, file_open
+from trytond.pyson import Eval, Bool, PYSONDecoder
+from trytond.tools import file_open
from trytond.transaction import Transaction
from trytond.wizard import Wizard, StateView, Button
from trytond.pool import Pool
@@ -24,6 +24,8 @@ __all__ = [
'ViewTreeWidth', 'ViewTreeState', 'ViewSearch',
]
+logger = logging.getLogger(__name__)
+
class View(ModelSQL, ModelView):
"View"
@@ -137,11 +139,10 @@ class View(ModelSQL, ModelView):
rng_type = view.inherit.type if view.inherit else view.type
validator = etree.RelaxNG(etree=cls.get_rng(rng_type))
if not validator.validate(tree):
- logger = logging.getLogger('ir')
error_log = reduce(lambda x, y: str(x) + '\n' + str(y),
validator.error_log.filter_from_errors())
- logger.error('Invalid xml view:\n%s'
- % (str(error_log) + '\n' + xml))
+ logger.error('Invalid xml view:\n%s',
+ str(error_log) + '\n' + xml)
cls.raise_user_error('invalid_xml', (view.rec_name,))
root_element = tree.getroottree().getroot()
@@ -154,14 +155,13 @@ class View(ModelSQL, ModelView):
for attr in ('states', 'domain', 'spell', 'colors'):
if element.get(attr):
try:
- value = safe_eval(element.get(attr), CONTEXT)
+ value = PYSONDecoder().decode(element.get(attr))
validates.get(attr, lambda a: True)(value)
except Exception, e:
- logger = logging.getLogger('ir')
logger.error('Invalid pyson view element "%s:%s":'
- '\n%s\n%s'
- % (element.get('id') or element.get('name'),
- attr, str(e), xml))
+ '\n%s\n%s',
+ element.get('id') or element.get('name'), attr,
+ str(e), xml)
return False
for child in element:
if not encode(child):
@@ -221,7 +221,7 @@ class ShowView(Wizard):
def __init__(self, model_name, buttons):
StateView.__init__(self, model_name, None, buttons)
- def get_view(self):
+ def get_view(self, wizard, state_name):
pool = Pool()
View = pool.get('ir.ui.view')
view = View(Transaction().context.get('active_id'))
@@ -339,6 +339,8 @@ class ViewTreeState(ModelSQL, ModelView):
@classmethod
def set(cls, model, domain, child_name, nodes, selected_nodes):
+ # Normalize the json domain
+ domain = json.dumps(json.loads(domain))
current_user = Transaction().user
records = cls.search([
('user', '=', current_user),
diff --git a/trytond/ir/view/action_act_window_list.xml b/trytond/ir/view/action_act_window_list.xml
index 586003a..e9dd672 100644
--- a/trytond/ir/view/action_act_window_list.xml
+++ b/trytond/ir/view/action_act_window_list.xml
@@ -5,4 +5,5 @@ this repository contains the full copyright notices and license terms. -->
<field name="name"/>
<field name="res_model"/>
<field name="domain"/>
+ <field name="active" tree_invisible="1"/>
</tree>
diff --git a/trytond/ir/view/action_list.xml b/trytond/ir/view/action_list.xml
index 28a7fcc..cdc8e09 100644
--- a/trytond/ir/view/action_list.xml
+++ b/trytond/ir/view/action_list.xml
@@ -4,4 +4,5 @@ this repository contains the full copyright notices and license terms. -->
<tree string="Action">
<field name="name"/>
<field name="type"/>
+ <field name="active" tree_invisible="1"/>
</tree>
diff --git a/trytond/ir/view/action_report_list.xml b/trytond/ir/view/action_report_list.xml
index 5acdb1c..48b29e3 100644
--- a/trytond/ir/view/action_report_list.xml
+++ b/trytond/ir/view/action_report_list.xml
@@ -6,5 +6,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="type"/>
<field name="model"/>
<field name="report_name"/>
+ <field name="active" tree_invisible="1"/>
</tree>
diff --git a/trytond/ir/view/action_url_list.xml b/trytond/ir/view/action_url_list.xml
index ad519f1..b10b9e8 100644
--- a/trytond/ir/view/action_url_list.xml
+++ b/trytond/ir/view/action_url_list.xml
@@ -5,4 +5,5 @@ this repository contains the full copyright notices and license terms. -->
<field name="name"/>
<field name="type"/>
<field name="url"/>
+ <field name="active" tree_invisible="1"/>
</tree>
diff --git a/trytond/ir/view/action_wizard_list.xml b/trytond/ir/view/action_wizard_list.xml
index ee6745e..89dc207 100644
--- a/trytond/ir/view/action_wizard_list.xml
+++ b/trytond/ir/view/action_wizard_list.xml
@@ -5,4 +5,5 @@ this repository contains the full copyright notices and license terms. -->
<field name="name"/>
<field name="type"/>
<field name="wiz_name"/>
+ <field name="active" tree_invisible="1"/>
</tree>
diff --git a/trytond/ir/view/attachment_list.xml b/trytond/ir/view/attachment_list.xml
index 07ca122..22a8385 100644
--- a/trytond/ir/view/attachment_list.xml
+++ b/trytond/ir/view/attachment_list.xml
@@ -6,6 +6,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name"/>
<field name="summary"/>
<field name="last_user"/>
- <field name="last_modification"/>
+ <field name="last_modification" widget="date"/>
+ <field name="last_modification" widget="time"
+ string="Last Modification Time"/>
<field name="data"/>
</tree>
diff --git a/trytond/ir/view/cron_list.xml b/trytond/ir/view/cron_list.xml
index 635f25a..b8a2e86 100644
--- a/trytond/ir/view/cron_list.xml
+++ b/trytond/ir/view/cron_list.xml
@@ -2,7 +2,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. -->
<tree string="Scheduled Actions">
- <field name="next_call"/>
+ <field name="next_call" widget="date"/>
+ <field name="next_call" widget="time"/>
<field name="name"/>
<field name="user"/>
<field name="request_user"/>
diff --git a/trytond/ir/view/module_list.xml b/trytond/ir/view/module_list.xml
index cf60347..921f224 100644
--- a/trytond/ir/view/module_list.xml
+++ b/trytond/ir/view/module_list.xml
@@ -1,8 +1,7 @@
<?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. -->
-<tree string="Modules" on_write="on_write"
- colors="If(In(Eval('state', ''), ['to upgrade', 'to install']), 'blue', If(Equal(Eval('state', ''), 'uninstalled'), 'grey', 'black'))">
+<tree string="Modules" on_write="on_write">
<field name="name" expand="1"/>
<field name="version"/>
<field name="state"/>
diff --git a/trytond/ir/view/sequence_form.xml b/trytond/ir/view/sequence_form.xml
index 9b96b59..13b1834 100644
--- a/trytond/ir/view/sequence_form.xml
+++ b/trytond/ir/view/sequence_form.xml
@@ -14,8 +14,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="suffix"/>
<label name="type"/>
<field name="type"/>
- <group string="Incremental" id="incremental" colspan="4"
- states="{'invisible': Not(In(Eval('type'), ['incremental']))}">
+ <group string="Incremental" id="incremental" colspan="4">
<label name="padding"/>
<field name="padding" />
<label name="number_increment"/>
@@ -23,8 +22,7 @@ this repository contains the full copyright notices and license terms. -->
<label name="number_next"/>
<field name="number_next"/>
</group>
- <group string="Timestamp" id="timestamp" colspan="4"
- states="{'invisible': Not(In(Eval('type'), ['decimal timestamp', 'hexadecimal timestamp']))}">
+ <group string="Timestamp" id="timestamp" colspan="4">
<label name="timestamp_rounding"/>
<field name="timestamp_rounding"/>
<label name="timestamp_offset"/>
diff --git a/trytond/ir/view/translation_form.xml b/trytond/ir/view/translation_form.xml
index 38c0f48..8835c4d 100644
--- a/trytond/ir/view/translation_form.xml
+++ b/trytond/ir/view/translation_form.xml
@@ -20,6 +20,6 @@ this repository contains the full copyright notices and license terms. -->
<separator name="src"/>
<separator name="value"/>
<field name="src"/>
- <field name="value" spell="Eval('lang')"/>
+ <field name="value"/>
</group>
</form>
diff --git a/trytond/ir/view/trigger_form.xml b/trytond/ir/view/trigger_form.xml
index 5301e59..5077179 100644
--- a/trytond/ir/view/trigger_form.xml
+++ b/trytond/ir/view/trigger_form.xml
@@ -22,8 +22,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="condition" colspan="3"/>
<label name="limit_number"/>
<field name="limit_number"/>
- <label name="minimum_delay"/>
- <field name="minimum_delay" widget="float_time"/>
+ <label name="minimum_time_delay"/>
+ <field name="minimum_time_delay"/>
<label name="action_model"/>
<field name="action_model"/>
<label name="action_function"/>
diff --git a/trytond/model/__init__.py b/trytond/model/__init__.py
index dae657d..1a71ef8 100644
--- a/trytond/model/__init__.py
+++ b/trytond/model/__init__.py
@@ -1,14 +1,16 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 .model import Model
from .modelview import ModelView
-from .modelstorage import ModelStorage
+from .modelstorage import ModelStorage, EvalEnvironment
from .modelsingleton import ModelSingleton
from .modelsql import ModelSQL
from .workflow import Workflow
from .dictschema import DictSchemaMixin
from .match import MatchMixin
from .union import UnionMixin
+from .descriptors import dualmethod
__all__ = ['Model', 'ModelView', 'ModelStorage', 'ModelSingleton', 'ModelSQL',
- 'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin']
+ 'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin', 'dualmethod',
+ 'EvalEnvironment']
diff --git a/trytond/model/descriptors.py b/trytond/model/descriptors.py
new file mode 100644
index 0000000..eace007
--- /dev/null
+++ b/trytond/model/descriptors.py
@@ -0,0 +1,34 @@
+# 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 functools
+
+
+class dualmethod(object):
+ """Descriptor implementing combination of class and instance method
+
+ When called on an instance, the class is passed as the first argument and a
+ list with the instance as the second.
+ When called on a class, the class itsefl is passed as the first argument.
+
+ >>> class Example(object):
+ ... @dualmethod
+ ... def method(cls, instances):
+ ... print len(instances)
+ ...
+ >>> Example.method([Example()])
+ 1
+ >>> Example().method()
+ 1
+ """
+ def __init__(self, func):
+ self.func = func
+
+ def __get__(self, instance, owner):
+
+ @functools.wraps(self.func)
+ def newfunc(*args, **kwargs):
+ if instance:
+ return self.func(owner, [instance], *args, **kwargs)
+ else:
+ return self.func(owner, *args, **kwargs)
+ return newfunc
diff --git a/trytond/model/fields/__init__.py b/trytond/model/fields/__init__.py
index 636f22a..529b3e9 100644
--- a/trytond/model/fields/__init__.py
+++ b/trytond/model/fields/__init__.py
@@ -1,5 +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.
+# 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 .field import *
from .boolean import *
diff --git a/trytond/model/fields/binary.py b/trytond/model/fields/binary.py
index 42faa79..ab65b91 100644
--- a/trytond/model/fields/binary.py
+++ b/trytond/model/fields/binary.py
@@ -1,5 +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.
+# 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 Query, Expression
from .field import Field, SQLType
@@ -9,9 +9,10 @@ from ... import backend
class Binary(Field):
'''
- Define a binary field (``str``).
+ Define a binary field (``bytes``).
'''
_type = 'binary'
+ cast = bytearray if bytes == str else bytes
def __init__(self, string='', help='', required=False, readonly=False,
domain=None, states=None, select=False, on_change=None,
@@ -28,10 +29,10 @@ class Binary(Field):
select=select, on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
- @staticmethod
- def get(ids, model, name, values=None):
+ @classmethod
+ def get(cls, ids, model, name, values=None):
'''
- Convert the binary value into ``str``
+ Convert the binary value into ``bytes``
:param ids: a list of ids
:param model: a string with the name of the model
@@ -42,7 +43,7 @@ class Binary(Field):
if values is None:
values = {}
res = {}
- converter = buffer
+ converter = cls.cast
default = None
format_ = Transaction().context.pop('%s.%s' % (model.__name__, name),
'')
@@ -50,7 +51,14 @@ class Binary(Field):
converter = len
default = 0
for i in values:
- res[i['id']] = converter(i[name]) if i[name] else default
+ value = i[name]
+ if value:
+ if isinstance(value, unicode):
+ value = value.encode('utf-8')
+ value = converter(value)
+ else:
+ value = default
+ res[i['id']] = value
for i in ids:
res.setdefault(i, default)
return res
diff --git a/trytond/model/fields/boolean.py b/trytond/model/fields/boolean.py
index 0dc00f4..dad5b63 100644
--- a/trytond/model/fields/boolean.py
+++ b/trytond/model/fields/boolean.py
@@ -1,5 +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.
+# 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 .field import Field, SQLType
diff --git a/trytond/model/fields/char.py b/trytond/model/fields/char.py
index 6254077..d2d6b61 100644
--- a/trytond/model/fields/char.py
+++ b/trytond/model/fields/char.py
@@ -1,5 +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.
+# 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
diff --git a/trytond/model/fields/date.py b/trytond/model/fields/date.py
index 428f6c4..e2b8445 100644
--- a/trytond/model/fields/date.py
+++ b/trytond/model/fields/date.py
@@ -1,5 +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.
+# 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 datetime
from sql import Query, Expression
@@ -134,3 +134,60 @@ class Time(DateTime):
def sql_type(self):
return SQLType('TIME', 'TIME')
+
+
+class TimeDelta(Field):
+ '''
+ Define a timedelta field (``timedelta``).
+ '''
+ _type = 'timedelta'
+
+ def __init__(self, string='', converter=None, help='', required=False,
+ readonly=False, domain=None, states=None, select=False,
+ on_change=None, on_change_with=None, depends=None,
+ context=None, loading='eager'):
+ '''
+ :param converter: The name of the context key containing
+ the time converter.
+ '''
+ super(TimeDelta, self).__init__(string=string, help=help,
+ required=required, 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.converter = converter
+
+ @staticmethod
+ def sql_format(value):
+ if isinstance(value, (Query, Expression)):
+ return value
+ if value is None:
+ return None
+ assert(isinstance(value, datetime.timedelta))
+ db_type = backend.name()
+ if db_type == 'mysql':
+ return value.total_seconds()
+ return value
+
+ def sql_type(self):
+ db_type = backend.name()
+ if db_type == 'mysql':
+ return SQLType('DOUBLE', 'DOUBLE(255, 6)')
+ return SQLType('INTERVAL', 'INTERVAL')
+
+ @classmethod
+ def get(cls, ids, model, name, values=None):
+ result = {}
+ for row in values:
+ value = row[name]
+ if (value is not None
+ and not isinstance(value, datetime.timedelta)):
+ if value >= datetime.timedelta.max.total_seconds():
+ value = datetime.timedelta.max
+ elif value <= datetime.timedelta.min.total_seconds():
+ value = datetime.timedelta.min
+ else:
+ value = datetime.timedelta(seconds=value)
+ result[row['id']] = value
+ else:
+ result[row['id']] = value
+ return result
diff --git a/trytond/model/fields/dict.py b/trytond/model/fields/dict.py
index ef63d52..be65ba2 100644
--- a/trytond/model/fields/dict.py
+++ b/trytond/model/fields/dict.py
@@ -8,6 +8,8 @@ from sql import Query, Expression
from .field import Field, SQLType
from ...protocols.jsonrpc import JSONDecoder, JSONEncoder
+from ...pool import Pool
+from ...tools import grouped_slice
class Dict(Field):
@@ -42,3 +44,50 @@ class Dict(Field):
def sql_type(self):
return SQLType('TEXT', 'TEXT')
+
+ def translated(self, name=None, type_='values'):
+ "Return a descriptor for the translated value of the field"
+ if name is None:
+ name = self.name
+ if name is None:
+ raise ValueError('Missing name argument')
+ return TranslatedDict(name, type_)
+
+
+class TranslatedDict(object):
+ 'A descriptor for translated values of Dict field'
+
+ def __init__(self, name, type_):
+ assert type_ in ['keys', 'values']
+ self.name = name
+ self.type_ = type_
+
+ def __get__(self, inst, cls):
+ if inst is None:
+ return self
+ pool = Pool()
+ schema_model = getattr(cls, self.name).schema_model
+ SchemaModel = pool.get(schema_model)
+
+ value = getattr(inst, self.name)
+ if not value:
+ return value
+
+ domain = []
+ if self.type_ == 'values':
+ domain = [('type_', '=', 'selection')]
+
+ records = []
+ for key_names in grouped_slice(value.keys()):
+ records += SchemaModel.search([
+ ('name', 'in', key_names),
+ ] + domain)
+ keys = SchemaModel.get_keys(records)
+
+ if self.type_ == 'keys':
+ return {k['name']: k['string'] for k in keys}
+
+ elif self.type_ == 'values':
+ trans = {k['name']: dict(k['selection']) for k in keys}
+ return {k: v if k not in trans else trans[k].get(v, v)
+ for k, v in value.iteritems()}
diff --git a/trytond/model/fields/field.py b/trytond/model/fields/field.py
index 3eb95f2..d09bdb0 100644
--- a/trytond/model/fields/field.py
+++ b/trytond/model/fields/field.py
@@ -1,14 +1,14 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 import operators, Column, Literal, Select, CombiningQuery, Null
from sql.conditionals import Coalesce, NullIf
from sql.operators import Concat
-from trytond.pyson import PYSON
+from trytond.pyson import PYSON, PYSONEncoder, Eval
from trytond.const import OPERATORS
from trytond.transaction import Transaction
from trytond.pool import Pool
@@ -91,6 +91,25 @@ def depends(*fields, **kwargs):
return decorator
+def get_eval_fields(value):
+ "Return fields evaluated"
+ class Encoder(PYSONEncoder):
+ def __init__(self, *args, **kwargs):
+ super(Encoder, self).__init__(*args, **kwargs)
+ self.fields = set()
+
+ def default(self, obj):
+ if isinstance(obj, Eval):
+ fname = obj._value
+ if not fname.startswith('_parent_'):
+ self.fields.add(fname)
+ return super(Encoder, self).default(obj)
+
+ encoder = Encoder()
+ encoder.encode(value)
+ return encoder.fields
+
+
SQL_OPERATORS = {
'=': operators.Equal,
'!=': operators.NotEqual,
@@ -241,9 +260,9 @@ class Field(object):
if (not isinstance(value, (Select, CombiningQuery))
and any(v is None for v in value)):
if operator == 'in':
- expression |= (column == None)
+ expression |= (column == Null)
else:
- expression &= (column != None)
+ expression &= (column != Null)
return expression
def convert_domain(self, domain, tables, Model):
diff --git a/trytond/model/fields/float.py b/trytond/model/fields/float.py
index 42a8db5..be716af 100644
--- a/trytond/model/fields/float.py
+++ b/trytond/model/fields/float.py
@@ -1,5 +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.
+# 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 Query, Expression
from ... import backend
diff --git a/trytond/model/fields/function.py b/trytond/model/fields/function.py
index 8a72955..e5a48af 100644
--- a/trytond/model/fields/function.py
+++ b/trytond/model/fields/function.py
@@ -1,9 +1,10 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 inspect
import copy
from trytond.model.fields.field import Field
+from trytond.tools import is_instance_method
from trytond.transaction import Transaction
@@ -57,6 +58,10 @@ class Function(Field):
return
setattr(self._field, name, value)
+ @property
+ def sql_type(self):
+ raise AttributeError
+
def convert_domain(self, domain, tables, Model):
name, operator, value = domain[:3]
if not self.searcher:
@@ -71,10 +76,11 @@ class Function(Field):
'''
with Transaction().set_context(_check_access=False):
method = getattr(Model, self.getter)
+ instance_method = is_instance_method(Model, self.getter)
def call(name):
records = Model.browse(ids)
- if not hasattr(method, 'im_self') or method.im_self:
+ if not instance_method:
return method(records, name)
else:
return dict((r.id, method(r, name)) for r in records)
diff --git a/trytond/model/fields/integer.py b/trytond/model/fields/integer.py
index 64f51bf..720fa4a 100644
--- a/trytond/model/fields/integer.py
+++ b/trytond/model/fields/integer.py
@@ -1,5 +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.
+# 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 Query, Expression
from ... import backend
diff --git a/trytond/model/fields/many2many.py b/trytond/model/fields/many2many.py
index 46f4da5..7816f26 100644
--- a/trytond/model/fields/many2many.py
+++ b/trytond/model/fields/many2many.py
@@ -1,7 +1,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.
+# 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
+
+from sql import Cast, Literal, Null
from sql.functions import Substring, Position
from .field import Field, size_validate
@@ -235,7 +236,7 @@ class Many2Many(Field):
return Target(**data)
else:
return Target(data)
- value = [instance(x) for x in (value or [])]
+ value = tuple(instance(x) for x in (value or []))
super(Many2Many, self).__set__(inst, value)
def convert_domain_child(self, domain, tables):
@@ -281,7 +282,7 @@ class Many2Many(Field):
if Target != Model:
query = Target.search([(domain[3], 'child_of', value)],
order=[], query=True)
- where = (target.in_(query) & (origin != None))
+ where = (target.in_(query) & (origin != Null))
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 e6aa9ba..a7051a5 100644
--- a/trytond/model/fields/many2one.py
+++ b/trytond/model/fields/many2one.py
@@ -1,5 +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.
+# 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 Query, Expression
from sql.operators import Or
@@ -180,25 +180,33 @@ class Many2One(Field):
return column.in_(query)
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
+ fname, _, oexpr = name.partition('.')
+ if not oexpr and getattr(Model, 'order_%s' % fname, None):
+ return super(Many2One, self).convert_order(fname, tables, Model)
+ assert fname == self.name
Target = self.get_target()
- oname = 'id'
- if Target._rec_name in Target._fields:
- oname = Target._rec_name
- if Target._order_name in Target._fields:
- oname = Target._order_name
+ if oexpr:
+ oname, _, _ = oexpr.partition('.')
+ else:
+ oname = 'id'
+ if Target._rec_name in Target._fields:
+ oname = Target._rec_name
+ if Target._order_name in Target._fields:
+ oname = Target._order_name
+ oexpr = oname
- ofield = Target._fields[oname]
table, _ = tables[None]
- target_tables = tables.get(name)
+ if oname == 'id':
+ return [self.sql_column(table)]
+
+ ofield = Target._fields[oname]
+ target_tables = tables.get(fname)
if target_tables is None:
target = Target.__table__()
target_tables = {
None: (target, target.id == self.sql_column(table)),
}
- tables[name] = target_tables
- return ofield.convert_order(oname, target_tables, Target)
+ tables[fname] = target_tables
+ return ofield.convert_order(oexpr, target_tables, Target)
diff --git a/trytond/model/fields/numeric.py b/trytond/model/fields/numeric.py
index 3580bb7..ce9abdc 100644
--- a/trytond/model/fields/numeric.py
+++ b/trytond/model/fields/numeric.py
@@ -1,13 +1,20 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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, Cast, Literal, Select, CombiningQuery
+from sql import Query, Expression, Cast, Literal, Select, CombiningQuery, As
from ... import backend
from .field import SQLType
from .float import Float
+class SQLite_Cast(Cast):
+
+ def as_(self, output_name):
+ # Use PARSE_COLNAMES instead of CAST for final column
+ return As(self.expression, '%s [NUMERIC]' % output_name)
+
+
class Numeric(Float):
'''
Define a numeric field (``decimal``).
@@ -36,7 +43,7 @@ class Numeric(Float):
db_type = backend.name()
if db_type == 'sqlite':
# Must be casted as Decimal is stored as bytes
- column = Cast(column, self.sql_type().base)
+ column = SQLite_Cast(column, self.sql_type().base)
return column
def _domain_value(self, operator, value):
diff --git a/trytond/model/fields/one2many.py b/trytond/model/fields/one2many.py
index 5387ab6..2ea150f 100644
--- a/trytond/model/fields/one2many.py
+++ b/trytond/model/fields/one2many.py
@@ -1,5 +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.
+# 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
from sql.functions import Substring, Position
@@ -218,7 +218,7 @@ class One2Many(Field):
return Target(**data)
else:
return Target(data)
- value = [instance(x) for x in (value or [])]
+ value = tuple(instance(x) for x in (value or []))
super(One2Many, self).__set__(inst, value)
def convert_domain(self, domain, tables, Model):
diff --git a/trytond/model/fields/one2one.py b/trytond/model/fields/one2one.py
index 4e6fe87..60139dc 100644
--- a/trytond/model/fields/one2one.py
+++ b/trytond/model/fields/one2one.py
@@ -1,5 +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.
+# 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 trytond.model.fields.field import Field
diff --git a/trytond/model/fields/property.py b/trytond/model/fields/property.py
index 94a77a4..84621f1 100644
--- a/trytond/model/fields/property.py
+++ b/trytond/model/fields/property.py
@@ -1,8 +1,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.
-
+# 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 copy
-from sql import Cast, Literal
+
+from sql import Cast, Literal, Null
from sql.functions import Substring, Position
from sql.conditionals import Case
@@ -82,7 +82,7 @@ class Property(Function):
model_field = Field.__table__()
model = IrModel.__table__()
- #Fetch res ids that comply with the domain
+ # Fetch res ids that comply with the domain
join = property_.join(model_field,
condition=model_field.id == property_.field)
join = join.join(model,
@@ -119,12 +119,12 @@ class Property(Function):
dom_operator = 'not in'
return [('id', dom_operator, [x[0] for x in props])]
- #Fetch the res ids that doesn't use the default value
+ # Fetch the res ids that doesn't use the default value
cursor.execute(*property_.select(
Cast(Substring(property_.res,
Position(',', property_.res) + Literal(1)),
Model.id.sql_type().base),
- where=property_cond & (property_.res != None)))
+ where=property_cond & (property_.res != Null)))
fetchall = cursor.fetchall()
if not fetchall:
@@ -165,7 +165,7 @@ class Property(Function):
return column.in_(value)
elif ((value is False or value is None)
and operator in ('=', '!=')):
- return (column == None) == value
+ return (column == Null) == value
elif operator == 'not like':
return column.like(value)
elif operator == 'not ilike':
diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py
index 19db451..970351c 100644
--- a/trytond/model/fields/reference.py
+++ b/trytond/model/fields/reference.py
@@ -1,5 +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.
+# 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
import warnings
diff --git a/trytond/model/fields/selection.py b/trytond/model/fields/selection.py
index 1f75d27..bd29efe 100644
--- a/trytond/model/fields/selection.py
+++ b/trytond/model/fields/selection.py
@@ -1,5 +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.
+# 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.conditionals import Case
diff --git a/trytond/model/fields/sha.py b/trytond/model/fields/sha.py
index 222aca8..2df89d3 100644
--- a/trytond/model/fields/sha.py
+++ b/trytond/model/fields/sha.py
@@ -1,5 +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.
+# 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 hashlib
from sql import Query, Expression
diff --git a/trytond/model/fields/text.py b/trytond/model/fields/text.py
index 6d2b37e..f97a805 100644
--- a/trytond/model/fields/text.py
+++ b/trytond/model/fields/text.py
@@ -1,5 +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.
+# 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 .field import SQLType
from .char import Char
diff --git a/trytond/model/model.py b/trytond/model/model.py
index 13e8554..eca190f 100644
--- a/trytond/model/model.py
+++ b/trytond/model/model.py
@@ -1,5 +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.
+# 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 copy
import collections
@@ -32,17 +32,10 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
cls.__rpc__ = {
'default_get': RPC(),
'fields_get': RPC(),
- 'on_change': RPC(instantiate=0),
- 'on_change_with': RPC(instantiate=0),
'pre_validate': RPC(instantiate=0),
}
cls._error_messages = {}
- 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('_'):
@@ -60,31 +53,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
field = copy.deepcopy(field)
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)
- if not getattr(cls, function_name, None):
- continue
- # Search depends on all parent class because field has been
- # copied with the original definition
- for parent_cls in cls.__mro__:
- function = getattr(parent_cls, function_name, None)
- if not function:
- continue
- 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):
super(Model, cls).__post_setup__()
@@ -110,30 +78,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
'Default function defined in %s but field %s does not exist!' \
% (cls.__name__, k,)
- # Update __rpc__
- for field_name, field in cls._fields.iteritems():
- if isinstance(field, (fields.Selection, fields.Reference)) \
- and not isinstance(field.selection, (list, tuple)) \
- and field.selection not in cls.__rpc__:
- instantiate = 0 if field.selection_change_with else None
- cls.__rpc__.setdefault(field.selection,
- RPC(instantiate=instantiate))
-
- for attribute in ('on_change', 'on_change_with', 'autocomplete'):
- function_name = '%s_%s' % (attribute, field_name)
- 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:
@@ -148,6 +92,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
'''
Returns the first non-empty line of the model docstring.
'''
+ assert cls.__doc__, '%s has no docstring' % cls
lines = cls.__doc__.splitlines()
for line in lines:
line = line.strip()
@@ -188,7 +133,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
value[field_name] = cls._defaults[field_name]()
field = cls._fields[field_name]
if (field._type == 'boolean'
- and not field_name in value):
+ and field_name not in value):
value[field_name] = False
if isinstance(field, fields.Property):
value[field_name] = Property.get(field_name, cls.__name__)
@@ -217,7 +162,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
FieldAccess = pool.get('ir.model.field.access')
ModelAccess = pool.get('ir.model.access')
- #Add translation to cache
+ # Add translation to cache
language = Transaction().language
trans_args = []
for field in (x for x in cls._fields.keys()
@@ -267,6 +212,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
'filename',
'selection_change_with',
'domain',
+ 'converter',
):
if getattr(cls._fields[field], arg, None) is not None:
value = getattr(cls._fields[field], arg)
@@ -291,7 +237,10 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
res[field]['sortable'] = False
if ((isinstance(cls._fields[field], fields.Function)
and not cls._fields[field].searcher)
- or cls._fields[field]._type in ('binary', 'sha')):
+ or (cls._fields[field]._type in ('binary', 'sha'))
+ or (isinstance(cls._fields[field], fields.Property)
+ and isinstance(cls._fields[field]._field,
+ fields.Many2One))):
res[field]['searchable'] = False
else:
res[field]['searchable'] = True
@@ -391,21 +340,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
del res[i]
return res
- def on_change(self, fieldnames):
- changes = []
- for fieldname in sorted(fieldnames):
- method = getattr(self, 'on_change_%s' % fieldname, None)
- if method:
- changes.append(method())
- return changes
-
- def on_change_with(self, fieldnames):
- changes = {}
- for fieldname in fieldnames:
- method_name = 'on_change_with_%s' % fieldname
- changes[fieldname] = getattr(self, method_name)()
- return changes
-
def pre_validate(self):
pass
@@ -429,6 +363,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
setattr(parent, field, value)
else:
setattr(self, parent_name, {field: value})
+ self._init_values = self._values.copy() if self._values else None
def __getattr__(self, name):
if name == 'id':
@@ -471,8 +406,8 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
def __eq__(self, other):
if not isinstance(other, Model):
return NotImplemented
- if self.id is None or other.id is None:
- return False
+ elif self.id is None or other.id is None:
+ return id(self) == id(other)
return (self.__name__, self.id) == (other.__name__, other.id)
def __lt__(self, other):
@@ -481,11 +416,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
return self.id < other.id
def __ne__(self, other):
- if not isinstance(other, Model):
- return NotImplemented
- if self.id is None or other.id is None:
- return True
- return (self.__name__, self.id) != (other.__name__, other.id)
+ return not self == other
def __hash__(self):
return hash((self.__name__, self.id))
@@ -495,18 +426,27 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
@property
def _default_values(self):
- if self.id >= 0:
- return self.id
+ """Return the values not stored.
+ By default, the value of a field is its internal representation except:
+ - for Many2One and One2One field: the id
+ - for Reference field: the string model,id
+ - for Many2Many: the list of ids
+ - for One2Many: the list of `_default_values`
+ """
values = {}
if self._values:
for fname, value in self._values.iteritems():
field = self._fields[fname]
- if isinstance(field, fields.Reference):
- if value is not None:
- value = str(value)
- elif isinstance(value, Model):
- value = value._default_values
- elif isinstance(value, list):
- value = [r._default_values for r in value]
+ if field._type in ('many2one', 'one2one', 'reference'):
+ if value:
+ if field._type == 'reference':
+ value = str(value)
+ else:
+ value = value.id
+ elif field._type in ('one2many', 'many2many'):
+ if field._type == 'one2many':
+ value = [r._default_values for r in value]
+ else:
+ value = [r.id for r in value]
values[fname] = value
return values
diff --git a/trytond/model/modelsingleton.py b/trytond/model/modelsingleton.py
index e1347f6..948a4ff 100644
--- a/trytond/model/modelsingleton.py
+++ b/trytond/model/modelsingleton.py
@@ -1,5 +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.
+# 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 ModelStorage
diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py
index 2fc90bb..71acbfe 100644
--- a/trytond/model/modelsql.py
+++ b/trytond/model/modelsql.py
@@ -1,11 +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.
+# 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 re
import datetime
from functools import reduce
from itertools import islice, izip, chain, ifilter
+from collections import OrderedDict
-from sql import Table, Column, Literal, Desc, Asc, Expression, Flavor
+from sql import Table, Column, Literal, Desc, Asc, Expression, Flavor, Null
from sql.functions import Now, Extract
from sql.conditionals import Coalesce
from sql.operators import Or, And, Operator
@@ -15,12 +16,15 @@ from trytond.model import ModelStorage, ModelView
from trytond.model import fields
from trytond import backend
from trytond.tools import reduce_ids, grouped_slice
-from trytond.const import OPERATORS, RECORD_CACHE_SIZE
+from trytond.const import OPERATORS
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
+
+from .modelstorage import cache_size
+
_RE_UNIQUE = re.compile('UNIQUE\s*\((.*)\)', re.I)
_RE_CHECK = re.compile('CHECK\s*\((.*)\)', re.I)
@@ -82,10 +86,9 @@ class ModelSQL(ModelStorage):
if field_name == 'id':
continue
default_fun = None
- try:
- sql_type = field.sql_type()
- except NotImplementedError:
+ if hasattr(field, 'set'):
continue
+ sql_type = field.sql_type()
if field_name in cls._defaults:
default_fun = cls._defaults[field_name]
@@ -295,8 +298,7 @@ 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'
+ def _restore_history(cls, ids, datetime, _before=False):
if not cls._history:
return
transaction = Transaction()
@@ -324,7 +326,11 @@ class ModelSQL(ModelStorage):
to_update = []
for id_ in ids:
column_datetime = Coalesce(history.write_date, history.create_date)
- hwhere = (column_datetime <= datetime) & (history.id == id_)
+ if not _before:
+ hwhere = (column_datetime <= datetime)
+ else:
+ hwhere = (column_datetime < datetime)
+ hwhere &= (history.id == id_)
horder = (column_datetime.desc, Column(history, '__id').desc)
cursor.execute(*history.select(*hcolumns,
where=hwhere, order_by=horder, limit=1))
@@ -353,6 +359,16 @@ class ModelSQL(ModelStorage):
cls.__insert_history(to_update)
@classmethod
+ def restore_history(cls, ids, datetime):
+ 'Restore record ids from history at the date time'
+ cls._restore_history(ids, datetime)
+
+ @classmethod
+ def restore_history_before(cls, ids, datetime):
+ 'Restore record ids from history before the date time'
+ cls._restore_history(ids, datetime, _before=True)
+
+ @classmethod
def __check_timestamp(cls, ids):
transaction = Transaction()
cursor = transaction.cursor
@@ -488,7 +504,7 @@ class ModelSQL(ModelStorage):
cls.__insert_history(new_ids)
records = cls.browse(new_ids)
- for sub_records in grouped_slice(records, RECORD_CACHE_SIZE):
+ for sub_records in grouped_slice(records, cache_size()):
cls._validate(sub_records)
field_names = cls._fields.keys()
@@ -552,8 +568,9 @@ class ModelSQL(ModelStorage):
columns = []
for f in fields_names + fields_related.keys() + datetime_fields:
- if (f in cls._fields and not hasattr(cls._fields[f], 'set')):
- columns.append(Column(table, f).as_(f))
+ field = cls._fields.get(f)
+ if field and not hasattr(field, 'set'):
+ columns.append(field.sql_column(table).as_(f))
elif f == '_timestamp' and not table_query:
sql_type = fields.Char('timestamp').sql_type().base
columns.append(Extract('EPOCH',
@@ -594,7 +611,8 @@ class ModelSQL(ModelStorage):
result = [{'id': x} for x in ids]
for column in columns:
- field = column.output_name
+ # Split the output name to remove SQLite type detection
+ field = column.output_name.split()[0]
if field == '_timestamp':
continue
if (getattr(cls._fields[field], 'translate', False)
@@ -736,7 +754,9 @@ class ModelSQL(ModelStorage):
Config = pool.get('ir.configuration')
assert not len(args) % 2
- all_records = sum(((records, values) + args)[0:None:2], [])
+ # Remove possible duplicates from all records
+ all_records = list(OrderedDict.fromkeys(
+ sum(((records, values) + args)[0:None:2], [])))
all_ids = [r.id for r in all_records]
all_field_names = set()
@@ -823,7 +843,7 @@ class ModelSQL(ModelStorage):
field.set(cls, fname, *fargs)
cls.__insert_history(all_ids)
- for sub_records in grouped_slice(all_records, RECORD_CACHE_SIZE):
+ for sub_records in grouped_slice(all_records, cache_size()):
cls._validate(sub_records, field_names=all_field_names)
cls.trigger_write(trigger_eligibles)
@@ -858,7 +878,7 @@ class ModelSQL(ModelStorage):
and field.left and field.right):
tree_ids[fname] = []
for sub_ids in grouped_slice(ids):
- where = reduce_ids(Column(table, fname), sub_ids)
+ where = reduce_ids(field.sql_column(table), sub_ids)
cursor.execute(*table.select(table.id, where=where))
tree_ids[fname] += [x[0] for x in cursor.fetchall()]
@@ -980,10 +1000,11 @@ class ModelSQL(ModelStorage):
}
if order is None or order is False:
order = cls._order
- for fname, otype in order:
+ for oexpr, otype in order:
+ fname, _, extra_expr = oexpr.partition('.')
field = cls._fields[fname]
Order = order_types[otype.upper()]
- forder = field.convert_order(fname, tables, cls)
+ forder = field.convert_order(oexpr, tables, cls)
order_by.extend((Order(o) for o in forder))
main_table, _ = tables[None]
@@ -1020,12 +1041,12 @@ class ModelSQL(ModelStorage):
main_table.create_date).as_('_datetime'))
columns.append(Column(main_table, '__id'))
if not query:
- columns += [Column(main_table, name).as_(name)
- for name, field in cls._fields.iteritems()
- if not hasattr(field, 'get')
- and name != 'id'
- and not getattr(field, 'translate', False)
- and field.loading == 'eager']
+ columns += [f.sql_column(main_table).as_(n)
+ for n, f in cls._fields.iteritems()
+ if not hasattr(f, 'get')
+ and n != 'id'
+ and not getattr(f, 'translate', False)
+ and f.loading == 'eager']
if not cls.table_query():
sql_type = fields.Char('timestamp').sql_type().base
columns += [Extract('EPOCH',
@@ -1040,7 +1061,7 @@ class ModelSQL(ModelStorage):
rows = cursor.dictfetchmany(cursor.IN_MAX)
cache = cursor.get_cache()
if cls.__name__ not in cache:
- cache[cls.__name__] = LRUDict(RECORD_CACHE_SIZE)
+ cache[cls.__name__] = LRUDict(cache_size())
delete_records = transaction.delete_records.setdefault(cls.__name__,
set())
@@ -1065,8 +1086,8 @@ class ModelSQL(ModelStorage):
where = reduce_ids(history.id, sub_ids)
cursor.execute(*history.select(history.id, history.write_date,
where=where
- & (history.write_date != None)
- & (history.create_date == None)
+ & (history.write_date != Null)
+ & (history.create_date == Null)
& (history.write_date
<= transaction.context['_datetime'])))
for deleted_id, delete_date in cursor.fetchall():
@@ -1233,7 +1254,7 @@ class ModelSQL(ModelStorage):
old_left, old_right, parent_id = fetchone
if old_left == old_right == 0:
cursor.execute(*table.select(Max(right),
- where=field == None))
+ where=field == Null))
old_left, = cursor.fetchone()
old_left += 1
old_right = old_left + 1
@@ -1248,7 +1269,7 @@ class ModelSQL(ModelStorage):
cursor.execute(*table.select(right, where=table.id == parent_id))
parent_right = cursor.fetchone()[0]
else:
- cursor.execute(*table.select(Max(right), where=field == None))
+ cursor.execute(*table.select(Max(right), where=field == Null))
fetchone = cursor.fetchone()
if fetchone:
parent_right = fetchone[0] + 1
diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py
index 5831c9d..45eb7b3 100644
--- a/trytond/model/modelstorage.py
+++ b/trytond/model/modelstorage.py
@@ -1,5 +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.
+# 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 datetime
import time
@@ -18,17 +18,25 @@ from collections import defaultdict
from trytond.model import Model
from trytond.model import fields
-from trytond.tools import reduce_domain, memoize
+from trytond.tools import reduce_domain, memoize, is_instance_method, \
+ grouped_slice
from trytond.pyson import PYSONEncoder, PYSONDecoder, PYSON
-from trytond.const import OPERATORS, RECORD_CACHE_SIZE, BROWSE_FIELD_TRESHOLD
+from trytond.const import OPERATORS
+from trytond.config import config
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.cache import LRUDict, freeze
from trytond import backend
from trytond.rpc import RPC
from .modelview import ModelView
+from .descriptors import dualmethod
-__all__ = ['ModelStorage']
+__all__ = ['ModelStorage', 'EvalEnvironment']
+
+
+def cache_size():
+ return Transaction().context.get('_record_cache_size',
+ config.getint('cache', 'record'))
class ModelStorage(Model):
@@ -358,7 +366,10 @@ class ModelStorage(Model):
fields_names = cls._fields.keys()
if 'id' not in fields_names:
fields_names.append('id')
- return cls.read(map(int, records), fields_names)
+ rows = cls.read(map(int, records), fields_names)
+ index = {r.id: i for i, r in enumerate(records)}
+ rows.sort(key=lambda r: index[r['id']])
+ return rows
@classmethod
def _search_domain_active(cls, domain, active_test=True):
@@ -376,7 +387,7 @@ class ModelStorage(Model):
active_found = False
while i < len(domain):
arg = domain[i]
- #add test for xmlrpc that doesn't handle tuple
+ # add test for xmlrpc that doesn't handle tuple
if (isinstance(arg, tuple)
or (isinstance(arg, list)
and len(arg) > 2
@@ -419,13 +430,13 @@ class ModelStorage(Model):
@classmethod
def search_global(cls, text):
'''
- Yield tuples (id, rec_name, icon) for text
+ Yield tuples (record, name, icon) for text
'''
# TODO improve search clause
for record in cls.search([
('rec_name', 'ilike', '%%%s%%' % text),
]):
- yield record.id, record.rec_name, None
+ yield record, record.rec_name, None
@classmethod
def browse(cls, ids):
@@ -433,7 +444,7 @@ class ModelStorage(Model):
Return a list of instance for the ids
'''
ids = map(int, ids)
- local_cache = LRUDict(RECORD_CACHE_SIZE)
+ local_cache = LRUDict(cache_size())
return [cls(int(x), _ids=ids, _local_cache=local_cache) for x in ids]
@staticmethod
@@ -848,7 +859,7 @@ class ModelStorage(Model):
def call(name):
method = getattr(cls, name)
- if not hasattr(method, 'im_self') or method.im_self:
+ if not is_instance_method(cls, name):
return method(records)
else:
return all(method(r) for r in records)
@@ -859,13 +870,14 @@ class ModelStorage(Model):
if not call(field[0]):
cls.raise_user_error(field[1])
- if not 'res.user' in pool.object_name_list() \
- or Transaction().user == 0:
- ctx_pref = {
- }
- else:
- User = pool.get('res.user')
- ctx_pref = User.get_preferences(context_only=True)
+ ctx_pref = {}
+ if Transaction().user:
+ try:
+ User = pool.get('res.user')
+ except KeyError:
+ pass
+ else:
+ ctx_pref = User.get_preferences(context_only=True)
def is_pyson(test):
if isinstance(test, PYSON):
@@ -917,23 +929,26 @@ class ModelStorage(Model):
def validate_relation_domain(field, records, Relation, domain):
if field._type in ('many2one', 'one2many', 'many2many', 'one2one'):
- relations = []
+ relations = set()
for record in records:
if getattr(record, field.name):
if field._type in ('many2one', 'one2one'):
- relations.append(getattr(record, field.name))
+ relations.add(getattr(record, field.name))
else:
- relations.extend(getattr(record, field.name))
+ relations.update(getattr(record, field.name))
else:
- relations = records
+ # Cache alignment is not a problem
+ relations = set(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))
+ for sub_relations in grouped_slice(relations):
+ sub_relations = set(sub_relations)
+ finds = Relation.search(['AND',
+ [('id', 'in', [r.id for r in sub_relations])],
+ domain,
+ ])
+ if sub_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()
@@ -1053,10 +1068,10 @@ class ModelStorage(Model):
value, _ = value.split(',')
if not isinstance(field.selection, (tuple, list)):
sel_func = getattr(cls, field.selection)
- if field.selection_change_with:
- test = sel_func(record)
- else:
+ if not is_instance_method(cls, field.selection):
test = sel_func()
+ else:
+ test = sel_func(record)
test = set(dict(test))
# None and '' are equivalent
if '' in test or None in test:
@@ -1149,7 +1164,7 @@ class ModelStorage(Model):
if _local_cache is not None:
self._local_cache = _local_cache
else:
- self._local_cache = LRUDict(RECORD_CACHE_SIZE)
+ self._local_cache = LRUDict(cache_size())
self._local_cache.counter = Transaction().counter
super(ModelStorage, self).__init__(id, **kwargs)
@@ -1158,7 +1173,7 @@ class ModelStorage(Model):
def _cache(self):
cache = self._cursor_cache
if self.__name__ not in cache:
- cache[self.__name__] = LRUDict(RECORD_CACHE_SIZE)
+ cache[self.__name__] = LRUDict(cache_size())
return cache[self.__name__]
def __getattr__(self, name):
@@ -1201,7 +1216,7 @@ class ModelStorage(Model):
to_remove = set(x for x, y in fread_accesses.iteritems()
if not y and x != name)
- threshold = BROWSE_FIELD_TRESHOLD
+ threshold = config.getint('cache', 'field')
def not_cached(item):
fname, field = item
@@ -1232,7 +1247,10 @@ class ModelStorage(Model):
# add depends of field with context
for field in ffields.values():
if field.context:
- for context_field_name in field.depends:
+ eval_fields = fields.get_eval_fields(field.context)
+ for context_field_name in eval_fields:
+ if context_field_name in field.depends:
+ continue
context_field = self._fields.get(context_field_name)
if context_field not in ffields:
ffields[context_field_name] = context_field
@@ -1282,7 +1300,7 @@ class ModelStorage(Model):
with Transaction().set_context(**ctx):
key = (Model, freeze(ctx))
local_cache = model2cache.setdefault(key,
- LRUDict(RECORD_CACHE_SIZE))
+ LRUDict(cache_size()))
ids = model2ids.setdefault(key, [])
if field._type in ('many2one', 'one2one', 'reference'):
ids.append(value)
@@ -1390,26 +1408,48 @@ class ModelStorage(Model):
values[fname] = value
return values
- def save(self):
- save_values = self._save_values
- values = self._values
- self._values = None
- if save_values or self.id < 0:
- try:
- 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:
- self.id = self.create([save_values])[0].id
- finally:
- self._ids.append(self.id)
- else:
- self.write([self], save_values)
- except:
- self._values = values
- raise
+ @dualmethod
+ def save(cls, records):
+ if not records:
+ return
+ values = {}
+ save_values = {}
+ to_create = []
+ to_write = []
+ cursor = records[0]._cursor
+ user = records[0]._user
+ context = records[0]._context
+ for record in records:
+ assert cursor == record._cursor
+ assert user == record._user
+ assert context == record._context
+ save_values[record] = record._save_values
+ values[record] = record._values
+ record._values = None
+ if record.id is None or record.id < 0:
+ to_create.append(record)
+ elif save_values[record]:
+ to_write.append(record)
+ transaction = Transaction()
+ try:
+ with transaction.set_cursor(cursor), \
+ transaction.set_user(user), \
+ transaction.set_context(context):
+ if to_create:
+ news = cls.create([save_values[r] for r in to_create])
+ for record, new in izip(to_create, news):
+ record._ids.remove(record.id)
+ record.id = new.id
+ record._ids.append(record.id)
+ if to_write:
+ cls.write(*sum(
+ (([r], save_values[r]) for r in to_write), ()))
+ except:
+ for record in records:
+ record._values = values[record]
+ raise
+ for record in records:
+ record._init_values = None
class EvalEnvironment(dict):
diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py
index 5d1950f..5f74fbc 100644
--- a/trytond/model/modelview.py
+++ b/trytond/model/modelview.py
@@ -1,12 +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.
+# 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 lxml import etree
from functools import wraps
import copy
+import collections
-from trytond.model import Model
-from trytond.tools import safe_eval, ClassProperty
-from trytond.pyson import PYSONEncoder, CONTEXT
+from trytond.model import Model, fields
+from trytond.tools import ClassProperty, is_instance_method
+from trytond.pyson import PYSONDecoder, PYSONEncoder
from trytond.transaction import Transaction
from trytond.cache import Cache
from trytond.pool import Pool
@@ -42,26 +43,26 @@ def _inherit_apply(src, inherit):
index = parent.index(enext)
parent.insert(index, child)
else:
- parent.extend(element2.getchildren())
+ parent.extend(list(element2))
parent.remove(element)
elif pos == 'replace_attributes':
- child = element2.getchildren()[0]
+ child = element2[0]
for attr in child.attrib:
element.set(attr, child.get(attr))
elif pos == 'inside':
- element.extend(element2.getchildren())
+ element.extend(list(element2))
elif pos == 'after':
parent = element.getparent()
enext = element.getnext()
if enext is not None:
- for child in element2:
+ for child in list(element2):
index = parent.index(enext)
parent.insert(index, child)
else:
- parent.extend(element2.getchildren())
+ parent.extend(list(element2))
elif pos == 'before':
parent = element.getparent()
- for child in element2:
+ for child in list(element2):
index = parent.index(element)
parent.insert(index, child)
else:
@@ -74,6 +75,20 @@ def _inherit_apply(src, inherit):
return etree.tostring(tree_src, encoding='utf-8')
+def on_change(func):
+ @wraps(func)
+ def wrapper(self, *args, **kwargs):
+ result = func(self, *args, **kwargs)
+ assert result is None, func
+ return self
+ wrapper.on_change = True
+ return wrapper
+
+
+def on_change_result(record):
+ return record._changed_values
+
+
class ModelView(Model):
"""
Define a model with views in Tryton.
@@ -101,8 +116,102 @@ class ModelView(Model):
super(ModelView, cls).__setup__()
cls.__rpc__['fields_view_get'] = RPC()
cls.__rpc__['view_toolbar_get'] = RPC()
+ cls.__rpc__['on_change'] = RPC(instantiate=0)
+ cls.__rpc__['on_change_with'] = RPC(instantiate=0)
cls._buttons = {}
+ if hasattr(cls, '__depend_methods'):
+ cls.__depend_methods = cls.__depend_methods.copy()
+ else:
+ cls.__depend_methods = collections.defaultdict(set)
+
+ if hasattr(cls, '__change_buttons'):
+ cls.__change_buttons = cls.__change_buttons.copy()
+ else:
+ cls.__change_buttons = collections.defaultdict(set)
+
+ def setup_field(field, field_name):
+ 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)
+ if not getattr(cls, function_name, None):
+ continue
+ # Search depends on all parent class because field has been
+ # copied with the original definition
+ for parent_cls in cls.__mro__:
+ function = getattr(parent_cls, function_name, None)
+ if not function:
+ continue
+ 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
+ function = getattr(cls, function_name, None)
+ if (attribute == 'on_change'
+ and not getattr(function, 'on_change', None)):
+ # Decorate on_change to always return self
+ setattr(cls, function_name, on_change(function))
+
+ def setup_callable(function, name):
+ if hasattr(function, 'change'):
+ cls.__change_buttons[name] |= function.change
+
+ for name in dir(cls):
+ attr = getattr(cls, name)
+ if isinstance(attr, fields.Field):
+ setup_field(attr, name)
+ elif isinstance(attr, collections.Callable):
+ setup_callable(attr, name)
+
+ @classmethod
+ def __post_setup__(cls):
+ super(ModelView, cls).__post_setup__()
+
+ # Update __rpc__
+ for field_name, field in cls._fields.iteritems():
+ if isinstance(field, (fields.Selection, fields.Reference)) \
+ and not isinstance(field.selection, (list, tuple)) \
+ and field.selection not in cls.__rpc__:
+ instantiate = 0 if field.selection_change_with else None
+ cls.__rpc__.setdefault(field.selection,
+ RPC(instantiate=instantiate))
+
+ for attribute in ('on_change', 'on_change_with', 'autocomplete'):
+ function_name = '%s_%s' % (attribute, field_name)
+ if getattr(cls, function_name, None):
+ result = None
+ if attribute == 'on_change':
+ result = on_change_result
+ cls.__rpc__.setdefault(function_name,
+ RPC(instantiate=0, result=result))
+
+ for button in cls._buttons:
+ if not is_instance_method(cls, button):
+ cls.__rpc__.setdefault(button,
+ RPC(readonly=False, instantiate=0))
+ else:
+ cls.__rpc__.setdefault(button,
+ RPC(instantiate=0, result=on_change_result))
+
+ # 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))
+
@classmethod
def fields_view_get(cls, view_id=None, view_type='form'):
'''
@@ -110,8 +219,11 @@ class ModelView(Model):
If view_id is None the first one will be used of view_type.
The definition is a dictionary with keys:
- model: the model name
+ - type: the type of the view
+ - view_id: the id of the view
- arch: the xml description of the view
- fields: a dictionary with the definition of each field in the view
+ - field_childs: the name of the childs field for tree
'''
key = (cls.__name__, view_id, view_type)
result = cls._fields_view_get_cache.get(key)
@@ -193,8 +305,8 @@ class ModelView(Model):
raise_p = True
for view in views:
if view.domain:
- if not safe_eval(view.domain,
- {'context': Transaction().context}):
+ if not PYSONDecoder({'context': Transaction().context}
+ ).decode(view.domain):
continue
if not view.arch or not view.arch.strip():
continue
@@ -281,11 +393,21 @@ class ModelView(Model):
return value
@classmethod
+ def view_attributes(cls):
+ 'Return a list of xpath, attribute name and value'
+ return []
+
+ @classmethod
def _view_look_dom_arch(cls, tree, type, field_children=None):
pool = Pool()
ModelAccess = pool.get('ir.model.access')
FieldAccess = pool.get('ir.model.field.access')
+ encoder = PYSONEncoder()
+ for xpath, attribute, value in cls.view_attributes():
+ for element in tree.xpath(xpath):
+ element.set(attribute, encoder.encode(value))
+
fields_width = {}
tree_root = tree.getroottree().getroot()
@@ -307,15 +429,16 @@ class ModelView(Model):
# Remove field without read access
for field in fields_to_remove:
- for element in tree.xpath(
- '//field[@name="%s"] | //label[@name="%s"]'
- % (field, field)):
- if type == 'form':
- element.tag = 'label'
- element.attrib.clear()
- elif type == 'tree':
+ xpath = ('//field[@name="%(field)s"] | //label[@name="%(field)s"]'
+ ' | //page[@name="%(field)s"] | //group[@name="%(field)s"]'
+ ' | //separator[@name="%(field)s"]') % {'field': field}
+ for element in tree.xpath(xpath):
+ if type == 'tree' or element.tag == 'page':
parent = element.getparent()
parent.remove(element)
+ elif type == 'form':
+ element.tag = 'label'
+ element.attrib.clear()
if type == 'tree':
ViewTreeWidth = pool.get('ir.ui.view_tree_width')
@@ -424,15 +547,7 @@ class ModelView(Model):
if element.get('name') in fields_width:
element.set('width', str(fields_width[element.get('name')]))
- # convert attributes into pyson
encoder = PYSONEncoder()
- for attr in ('states', 'domain', 'spell', 'colors'):
- if (element.get(attr)
- # Avoid double evaluation from inherit with different model
- and '__' not in element.get(attr)):
- element.set(attr, encoder.encode(safe_eval(element.get(attr),
- CONTEXT)))
-
if element.tag == 'button':
button_name = element.attrib['name']
if button_name in cls._buttons:
@@ -446,6 +561,14 @@ class ModelView(Model):
states['readonly'] = True
element.set('states', encoder.encode(states))
+ change = cls.__change_buttons[button_name]
+ if change:
+ element.set('change', encoder.encode(list(change)))
+ if not is_instance_method(cls, button_name):
+ element.set('type', 'class')
+ else:
+ element.set('type', 'instance')
+
# translate view
if Transaction().language != 'en_US':
for attr in ('string', 'sum', 'confirm', 'help'):
@@ -516,3 +639,83 @@ class ModelView(Model):
return action_id
return wrapper
return decorator
+
+ @staticmethod
+ def button_change(*fields):
+ def decorator(func):
+ func = ModelView.button(func)
+ func = on_change(func)
+ func.change = set(fields)
+ return func
+ return decorator
+
+ def on_change(self, fieldnames):
+ for fieldname in sorted(fieldnames):
+ method = getattr(self, 'on_change_%s' % fieldname, None)
+ if method:
+ method()
+ # XXX remove backward compatibility
+ return [self._changed_values]
+
+ def on_change_with(self, fieldnames):
+ changes = {}
+ for fieldname in fieldnames:
+ method_name = 'on_change_with_%s' % fieldname
+ changes[fieldname] = getattr(self, method_name)()
+ return changes
+
+ @property
+ def _changed_values(self):
+ """Return the values changed since the instantiation.
+ By default, the value of a field is its internal representation except:
+ - for Many2One and One2One field: the id.
+ - for Reference field: the string model,id
+ - for Many2Many: the list of ids
+ - for One2Many: a dictionary composed of three keys:
+ - add: a list of tuple, the first element is the index where
+ the new line is added, the second element is
+ `_default_values`
+ - update: a list of dictionary of `_changed_values` including
+ the `id`
+ - remove: a list of ids
+ """
+ from .modelstorage import ModelStorage
+ changed = {}
+ init_values = self._init_values or {}
+ if not self._values:
+ return changed
+ for fname, value in self._values.iteritems():
+ field = self._fields[fname]
+ if (value == init_values.get(fname)
+ and field._type != 'one2many'):
+ continue
+ if field._type in ('many2one', 'one2one', 'reference'):
+ if value:
+ if isinstance(value, ModelStorage):
+ changed['%s.rec_name' % fname] = value.rec_name
+ if field._type == 'reference':
+ value = str(value)
+ else:
+ value = value.id
+ elif field._type == 'one2many':
+ targets = value
+ init_targets = list(init_values.get(fname, []))
+ value = collections.defaultdict(list)
+ value['remove'] = [t.id for t in init_targets if t.id]
+ for i, target in enumerate(targets):
+ if target.id in value['remove']:
+ value['remove'].remove(target.id)
+ target_changed = target._changed_values
+ if target_changed:
+ target_changed['id'] = target.id
+ value['update'].append(target_changed)
+ else:
+ value['add'].append((i, target._default_values))
+ if not value['remove']:
+ del value['remove']
+ if not value:
+ continue
+ elif field._type == 'many2many':
+ value = [r.id for r in value]
+ changed[fname] = value
+ return changed
diff --git a/trytond/model/workflow.py b/trytond/model/workflow.py
index ed346cd..5d79934 100644
--- a/trytond/model/workflow.py
+++ b/trytond/model/workflow.py
@@ -1,5 +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.
+# 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
diff --git a/trytond/modules/__init__.py b/trytond/modules/__init__.py
index 7c233b5..d196c9d 100644
--- a/trytond/modules/__init__.py
+++ b/trytond/modules/__init__.py
@@ -1,5 +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.
+# 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 sys
import itertools
@@ -19,6 +19,8 @@ from trytond.transaction import Transaction
from trytond.cache import Cache
import trytond.convert as convert
+logger = logging.getLogger(__name__)
+
ir_module = Table('ir_module_module')
ir_model_data = Table('ir_model_data')
@@ -165,7 +167,7 @@ def create_graph(module_list):
# add 'package' in the graph
all_deps = deps + [x for x in xdep if x in all_packages]
if reduce(lambda x, y: x and y in graph, all_deps, True):
- if not package in current:
+ if package not in current:
packages.pop(0)
continue
later.clear()
@@ -202,7 +204,6 @@ def load_module_graph(graph, pool, update=None, lang=None):
update = []
modules_todo = []
models_to_update_history = set()
- logger = logging.getLogger('modules')
cursor = Transaction().cursor
modules = [x.name for x in graph]
@@ -222,25 +223,25 @@ def load_module_graph(graph, pool, update=None, lang=None):
if package_state not in ('to install', 'to upgrade'):
if package_state == 'installed':
package_state = 'to upgrade'
- else:
+ elif package_state != 'to remove':
package_state = 'to install'
for child in package.childs:
module2state[child.name] = package_state
for type in classes.keys():
for cls in classes[type]:
- logger.info('%s:register %s' % (module, cls.__name__))
+ logger.info('%s:register %s', module, cls.__name__)
cls.__register__(module)
for model in classes['model']:
if hasattr(model, '_history'):
models_to_update_history.add(model.__name__)
- #Instanciate a new parser for the package:
+ # Instanciate a new parser for the package:
tryton_parser = convert.TrytondXmlHandler(pool=pool, module=module,
module_state=package_state)
for filename in package.info.get('xml', []):
filename = filename.replace('/', os.sep)
- logger.info('%s:loading %s' % (module, filename))
+ logger.info('%s:loading %s', module, filename)
# Feed the parser with xml content:
with tools.file_open(OPJ(module, filename)) as fp:
tryton_parser.parse_xmlstream(fp)
@@ -253,11 +254,13 @@ def load_module_graph(graph, pool, update=None, lang=None):
lang2 = os.path.splitext(os.path.basename(filename))[0]
if lang2 not in lang:
continue
- logger.info('%s:loading %s' % (module,
- filename[len(package.info['directory']) + 1:]))
+ logger.info('%s:loading %s', module,
+ filename[len(package.info['directory']) + 1:])
Translation = pool.get('ir.translation')
Translation.translation_import(lang2, module, filename)
+ if package_state == 'to remove':
+ continue
cursor.execute(*ir_module.select(ir_module.id,
where=(ir_module.name == package.name)))
try:
@@ -276,7 +279,7 @@ def load_module_graph(graph, pool, update=None, lang=None):
for model_name in models_to_update_history:
model = pool.get(model_name)
if model._history:
- logger.info('history:update %s' % model.__name__)
+ logger.info('history:update %s', model.__name__)
model._update_history_table()
# Vacuum :
@@ -293,6 +296,8 @@ def get_module_list():
for file in os.listdir(MODULES_PATH):
if file.startswith('.'):
continue
+ if file == '__pycache__':
+ continue
if os.path.isdir(OPJ(MODULES_PATH, file)):
module_list.add(file)
update_egg_modules()
@@ -316,11 +321,10 @@ def register_classes():
trytond.webdav.register()
import trytond.tests
trytond.tests.register()
- logger = logging.getLogger('modules')
for package in create_graph(get_module_list())[0]:
module = package.name
- logger.info('%s:registering classes' % module)
+ logger.info('%s:registering classes', module)
if module in ('ir', 'res', 'webdav', 'tests'):
MODULES.append(module)
@@ -364,9 +368,6 @@ def load_modules(database_name, pool, update=None, lang=None):
global res
cursor = Transaction().cursor
if update:
- # Migration from 2.2: workflow module removed
- cursor.execute(*ir_module.delete(
- where=(ir_module.name == 'workflow')))
cursor.execute(*ir_module.select(ir_module.name,
where=ir_module.state.in_(('installed', 'to install',
'to upgrade', 'to remove'))))
@@ -391,7 +392,7 @@ def load_modules(database_name, pool, update=None, lang=None):
fetchall = cursor.fetchall()
if fetchall:
for (mod_name,) in fetchall:
- #TODO check if ressource not updated by the user
+ # TODO check if ressource not updated by the user
cursor.execute(*ir_model_data.select(ir_model_data.model,
ir_model_data.db_id,
where=(ir_model_data.module == mod_name),
diff --git a/trytond/monitor.py b/trytond/monitor.py
index 128a0bf..866d594 100644
--- a/trytond/monitor.py
+++ b/trytond/monitor.py
@@ -1,5 +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.
+# 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 sys
import os
import subprocess
diff --git a/trytond/pool.py b/trytond/pool.py
index 65c6f49..7bdd4c4 100644
--- a/trytond/pool.py
+++ b/trytond/pool.py
@@ -1,5 +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.
+# 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 threading import RLock
import logging
from trytond.modules import load_modules, register_classes
@@ -8,13 +8,18 @@ import __builtin__
__all__ = ['Pool', 'PoolMeta', 'PoolBase']
+logger = logging.getLogger(__name__)
+
class PoolMeta(type):
def __new__(cls, name, bases, dct):
new = type.__new__(cls, name, bases, dct)
if '__name__' in dct:
- new.__name__ = dct['__name__']
+ try:
+ new.__name__ = dct['__name__']
+ except TypeError:
+ new.__name__ = dct['__name__'].encode('utf-8')
return new
@@ -134,7 +139,6 @@ class Pool(object):
Set update to proceed to update
lang is a list of language code to be updated
'''
- logger = logging.getLogger('pool')
with self._lock:
if not self._started:
self.start()
@@ -142,9 +146,9 @@ class Pool(object):
# Don't reset pool if already init and not to update
if not update and self._pool.get(self.database_name):
return
- logger.info('init pool for "%s"' % self.database_name)
+ logger.info('init pool for "%s"', self.database_name)
self._pool.setdefault(self.database_name, {})
- #Clean the _pool before loading modules
+ # Clean the _pool before loading modules
for type in self.classes.keys():
self._pool[self.database_name][type] = {}
restart = not load_modules(self.database_name, self, update=update,
@@ -183,20 +187,6 @@ class Pool(object):
with self._locks[self.database_name]:
self._pool[self.database_name][type][cls.__name__] = cls
- def object_name_list(self, type='model'):
- '''
- Return the object name list of a type
-
- :param type: the type
- :return: a list of name
- '''
- if type == '*':
- res = []
- for type in self.classes.keys():
- res += self._pool[self.database_name][type].keys()
- return res
- return self._pool[self.database_name][type].keys()
-
def iterobject(self, type='model'):
'''
Return an iterator over object name, object
diff --git a/trytond/protocols/__init__.py b/trytond/protocols/__init__.py
index c86640b..4effdfa 100644
--- a/trytond/protocols/__init__.py
+++ b/trytond/protocols/__init__.py
@@ -1,2 +1,2 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
diff --git a/trytond/protocols/common.py b/trytond/protocols/common.py
index 959b83a..c38ac17 100644
--- a/trytond/protocols/common.py
+++ b/trytond/protocols/common.py
@@ -1,5 +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.
+# 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 errno
import os
import socket
diff --git a/trytond/protocols/dispatcher.py b/trytond/protocols/dispatcher.py
index 05e3345..e22994c 100644
--- a/trytond/protocols/dispatcher.py
+++ b/trytond/protocols/dispatcher.py
@@ -1,9 +1,8 @@
# -*- 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.
+# 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 time
-import sys
import pydoc
from sql import Table
@@ -12,12 +11,12 @@ from trytond.pool import Pool
from trytond import security
from trytond import backend
from trytond.config import config
-from trytond.version import VERSION
+from trytond import __version__
from trytond.transaction import Transaction
from trytond.cache import Cache
from trytond.exceptions import UserError, UserWarning, NotLogged, \
ConcurrencyException
-from trytond.rpc import RPC
+from trytond.tools import is_instance_method
logger = logging.getLogger(__name__)
@@ -44,17 +43,17 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
Cache.clean(database_name)
Cache.resets(database_name)
msg = res and 'successful login' or 'bad login or password'
- logger.info('%s \'%s\' from %s:%d using %s on database \'%s\''
- % (msg, user, host, port, protocol, database_name))
+ logger.info('%s \'%s\' from %s:%d using %s on database \'%s\'',
+ msg, user, host, port, protocol, database_name)
return res or False
elif method == 'logout':
name = security.logout(database_name, user, session)
- logger.info(('logout \'%s\' from %s:%d '
- 'using %s on database \'%s\'')
- % (name, host, port, protocol, database_name))
+ logger.info('logout \'%s\' from %s:%d '
+ 'using %s on database \'%s\'',
+ name, host, port, protocol, database_name)
return True
elif method == 'version':
- return VERSION
+ return __version__
elif method == 'list_lang':
return [
('bg_BG', 'Български'),
@@ -98,7 +97,7 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
database = Database(database_name).connect()
database_list = Pool.database_list()
pool = Pool(database_name)
- if not database_name in database_list:
+ if database_name not in database_list:
pool.init()
if method == 'listMethods':
res = []
@@ -106,9 +105,6 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
for object_name, obj in pool.iterobject(type=type):
for method in obj.__rpc__:
res.append(type + '.' + object_name + '.' + method)
- if hasattr(obj, '_buttons'):
- for button in obj._buttons:
- res.append(type + '.' + object_name + '.' + button)
return res
elif method == 'methodSignature':
return 'signatures not supported'
@@ -132,7 +128,7 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
database_list = Pool.database_list()
pool = Pool(database_name)
- if not database_name in database_list:
+ if database_name not in database_list:
with Transaction().start(database_name, user,
readonly=True) as transaction:
pool.init()
@@ -140,15 +136,15 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
if method in obj.__rpc__:
rpc = obj.__rpc__[method]
- elif method in getattr(obj, '_buttons', {}):
- rpc = RPC(readonly=False, instantiate=0)
else:
raise UserError('Calling method %s on %s %s is not allowed!'
% (method, object_type, object_name))
- exception_message = ('Exception calling %s.%s.%s from %s@%s:%d/%s' %
- (object_type, object_name, method, user, host, port, database_name))
+ log_message = '%s.%s.%s(*%s, **%s) from %s@%s:%d/%s'
+ log_args = (object_type, object_name, method, args, kwargs,
+ user, host, port, database_name)
+ logger.info(log_message, *log_args)
for count in range(config.getint('database', 'retry'), -1, -1):
with Transaction().start(database_name, user,
readonly=rpc.readonly) as transaction:
@@ -157,7 +153,8 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
c_args, c_kwargs, transaction.context, transaction.timestamp \
= rpc.convert(obj, *args, **kwargs)
meth = getattr(obj, method)
- if not hasattr(meth, 'im_self') or meth.im_self:
+ if (rpc.instantiate is None
+ or not is_instance_method(obj, method)):
result = rpc.result(meth(*c_args, **c_kwargs))
else:
assert rpc.instantiate == 0
@@ -175,11 +172,11 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
continue
raise
except (NotLogged, ConcurrencyException, UserError, UserWarning):
- logger.debug(exception_message, exc_info=sys.exc_info())
+ logger.debug(log_message, *log_args, exc_info=True)
transaction.cursor.rollback()
raise
except Exception:
- logger.error(exception_message, exc_info=sys.exc_info())
+ logger.error(log_message, *log_args, exc_info=True)
transaction.cursor.rollback()
raise
Cache.resets(database_name)
@@ -189,10 +186,12 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
try:
Session.reset(session)
except DatabaseOperationalError:
+ logger.debug('Reset session failed', exc_info=True)
# Silently fail when reseting session
transaction.cursor.rollback()
else:
transaction.cursor.commit()
+ logger.debug('Result: %s', result)
return result
@@ -212,7 +211,7 @@ def create(database_name, password, lang, admin_password):
try:
with Transaction().start(None, 0, close=True, autocommit=True) \
- as transaction:
+ as transaction:
transaction.database.create(transaction.cursor, database_name)
transaction.cursor.commit()
@@ -244,11 +243,10 @@ def create(database_name, password, lang, admin_password):
transaction.cursor.commit()
res = True
except Exception:
- logger.error('CREATE DB: %s failed' % database_name,
- exc_info=sys.exc_info())
+ logger.error('CREATE DB: %s failed', database_name, exc_info=True)
raise
else:
- logger.info('CREATE DB: %s' % (database_name,))
+ logger.info('CREATE DB: %s', database_name)
return res
@@ -260,18 +258,18 @@ def drop(database_name, password):
time.sleep(1)
with Transaction().start(None, 0, close=True, autocommit=True) \
- as transaction:
+ as transaction:
cursor = transaction.cursor
try:
Database.drop(cursor, database_name)
cursor.commit()
except Exception:
- logger.error('DROP DB: %s failed' % database_name,
- exc_info=sys.exc_info())
+ logger.error('DROP DB: %s failed', database_name, exc_info=True)
raise
else:
- logger.info('DROP DB: %s' % (database_name))
+ logger.info('DROP DB: %s', database_name)
Pool.stop(database_name)
+ Cache.drop(database_name)
return True
@@ -283,8 +281,11 @@ def dump(database_name, password):
time.sleep(1)
data = Database.dump(database_name)
- logger.info('DUMP DB: %s' % (database_name))
- return buffer(data)
+ logger.info('DUMP DB: %s', database_name)
+ if bytes == str:
+ return bytearray(data)
+ else:
+ return bytes(data)
def restore(database_name, password, data, update=False):
@@ -298,7 +299,7 @@ def restore(database_name, password, data, update=False):
except Exception:
pass
Database.restore(database_name, data)
- logger.info('RESTORE DB: %s' % (database_name))
+ logger.info('RESTORE DB: %s', database_name)
if update:
with Transaction().start(database_name, 0) as transaction:
cursor = transaction.cursor
diff --git a/trytond/protocols/jsonrpc.py b/trytond/protocols/jsonrpc.py
index 088d7da..df9f46e 100644
--- a/trytond/protocols/jsonrpc.py
+++ b/trytond/protocols/jsonrpc.py
@@ -1,5 +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.
+# 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.protocols.sslsocket import SSLSocket
from trytond.protocols.dispatcher import dispatch
from trytond.config import config
@@ -55,8 +55,14 @@ JSONDecoder.register('date',
JSONDecoder.register('time',
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
dct['microsecond']))
-JSONDecoder.register('buffer', lambda dct:
- buffer(base64.decodestring(dct['base64'])))
+JSONDecoder.register('timedelta',
+ lambda dct: datetime.timedelta(seconds=dct['seconds']))
+
+
+def _bytes_decoder(dct):
+ cast = bytearray if bytes == str else bytes
+ return cast(base64.decodestring(dct['base64']))
+JSONDecoder.register('bytes', _bytes_decoder)
JSONDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
@@ -105,11 +111,17 @@ JSONEncoder.register(datetime.time,
'second': o.second,
'microsecond': o.microsecond,
})
-JSONEncoder.register(buffer,
+JSONEncoder.register(datetime.timedelta,
lambda o: {
- '__class__': 'buffer',
- 'base64': base64.encodestring(o),
+ '__class__': 'timedelta',
+ 'seconds': o.total_seconds(),
})
+_bytes_encoder = lambda o: {
+ '__class__': 'bytes',
+ 'base64': base64.encodestring(o),
+ }
+JSONEncoder.register(bytes, _bytes_encoder)
+JSONEncoder.register(bytearray, _bytes_encoder)
JSONEncoder.register(Decimal,
lambda o: {
'__class__': 'Decimal',
@@ -145,7 +157,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
response = {'id': req_id}
try:
- #generate response
+ # generate response
if dispatch_method is not None:
response['result'] = dispatch_method(method, params)
else:
diff --git a/trytond/protocols/sslsocket.py b/trytond/protocols/sslsocket.py
index 9c18a26..4d12720 100644
--- a/trytond/protocols/sslsocket.py
+++ b/trytond/protocols/sslsocket.py
@@ -1,5 +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.
+# 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.config import config
diff --git a/trytond/protocols/webdav.py b/trytond/protocols/webdav.py
index 584a7c3..23d4056 100644
--- a/trytond/protocols/webdav.py
+++ b/trytond/protocols/webdav.py
@@ -1,12 +1,11 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 SocketServer
import socket
import BaseHTTPServer
import urlparse
import time
import urllib
-import sys
import logging
from threading import local
import xml.dom.minidom
@@ -21,7 +20,7 @@ from pywebdav.lib.davcmd import copyone, copytree, moveone, movetree, \
from trytond.protocols.sslsocket import SSLSocket
from trytond.protocols.common import daemon
from trytond.security import login
-from trytond.version import PACKAGE, VERSION, WEBSITE
+from trytond import __version__
from trytond.tools.misc import LocalDict
from trytond import backend
from trytond.pool import Pool
@@ -134,9 +133,9 @@ class TrytonDAVInterface(iface.dav_interface):
if isinstance(exception, (NotLogged, ConcurrencyException, UserError,
UserWarning, DAV_Error, DAV_NotFound, DAV_Secret,
DAV_Forbidden)):
- logger.debug('Exception', exc_info=sys.exc_info())
+ logger.debug('Exception %s', exception, exc_info=True)
else:
- logger.error('Exception', exc_info=sys.exc_info())
+ logger.error('Exception %s', exception, exc_info=True)
@staticmethod
def get_dburi(uri):
@@ -199,8 +198,7 @@ class TrytonDAVInterface(iface.dav_interface):
res += '<head>'
res += ('<meta http-equiv="Content-Type" content="text/html; '
'charset=utf-8">')
- res += ('<title>%s - WebDAV - %s</title>'
- % (PACKAGE, dbname or 'root'))
+ res += '<title>Tryton - WebDAV - %s</title>' % dbname or 'root'
res += '</head>'
res += '<body>'
res += '<h2>Collection: %s</h2>' % (get_urifilename(uri) or '/')
@@ -220,8 +218,8 @@ class TrytonDAVInterface(iface.dav_interface):
% (quote_uri(child), get_urifilename(child)))
res += '</ul>'
res += '<hr noshade>'
- res += ('<em>Powered by <a href="%s">%s</a> version %s</em>'
- % (quote_uri(WEBSITE), PACKAGE, VERSION))
+ res += ('<em>Powered by <a href="http://www.tryton.org/">'
+ 'Tryton</a> version %s</em>' % __version__)
res += '</body>'
res += '</html>'
return res
@@ -574,7 +572,7 @@ class WebDAVAuthRequestHandler(WebDAVServer.DAVRequestHandler):
with Transaction().start(dbname, 0) as transaction:
database_list = Pool.database_list()
pool = Pool(dbname)
- if not dbname in database_list:
+ if dbname not in database_list:
pool.init()
Share = pool.get('webdav.share')
user = Share.get_login(key, command, path)
diff --git a/trytond/protocols/xmlrpc.py b/trytond/protocols/xmlrpc.py
index 7b48c53..9febacf 100644
--- a/trytond/protocols/xmlrpc.py
+++ b/trytond/protocols/xmlrpc.py
@@ -1,5 +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.
+# 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.protocols.sslsocket import SSLSocket
from trytond.protocols.dispatcher import dispatch
from trytond.protocols.common import daemon, RegisterHandlerMixin
@@ -10,7 +10,6 @@ import SimpleXMLRPCServer
import SocketServer
import xmlrpclib
import socket
-import sys
import base64
import datetime
from types import DictType
@@ -29,7 +28,7 @@ def dump_decimal(self, value, write):
self.dump_struct(value, write)
-def dump_buffer(self, value, write):
+def dump_bytes(self, value, write):
self.write = write
value = xmlrpclib.Binary(value)
value.encode(self)
@@ -54,12 +53,22 @@ def dump_time(self, value, write):
}
self.dump_struct(value, write)
+
+def dump_timedelta(self, value, write):
+ value = {'__class__': 'timedelta',
+ 'seconds': value.total_seconds(),
+ }
+ self.dump_struct(value, write)
+
xmlrpclib.Marshaller.dispatch[Decimal] = dump_decimal
xmlrpclib.Marshaller.dispatch[type(None)] = \
lambda self, value, write: write("<value><nil/></value>")
xmlrpclib.Marshaller.dispatch[datetime.date] = dump_date
xmlrpclib.Marshaller.dispatch[datetime.time] = dump_time
-xmlrpclib.Marshaller.dispatch[buffer] = dump_buffer
+xmlrpclib.Marshaller.dispatch[datetime.timedelta] = dump_timedelta
+if bytes != str:
+ xmlrpclib.Marshaller.dispatch[bytes] = dump_bytes
+xmlrpclib.Marshaller.dispatch[bytearray] = dump_bytes
def dump_struct(self, value, write, escape=xmlrpclib.escape):
@@ -94,6 +103,8 @@ XMLRPCDecoder.register('date',
XMLRPCDecoder.register('time',
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
dct['microsecond']))
+XMLRPCDecoder.register('timedelta',
+ lambda dct: datetime.timedelta(seconds=dct['seconds']))
XMLRPCDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
@@ -122,7 +133,8 @@ xmlrpclib.Unmarshaller.dispatch["dateTime.iso8601"] = _end_dateTime
def _end_base64(self, data):
value = xmlrpclib.Binary()
value.decode(data)
- self.append(buffer(value.data))
+ cast = bytearray if bytes == str else bytes
+ self.append(cast(value.data))
self._value = 0
xmlrpclib.Unmarshaller.dispatch['base64'] = _end_base64
@@ -152,14 +164,14 @@ class GenericXMLRPCRequestHandler:
return dispatch(host, port, 'XML-RPC', database_name, user,
session, object_type, object_name, method, *params)
except (NotLogged, ConcurrencyException), exception:
- logger.debug(exception_message, exc_info=sys.exc_info())
+ logger.debug(exception_message, exc_info=True)
raise xmlrpclib.Fault(exception.code, str(exception))
except (UserError, UserWarning), exception:
- logger.debug(exception_message, exc_info=sys.exc_info())
+ logger.debug(exception_message, exc_info=True)
error, description = exception.args
raise xmlrpclib.Fault(exception.code, str(exception))
except Exception, exception:
- logger.error(exception_message, exc_info=sys.exc_info())
+ logger.error(exception_message, exc_info=True)
raise xmlrpclib.Fault(255, str(exception))
finally:
security.logout(database_name, user, session)
diff --git a/trytond/pyson.py b/trytond/pyson.py
index 28979fd..c455588 100644
--- a/trytond/pyson.py
+++ b/trytond/pyson.py
@@ -1,12 +1,28 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
try:
import simplejson as json
except ImportError:
import json
import datetime
from dateutil.relativedelta import relativedelta
-from functools import reduce
+from functools import reduce, wraps
+
+
+def reduced_type(types):
+ types = types.copy()
+ for k, r in [(long, int), (str, basestring), (unicode, basestring)]:
+ if k in types:
+ types.remove(k)
+ types.add(r)
+ return types
+
+
+def reduce_type(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ return reduced_type(func(*args, **kwargs))
+ return wrapper
class PYSON(object):
@@ -79,6 +95,14 @@ class PYSON(object):
def contains(self, k):
return In(k, self)
+ def __repr__(self):
+ klass = self.__class__.__name__
+ return '%s(%s)' % (klass, ', '.join(map(repr, self.__repr_params__)))
+
+ @property
+ def __repr_params__(self):
+ return NotImplementedError
+
class PYSONEncoder(json.JSONEncoder):
@@ -97,24 +121,34 @@ class PYSONEncoder(json.JSONEncoder):
class PYSONDecoder(json.JSONDecoder):
- def __init__(self, context=None):
+ def __init__(self, context=None, noeval=False):
self.__context = context or {}
+ self.noeval = noeval
super(PYSONDecoder, self).__init__(object_hook=self._object_hook)
def _object_hook(self, dct):
if '__class__' in dct:
- klass = globals().get(dct['__class__'])
- if klass and hasattr(klass, 'eval'):
- return klass.eval(dct, self.__context)
+ klass = CONTEXT.get(dct['__class__'])
+ if klass:
+ if not self.noeval:
+ return klass.eval(dct, self.__context)
+ else:
+ dct = dct.copy()
+ del dct['__class__']
+ return klass(**dct)
return dct
class Eval(PYSON):
- def __init__(self, value, default=''):
+ def __init__(self, v, d=''):
super(Eval, self).__init__()
- self._value = value
- self._default = default
+ self._value = v
+ self._default = d
+
+ @property
+ def __repr_params__(self):
+ return self._value, self._default
def pyson(self):
return {
@@ -123,6 +157,7 @@ class Eval(PYSON):
'd': self._default,
}
+ @reduce_type
def types(self):
if isinstance(self._default, PYSON):
return self._default.types()
@@ -136,13 +171,17 @@ class Eval(PYSON):
class Not(PYSON):
- def __init__(self, value):
+ def __init__(self, v):
super(Not, self).__init__()
- if isinstance(value, PYSON):
- assert value.types() == set([bool]), 'value must be boolean'
+ if isinstance(v, PYSON):
+ assert v.types() == set([bool]), 'value must be boolean'
else:
- assert isinstance(value, bool), 'value must be boolean'
- self._value = value
+ assert isinstance(v, bool), 'value must be boolean'
+ self._value = v
+
+ @property
+ def __repr_params__(self):
+ return (self._value,)
def pyson(self):
return {
@@ -160,9 +199,13 @@ class Not(PYSON):
class Bool(PYSON):
- def __init__(self, value):
+ def __init__(self, v):
super(Bool, self).__init__()
- self._value = value
+ self._value = v
+
+ @property
+ def __repr_params__(self):
+ return (self._value,)
def pyson(self):
return {
@@ -180,8 +223,9 @@ class Bool(PYSON):
class And(PYSON):
- def __init__(self, *statements):
+ def __init__(self, *statements, **kwargs):
super(And, self).__init__()
+ statements = list(statements) + kwargs.get('s', [])
for statement in statements:
if isinstance(statement, PYSON):
assert statement.types() == set([bool]), \
@@ -190,7 +234,11 @@ class And(PYSON):
assert isinstance(statement, bool), \
'statement must be boolean'
assert len(statements) >= 2, 'must have at least 2 statements'
- self._statements = list(statements)
+ self._statements = statements
+
+ @property
+ def __repr_params__(self):
+ return tuple(self._statements)
def pyson(self):
return {
@@ -220,20 +268,25 @@ class Or(And):
class Equal(PYSON):
- def __init__(self, statement1, statement2):
+ def __init__(self, s1, s2):
+ statement1, statement2 = s1, s2
super(Equal, self).__init__()
if isinstance(statement1, PYSON):
types1 = statement1.types()
else:
- types1 = set([type(statement1)])
+ types1 = reduced_type(set([type(s1)]))
if isinstance(statement2, PYSON):
types2 = statement2.types()
else:
- types2 = set([type(statement2)])
+ types2 = reduced_type(set([type(s2)]))
assert types1 == types2, 'statements must have the same type'
self._statement1 = statement1
self._statement2 = statement2
+ @property
+ def __repr_params__(self):
+ return (self._statement1, self._statement2)
+
def pyson(self):
return {
'__class__': 'Equal',
@@ -251,7 +304,8 @@ class Equal(PYSON):
class Greater(PYSON):
- def __init__(self, statement1, statement2, equal=False):
+ def __init__(self, s1, s2, e=False):
+ statement1, statement2, equal = s1, s2, e
super(Greater, self).__init__()
for i in (statement1, statement2):
if isinstance(i, PYSON):
@@ -268,6 +322,10 @@ class Greater(PYSON):
self._statement2 = statement2
self._equal = equal
+ @property
+ def __repr_params__(self):
+ return (self._statement1, self._statement2, self._equal)
+
def pyson(self):
return {
'__class__': 'Greater',
@@ -314,7 +372,8 @@ class Less(Greater):
class If(PYSON):
- def __init__(self, condition, then_statement, else_statement=None):
+ def __init__(self, c, t, e=None):
+ condition, then_statement, else_statement = c, t, e
super(If, self).__init__()
if isinstance(condition, PYSON):
assert condition.types() == set([bool]), \
@@ -324,17 +383,21 @@ class If(PYSON):
if isinstance(then_statement, PYSON):
then_types = then_statement.types()
else:
- then_types = set([type(then_statement)])
+ then_types = reduced_type(set([type(then_statement)]))
if isinstance(else_statement, PYSON):
- assert then_types == else_statement.types(), \
- 'then and else statements must be the same type'
+ else_types = else_statement.types()
else:
- assert then_types == set([type(else_statement)]), \
- 'then and else statements must be the same type'
+ else_types = reduced_type(set([type(else_statement)]))
+ assert then_types == else_types, \
+ 'then and else statements must be the same type'
self._condition = condition
self._then_statement = then_statement
self._else_statement = else_statement
+ @property
+ def __repr_params__(self):
+ return (self._condition, self._then_statement, self._else_statement)
+
def pyson(self):
return {
'__class__': 'If',
@@ -343,6 +406,7 @@ class If(PYSON):
'e': self._else_statement,
}
+ @reduce_type
def types(self):
if isinstance(self._then_statement, PYSON):
return self._then_statement.types()
@@ -359,7 +423,8 @@ class If(PYSON):
class Get(PYSON):
- def __init__(self, obj, key, default=''):
+ def __init__(self, v, k, d=''):
+ obj, key, default = v, k, d
super(Get, self).__init__()
if isinstance(obj, PYSON):
assert obj.types() == set([dict]), 'obj must be a dict'
@@ -367,12 +432,16 @@ class Get(PYSON):
assert isinstance(obj, dict), 'obj must be a dict'
self._obj = obj
if isinstance(key, PYSON):
- assert key.types() == set([str]), 'key must be a string'
+ assert key.types() == set([basestring]), 'key must be a string'
else:
- assert type(key) == str, 'key must be a string'
+ assert isinstance(key, basestring), 'key must be a string'
self._key = key
self._default = default
+ @property
+ def __repr_params__(self):
+ return (self._obj, self._key, self._default)
+
def pyson(self):
return {
'__class__': 'Get',
@@ -381,6 +450,7 @@ class Get(PYSON):
'd': self._default,
}
+ @reduce_type
def types(self):
if isinstance(self._default, PYSON):
return self._default.types()
@@ -394,26 +464,31 @@ class Get(PYSON):
class In(PYSON):
- def __init__(self, key, obj):
+ def __init__(self, k, v):
+ key, obj = k, v
super(In, self).__init__()
if isinstance(key, PYSON):
- assert key.types().issubset(set([str, int, long])), \
+ assert key.types().issubset(set([basestring, int])), \
'key must be a string or an integer or a long'
else:
- assert type(key) in [str, int, long], \
+ assert isinstance(key, (basestring, int, long)), \
'key must be a string or an integer or a long'
if isinstance(obj, PYSON):
assert obj.types().issubset(set([dict, list])), \
'obj must be a dict or a list'
if obj.types() == set([dict]):
- assert type(key) == str, 'key must be a string'
+ assert isinstance(key, basestring), 'key must be a string'
else:
- assert type(obj) in [dict, list]
- if type(obj) == dict:
- assert type(key) == str, 'key must be a string'
+ assert isinstance(obj, (dict, list))
+ if isinstance(obj, dict):
+ assert isinstance(key, basestring), 'key must be a string'
self._key = key
self._obj = obj
+ @property
+ def __repr_params__(self):
+ return (self._key, self._obj)
+
def pyson(self):
return {
'__class__': 'In',
@@ -432,7 +507,13 @@ class In(PYSON):
class Date(PYSON):
def __init__(self, year=None, month=None, day=None,
- delta_years=0, delta_months=0, delta_days=0):
+ delta_years=0, delta_months=0, delta_days=0, **kwargs):
+ year = kwargs.get('y', year)
+ month = kwargs.get('M', month)
+ day = kwargs.get('d', day)
+ delta_years = kwargs.get('dy', delta_years)
+ delta_months = kwargs.get('dM', delta_months)
+ delta_days = kwargs.get('dd', delta_days)
super(Date, self).__init__()
for i in (year, month, day, delta_years, delta_months, delta_days):
if isinstance(i, PYSON):
@@ -448,6 +529,11 @@ class Date(PYSON):
self._delta_months = delta_months
self._delta_days = delta_days
+ @property
+ def __repr_params__(self):
+ return (self._year, self._month, self._day,
+ self._delta_years, self._delta_months, self._delta_days)
+
def pyson(self):
return {
'__class__': 'Date',
@@ -480,14 +566,22 @@ class DateTime(Date):
hour=None, minute=None, second=None, microsecond=None,
delta_years=0, delta_months=0, delta_days=0,
delta_hours=0, delta_minutes=0, delta_seconds=0,
- delta_microseconds=0):
+ delta_microseconds=0, **kwargs):
+ hour = kwargs.get('h', hour)
+ minute = kwargs.get('m', minute)
+ second = kwargs.get('s', second)
+ microsecond = kwargs.get('ms', microsecond)
+ delta_hours = kwargs.get('dh', delta_hours)
+ delta_minutes = kwargs.get('dm', delta_minutes)
+ delta_seconds = kwargs.get('ds', delta_seconds)
+ delta_microseconds = kwargs.get('dms', delta_microseconds)
super(DateTime, self).__init__(year=year, month=month, day=day,
delta_years=delta_years, delta_months=delta_months,
- delta_days=delta_days)
+ delta_days=delta_days, **kwargs)
for i in (hour, minute, second, microsecond,
delta_hours, delta_minutes, delta_seconds, delta_microseconds):
if isinstance(i, PYSON):
- assert i.types() == set([int, long, type(None)]), \
+ assert i.types() == set([int, type(None)]), \
'%s must be an integer or None' % (i,)
else:
assert isinstance(i, (int, long, type(None))), \
@@ -501,6 +595,15 @@ class DateTime(Date):
self._delta_seconds = delta_seconds
self._delta_microseconds = delta_microseconds
+ @property
+ def __repr_params__(self):
+ date_params = super(DateTime, self).__repr_params__
+ return (date_params[:3]
+ + (self._hour, self._minute, self._second, self._microsecond)
+ + date_params[3:]
+ + (self._delta_hours, self._delta_minutes, self._delta_seconds,
+ self._delta_microseconds))
+
def pyson(self):
res = super(DateTime, self).pyson()
res['__class__'] = 'DateTime'
@@ -539,15 +642,19 @@ class DateTime(Date):
class Len(PYSON):
- def __init__(self, value):
+ def __init__(self, v):
super(Len, self).__init__()
- if isinstance(value, PYSON):
- assert value.types().issubset(set([dict, list, str])), \
+ if isinstance(v, PYSON):
+ assert v.types().issubset(set([dict, list, basestring])), \
'value must be a dict or a list or a string'
else:
- assert type(value) in [dict, list, str], \
+ assert isinstance(v, (dict, list, basestring)), \
'value must be a dict or list or a string'
- self._value = value
+ self._value = v
+
+ @property
+ def __repr_params__(self):
+ return (self._value,)
def pyson(self):
return {
@@ -556,7 +663,7 @@ class Len(PYSON):
}
def types(self):
- return set([int, long])
+ return set([int])
@staticmethod
def eval(dct, context):
@@ -571,6 +678,10 @@ class Id(PYSON):
self._module = module
self._fs_id = fs_id
+ @property
+ def __repr_params__(self):
+ return (self._module, self._fs_id)
+
def pyson(self):
from trytond.pool import Pool
ModelData = Pool().get('ir.model.data')
diff --git a/trytond/report/__init__.py b/trytond/report/__init__.py
index e7e310a..15e5214 100644
--- a/trytond/report/__init__.py
+++ b/trytond/report/__init__.py
@@ -1,3 +1,3 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 .report import *
diff --git a/trytond/report/report.py b/trytond/report/report.py
index b0a3ef1..9dc99e9 100644
--- a/trytond/report/report.py
+++ b/trytond/report/report.py
@@ -1,14 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
try:
import cStringIO as StringIO
except ImportError:
import StringIO
import zipfile
-import time
import os
import datetime
-import inspect
import tempfile
import warnings
import subprocess
@@ -130,20 +128,27 @@ class Report(URLMixin, PoolBase):
'''
pool = Pool()
ActionReport = pool.get('ir.action.report')
- action_reports = ActionReport.search([
- ('report_name', '=', cls.__name__)
- ])
- if not action_reports:
- raise Exception('Error', 'Report (%s) not find!' % cls.__name__)
cls.check_access()
- action_report = action_reports[0]
+
+ action_id = data.get('action_id')
+ if action_id is None:
+ action_reports = ActionReport.search([
+ ('report_name', '=', cls.__name__)
+ ])
+ assert action_reports, '%s not found' % cls
+ action_report = action_reports[0]
+ else:
+ action_report = ActionReport(action_id)
+
records = None
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)
+ report_context = cls.get_context(records, data)
+ oext, content = cls.convert(action_report,
+ cls.render(action_report, report_context))
+ content = bytearray(content) if bytes == str else bytes(content)
+ return (oext, content, action_report.direct_print, action_report.name)
@classmethod
def _get_records(cls, ids, model, data):
@@ -157,7 +162,7 @@ class Report(URLMixin, PoolBase):
self.id = id
self._language = Transaction().language
- def setLang(self, language):
+ def set_lang(self, language):
self._language = language
def __getattr__(self, name):
@@ -173,33 +178,29 @@ class Report(URLMixin, PoolBase):
return [TranslateModel(id) for id in ids]
@classmethod
- def parse(cls, report, records, data, localcontext):
- '''
- Parse the report and return a tuple with report type and report.
- '''
+ def get_context(cls, records, data):
pool = Pool()
User = pool.get('res.user')
- Translation = pool.get('ir.translation')
- localcontext['data'] = data
- localcontext['user'] = User(Transaction().user)
- localcontext['formatLang'] = lambda *args, **kargs: \
- cls.format_lang(*args, **kargs)
- localcontext['StringIO'] = StringIO.StringIO
- localcontext['time'] = time
- localcontext['datetime'] = datetime
- localcontext['context'] = Transaction().context
+ report_context = {}
+ report_context['data'] = data
+ report_context['context'] = Transaction().context
+ report_context['user'] = User(Transaction().user)
+ report_context['records'] = records
+ report_context['format_date'] = cls.format_date
+ report_context['format_currency'] = cls.format_currency
+ report_context['format_number'] = cls.format_number
+ report_context['datetime'] = datetime
- translate = TranslateFactory(cls.__name__, Transaction().language,
- Translation)
- localcontext['setLang'] = lambda language: translate.set_language(
- language)
+ return report_context
- # Convert to str as buffer from DB is not supported by StringIO
- report_content = (str(report.report_content) if report.report_content
- else False)
- style_content = (str(report.style_content) if report.style_content
- else False)
+ @classmethod
+ def _prepare_template_file(cls, report):
+ # Convert to str as value from DB is not supported by StringIO
+ report_content = (bytes(report.report_content) if report.report_content
+ else None)
+ style_content = (bytes(report.style_content) if report.style_content
+ else None)
if not report_content:
raise Exception('Error', 'Missing report file!')
@@ -268,44 +269,52 @@ class Report(URLMixin, PoolBase):
outzip.writestr(file, picture)
if manifest:
- outzip.writestr(MANIFEST, str(manifest))
+ outzip.writestr(MANIFEST, bytes(manifest))
content_z.close()
content_io.close()
outzip.close()
- # Since Genshi >= 0.6, Translator requires a function type
+ return fd, path
+
+ @classmethod
+ def _add_translation_hook(cls, relatorio_report, context):
+ pool = Pool()
+ Translation = pool.get('ir.translation')
+
+ translate = TranslateFactory(cls.__name__, Transaction().language,
+ Translation)
+ context['set_lang'] = lambda language: translate.set_language(language)
translator = Translator(lambda text: translate(text))
+ relatorio_report.filters.insert(0, translator)
+
+ @classmethod
+ def render(cls, report, report_context):
+ "calls the underlying templating engine to renders the report"
+ fd, path = cls._prepare_template_file(report)
mimetype = MIMETYPES[report.template_extension]
rel_report = relatorio.reporting.Report(path, mimetype,
ReportFactory(), relatorio.reporting.MIMETemplateLoader())
- rel_report.filters.insert(0, translator)
- #convert unicode key into str
- localcontext = dict(map(lambda x: (str(x[0]), x[1]),
- localcontext.iteritems()))
- #Test compatibility with old relatorio version <= 0.3.0
- if len(inspect.getargspec(rel_report.__call__)[0]) == 2:
- data = rel_report(records, **localcontext).render().getvalue()
- else:
- localcontext['objects'] = records # XXX to remove
- localcontext['records'] = records
- data = rel_report(**localcontext).render()
- if hasattr(data, 'getvalue'):
- data = data.getvalue()
+ cls._add_translation_hook(rel_report, report_context)
+
+ data = rel_report(**report_context).render()
+ if hasattr(data, 'getvalue'):
+ data = data.getvalue()
os.close(fd)
os.remove(path)
- output_format = report.extension or report.template_extension
- if output_format not in MIMETYPES:
- data = cls.unoconv(data, report.template_extension, output_format)
- oext = FORMAT2EXT.get(output_format, output_format)
- return (oext, data)
+
+ return data
@classmethod
- def unoconv(cls, data, input_format, output_format):
- '''
- Call unoconv to convert the OpenDocument
- '''
+ def convert(cls, report, data):
+ "converts the report data to another mimetype if necessary"
+ input_format = report.template_extension
+ output_format = report.extension or report.template_extension
+
+ if output_format in MIMETYPES:
+ return output_format, data
+
fd, path = tempfile.mkstemp(suffix=(os.extsep + input_format),
prefix='trytond_')
oext = FORMAT2EXT.get(output_format, output_format)
@@ -318,31 +327,37 @@ class Report(URLMixin, PoolBase):
stdoutdata, stderrdata = proc.communicate()
if proc.wait() != 0:
raise Exception(stderrdata)
- return stdoutdata
+ return oext, stdoutdata
finally:
os.remove(path)
@classmethod
- def format_lang(cls, value, lang, digits=2, grouping=True, monetary=False,
- date=False, currency=None, symbol=True):
+ def format_date(cls, value, lang):
pool = Pool()
Lang = pool.get('ir.lang')
Config = pool.get('ir.configuration')
- if date or isinstance(value, datetime.date):
- if date:
- warnings.warn('date parameter of format_lang is deprecated, '
- 'use a datetime.date as value instead', DeprecationWarning,
- stacklevel=2)
- if lang:
- locale_format = lang.date
- code = lang.code
- else:
- locale_format = Lang.default_date()
- code = Config.get_language()
- return Lang.strftime(value, code, locale_format)
- if currency:
- return Lang.currency(lang, value, currency, grouping=grouping,
- symbol=symbol)
+ if lang:
+ locale_format = lang.date
+ code = lang.code
+ else:
+ locale_format = Lang.default_date()
+ code = Config.get_language()
+ return Lang.strftime(value, code, locale_format)
+
+ @classmethod
+ def format_currency(cls, value, lang, currency, symbol=True,
+ grouping=True):
+ pool = Pool()
+ Lang = pool.get('ir.lang')
+
+ return Lang.currency(lang, value, currency, symbol, grouping)
+
+ @classmethod
+ def format_number(cls, value, lang, digits=2, grouping=True,
+ monetary=None):
+ pool = Pool()
+ Lang = pool.get('ir.lang')
+
return Lang.format(lang, '%.' + str(digits) + 'f', value,
grouping=grouping, monetary=monetary)
diff --git a/trytond/res/__init__.py b/trytond/res/__init__.py
index 481f45a..6b157c1 100644
--- a/trytond/res/__init__.py
+++ b/trytond/res/__init__.py
@@ -1,5 +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.
+# 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 ..pool import Pool
from .group import *
from .user import *
diff --git a/trytond/res/group.py b/trytond/res/group.py
index 86649dd..03772c8 100644
--- a/trytond/res/group.py
+++ b/trytond/res/group.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
"Group"
from itertools import chain
from ..model import ModelView, ModelSQL, fields
diff --git a/trytond/res/ir.py b/trytond/res/ir.py
index a867611..e6622fa 100644
--- a/trytond/res/ir.py
+++ b/trytond/res/ir.py
@@ -1,5 +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.
+# 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 ..model import ModelSQL, fields
from .. import backend
from ..transaction import Transaction
diff --git a/trytond/res/ir.xml b/trytond/res/ir.xml
index 041e090..a51e5ba 100644
--- a/trytond/res/ir.xml
+++ b/trytond/res/ir.xml
@@ -265,6 +265,14 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
+ <record model="ir.model.access" id="access_ir_model_data_admin">
+ <field name="model" search="[('model', '=', 'ir.model.data')]"/>
+ <field name="group" ref="group_admin"/>
+ <field name="perm_read" eval="True"/>
+ <field name="perm_write" eval="True"/>
+ <field name="perm_create" eval="False"/>
+ <field name="perm_delete" eval="False"/>
+ </record>
<record model="ir.model.access" id="access_ir_cron">
<field name="model" search="[('model', '=', 'ir.cron')]"/>
<field name="perm_read" eval="True"/>
@@ -420,11 +428,13 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_menu1">
- <field name="domain">[('groups', 'in', [g.id for g in user.groups])]</field>
+ <field name="domain"
+ eval="[('groups', 'in', Eval('user', {}).get('groups', []))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_menu"/>
</record>
<record model="ir.rule" id="rule_menu2">
- <field name="domain">[('groups', '=', None)]</field>
+ <field name="domain" eval="[('groups', '=', None)]" pyson="1"/>
<field name="rule_group" ref="rule_group_menu"/>
</record>
<record model="ir.rule.group" id="rule_group_action">
@@ -432,11 +442,13 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_action1">
- <field name="domain">[('groups', 'in', [g.id for g in user.groups])]</field>
+ <field name="domain"
+ eval="[('groups', 'in', Eval('user', {}).get('groups', []))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_action"/>
</record>
<record model="ir.rule" id="rule_action2">
- <field name="domain">[('groups', '=', None)]</field>
+ <field name="domain" eval="[('groups', '=', None)]" pyson="1"/>
<field name="rule_group" ref="rule_group_action"/>
</record>
<record model="ir.rule.group" id="rule_group_action_keyword">
@@ -444,11 +456,13 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_action_keyword1">
- <field name="domain">[('groups', 'in', [g.id for g in user.groups])]</field>
+ <field name="domain"
+ eval="[('groups', 'in', Eval('user', {}).get('groups', []))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_action_keyword"/>
</record>
<record model="ir.rule" id="rule_action_keyword2">
- <field name="domain">[('groups', '=', None)]</field>
+ <field name="domain" eval="[('groups', '=', None)]" pyson="1"/>
<field name="rule_group" ref="rule_group_action_keyword"/>
</record>
@@ -457,11 +471,13 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_action_report1">
- <field name="domain">[('groups', 'in', [g.id for g in user.groups])]</field>
+ <field name="domain"
+ eval="[('groups', 'in', Eval('user', {}).get('groups', []))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_action_report"/>
</record>
<record model="ir.rule" id="rule_action_report2">
- <field name="domain">[('groups', '=', None)]</field>
+ <field name="domain" eval="[('groups', '=', None)]" pyson="1"/>
<field name="rule_group" ref="rule_group_action_report"/>
</record>
@@ -470,11 +486,13 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_action_act_window1">
- <field name="domain">[('groups', 'in', [g.id for g in user.groups])]</field>
+ <field name="domain"
+ eval="[('groups', 'in', Eval('user', {}).get('groups', []))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_action_act_window"/>
</record>
<record model="ir.rule" id="rule_action_act_window2">
- <field name="domain">[('groups', '=', None)]</field>
+ <field name="domain" eval="[('groups', '=', None)]" pyson="1"/>
<field name="rule_group" ref="rule_group_action_act_window"/>
</record>
@@ -483,11 +501,13 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_action_wizard1">
- <field name="domain">[('groups', 'in', [g.id for g in user.groups])]</field>
+ <field name="domain"
+ eval="[('groups', 'in', Eval('user', {}).get('groups', []))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_action_wizard"/>
</record>
<record model="ir.rule" id="rule_action_wizard2">
- <field name="domain">[('groups', '=', None)]</field>
+ <field name="domain" eval="[('groups', '=', None)]" pyson="1"/>
<field name="rule_group" ref="rule_group_action_wizard"/>
</record>
@@ -496,11 +516,13 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_action_url1">
- <field name="domain">[('groups', 'in', [g.id for g in user.groups])]</field>
+ <field name="domain"
+ eval="[('groups', 'in', Eval('user', {}).get('groups', []))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_action_url"/>
</record>
<record model="ir.rule" id="rule_action_url2">
- <field name="domain">[('groups', '=', None)]</field>
+ <field name="domain" eval="[('groups', '=', None)]" pyson="1"/>
<field name="rule_group" ref="rule_group_action_url"/>
</record>
@@ -760,7 +782,9 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_read" eval="False"/>
</record>
<record model="ir.rule" id="rule_sequence">
- <field name="domain">[('groups', 'in', [g.id for g in user.groups])]</field>
+ <field name="domain"
+ eval="[('groups', 'in', Eval('user', {}).get('groups', []))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_sequence"/>
</record>
@@ -770,7 +794,9 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_read" eval="False"/>
</record>
<record model="ir.rule" id="rule_sequence_strict">
- <field name="domain">[('groups', 'in', [g.id for g in user.groups])]</field>
+ <field name="domain"
+ eval="[('groups', 'in', Eval('user', {}).get('groups', []))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_sequence_strict"/>
</record>
@@ -824,7 +850,9 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.rule" id="rule_group_view_search1">
- <field name="domain">[('user', '=', user.id)]</field>
+ <field name="domain"
+ eval="[('user', '=', Eval('user', {}).get('id', -1))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_view_search"/>
</record>
@@ -850,7 +878,9 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_session1">
- <field name="domain">[('create_uid', '=', user.id)]</field>
+ <field name="domain"
+ eval="[('create_uid', '=', Eval('user', {}).get('id', -1))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_session"/>
</record>
@@ -860,7 +890,9 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_session_wizard1">
- <field name="domain">[('create_uid', '=', user.id)]</field>
+ <field name="domain"
+ eval="[('create_uid', '=', Eval('user', {}).get('id', -1))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_session_wizard"/>
</record>
@@ -870,7 +902,9 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_ui_menu_favorite1">
- <field name="domain">[('user', '=', user.id)]</field>
+ <field name="domain"
+ eval="[('create_uid', '=', Eval('user', {}).get('id', -1))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_ui_menu_favorite"/>
</record>
diff --git a/trytond/res/locale/bg_BG.po b/trytond/res/locale/bg_BG.po
index 83777ba..a2223d7 100644
--- a/trytond/res/locale/bg_BG.po
+++ b/trytond/res/locale/bg_BG.po
@@ -737,7 +737,7 @@ msgid "Cancel"
msgstr "Отказ"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "Добре"
msgctxt "wizard_button:res.user.config,user,add:"
diff --git a/trytond/res/locale/ca_ES.po b/trytond/res/locale/ca_ES.po
index 25cdfb9..e4f0144 100644
--- a/trytond/res/locale/ca_ES.po
+++ b/trytond/res/locale/ca_ES.po
@@ -8,7 +8,7 @@ msgstr "El nom del grup ha de ser únic."
msgctxt "error:res.user:"
msgid "Wrong password!"
-msgstr "Contrasenya incorrecta."
+msgstr "La contrasenya és incorrecta."
msgctxt "error:res.user:"
msgid "You can not have two users with the same login!"
@@ -705,7 +705,7 @@ msgid "Cancel"
msgstr "Cancel·la"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "D'acord"
msgctxt "wizard_button:res.user.config,user,add:"
diff --git a/trytond/res/locale/cs_CZ.po b/trytond/res/locale/cs_CZ.po
index 57228c3..1c6df59 100644
--- a/trytond/res/locale/cs_CZ.po
+++ b/trytond/res/locale/cs_CZ.po
@@ -726,7 +726,7 @@ msgid "Cancel"
msgstr ""
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr ""
msgctxt "wizard_button:res.user.config,user,add:"
diff --git a/trytond/res/locale/de_DE.po b/trytond/res/locale/de_DE.po
index 40fed63..607bebf 100644
--- a/trytond/res/locale/de_DE.po
+++ b/trytond/res/locale/de_DE.po
@@ -730,7 +730,7 @@ msgid "Cancel"
msgstr "Abbrechen"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "OK"
msgctxt "wizard_button:res.user.config,user,add:"
diff --git a/trytond/res/locale/es_AR.po b/trytond/res/locale/es_AR.po
index 71897ae..03d5011 100644
--- a/trytond/res/locale/es_AR.po
+++ b/trytond/res/locale/es_AR.po
@@ -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 ""
@@ -538,7 +538,7 @@ msgstr "Usuario modificación"
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 "Los grupos autorizados para editar las secuencias de este tipo"
msgctxt "help:res.user,actions:"
msgid "Actions that will be run at login"
@@ -646,7 +646,7 @@ msgstr "Cancelar instalación"
msgctxt "view:ir.module.module:"
msgid "Cancel Uninstallation"
-msgstr "Cancelar la desinstalación"
+msgstr "Cancelar desinstalación"
msgctxt "view:ir.module.module:"
msgid "Cancel Upgrade"
@@ -729,7 +729,7 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:res.user.config,user,add:"
diff --git a/trytond/res/locale/es_CO.po b/trytond/res/locale/es_CO.po
index 4f741c0..9222050 100644
--- a/trytond/res/locale/es_CO.po
+++ b/trytond/res/locale/es_CO.po
@@ -729,7 +729,7 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:res.user.config,user,add:"
diff --git a/trytond/res/locale/es_EC.po b/trytond/res/locale/es_EC.po
index bdae57a..2dc4995 100644
--- a/trytond/res/locale/es_EC.po
+++ b/trytond/res/locale/es_EC.po
@@ -30,11 +30,11 @@ msgstr "Acción"
msgctxt "field:ir.action-res.group,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.action-res.group,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.action-res.group,group:"
msgid "Group"
@@ -50,11 +50,11 @@ msgstr "Nombre"
msgctxt "field:ir.action-res.group,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.action-res.group,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.model.button-res.group,active:"
msgid "Active"
@@ -66,11 +66,11 @@ msgstr "Botón"
msgctxt "field:ir.model.button-res.group,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.model.button-res.group,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.model.button-res.group,group:"
msgid "Group"
@@ -86,23 +86,23 @@ msgstr "Nombre"
msgctxt "field:ir.model.button-res.group,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.model.button-res.group,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.model.field-res.group,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.model.field-res.group,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.model.field-res.group,field:"
msgid "Model Field"
-msgstr "Campo del Modelo"
+msgstr "Campo del modelo"
msgctxt "field:ir.model.field-res.group,group:"
msgid "Group"
@@ -118,19 +118,19 @@ msgstr "Nombre"
msgctxt "field:ir.model.field-res.group,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.model.field-res.group,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.rule.group-res.group,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.rule.group-res.group,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.rule.group-res.group,group:"
msgid "Group"
@@ -146,23 +146,23 @@ msgstr "Nombre"
msgctxt "field:ir.rule.group-res.group,rule_group:"
msgid "Rule Group"
-msgstr "Grupo de Reglas"
+msgstr "Grupo de reglas"
msgctxt "field:ir.rule.group-res.group,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.rule.group-res.group,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.rule.group-res.user,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.rule.group-res.user,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.rule.group-res.user,id:"
msgid "ID"
@@ -174,7 +174,7 @@ msgstr "Nombre"
msgctxt "field:ir.rule.group-res.user,rule_group:"
msgid "Rule Group"
-msgstr "Grupo de Reglas"
+msgstr "Grupo de reglas"
msgctxt "field:ir.rule.group-res.user,user:"
msgid "User"
@@ -182,35 +182,35 @@ msgstr "Usuario"
msgctxt "field:ir.rule.group-res.user,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.rule.group-res.user,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+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"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.sequence.type-res.group,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+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"
@@ -222,23 +222,23 @@ 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"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.sequence.type-res.group,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:ir.ui.menu-res.group,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:ir.ui.menu-res.group,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:ir.ui.menu-res.group,group:"
msgid "Group"
@@ -258,23 +258,23 @@ msgstr "Nombre"
msgctxt "field:ir.ui.menu-res.group,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:ir.ui.menu-res.group,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:res.group,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:res.group,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:res.group,field_access:"
msgid "Access Field"
-msgstr "Acceso a Campo"
+msgstr "Acceso a campos"
msgctxt "field:res.group,id:"
msgid "ID"
@@ -282,11 +282,11 @@ msgstr "ID"
msgctxt "field:res.group,menu_access:"
msgid "Access Menu"
-msgstr "Acceso a Menú"
+msgstr "Acceso a menús"
msgctxt "field:res.group,model_access:"
msgid "Access Model"
-msgstr "Acceso a Modelo"
+msgstr "Acceso a modelos"
msgctxt "field:res.group,name:"
msgid "Name"
@@ -306,11 +306,11 @@ msgstr "Usuarios"
msgctxt "field:res.group,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:res.group,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:res.user,actions:"
msgid "Actions"
@@ -322,11 +322,11 @@ msgstr "Activo"
msgctxt "field:res.user,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:res.user,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:res.user,email:"
msgid "Email"
@@ -346,7 +346,7 @@ msgstr "Idioma"
msgctxt "field:res.user,language_direction:"
msgid "Language Direction"
-msgstr "Dirección del Idioma"
+msgstr "Dirección del idioma"
msgctxt "field:res.user,login:"
msgid "Login"
@@ -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"
@@ -366,7 +366,7 @@ msgstr "Contraseña"
msgctxt "field:res.user,password_hash:"
msgid "Password Hash"
-msgstr "Hash de Contraseña"
+msgstr "Hash de la contraseña"
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
@@ -390,7 +390,7 @@ msgstr "Firma"
msgctxt "field:res.user,status_bar:"
msgid "Status Bar"
-msgstr "Barra de Estado"
+msgstr "Barra de estado"
msgctxt "field:res.user,warnings:"
msgid "Warnings"
@@ -398,11 +398,11 @@ msgstr "Advertencias"
msgctxt "field:res.user,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:res.user,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:res.user-ir.action,action:"
msgid "Action"
@@ -410,11 +410,11 @@ msgstr "Acción"
msgctxt "field:res.user-ir.action,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:res.user-ir.action,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:res.user-ir.action,id:"
msgid "ID"
@@ -430,19 +430,19 @@ msgstr "Usuario"
msgctxt "field:res.user-ir.action,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:res.user-ir.action,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:res.user-res.group,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:res.user-res.group,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:res.user-res.group,group:"
msgid "Group"
@@ -462,11 +462,11 @@ msgstr "Usuario"
msgctxt "field:res.user-res.group,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:res.user-res.group,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:res.user.config.start,id:"
msgid "ID"
@@ -474,11 +474,11 @@ msgstr "ID"
msgctxt "field:res.user.login.attempt,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:res.user.login.attempt,create_uid:"
msgid "Create User"
-msgstr "Usuario creación"
+msgstr "Creado por usuario"
msgctxt "field:res.user.login.attempt,id:"
msgid "ID"
@@ -494,11 +494,11 @@ msgstr "Nombre"
msgctxt "field:res.user.login.attempt,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:res.user.login.attempt,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:res.user.warning,always:"
msgid "Always"
@@ -506,11 +506,11 @@ msgstr "Siempre"
msgctxt "field:res.user.warning,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:res.user.warning,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:res.user.warning,id:"
msgid "ID"
@@ -530,15 +530,15 @@ msgstr "Usuario"
msgctxt "field:res.user.warning,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:res.user.warning,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "help:ir.sequence.type,groups:"
msgid "Groups allowed to edit the sequences of this type"
-msgstr "Grupos autorizados para editar las secuencias de este tipo"
+msgstr "Los grupos autorizados para editar las secuencias de este tipo"
msgctxt "help:res.user,actions:"
msgid "Actions that will be run at login"
@@ -550,7 +550,7 @@ msgstr "Grupos"
msgctxt "model:ir.action,name:act_user_config"
msgid "Configure Users"
-msgstr "Configurar Usuarios"
+msgstr "Configurar usuarios"
msgctxt "model:ir.action,name:act_user_form"
msgid "Users"
@@ -562,7 +562,7 @@ msgstr "Acción - Grupo"
msgctxt "model:ir.cron,name:cron_trigger_time"
msgid "Run On Time Triggers"
-msgstr "Ejecutar Disparadores a Tiempo"
+msgstr "Ejecutar Disparadores \"A tiempo\""
msgctxt "model:ir.model.button-res.group,name:"
msgid "Model Button - Group"
@@ -574,15 +574,15 @@ msgstr "Relación entre grupo y campo del modelo"
msgctxt "model:ir.rule.group-res.group,name:"
msgid "Rule Group - Group"
-msgstr "Grupo de Reglas - Grupo"
+msgstr "Grupo de reglas - Grupo"
msgctxt "model:ir.rule.group-res.user,name:"
msgid "Rule Group - User"
-msgstr "Grupo de Reglas - Usuario"
+msgstr "Grupo de reglas - Usuario"
msgctxt "model:ir.sequence.type-res.group,name:"
msgid "Sequence Type - Group"
-msgstr "Tipo de Secuencia - Grupo"
+msgstr "Tipo de secuencia - Grupo"
msgctxt "model:ir.ui.menu,name:menu_group_form"
msgid "Groups"
@@ -598,7 +598,7 @@ msgstr "Usuarios"
msgctxt "model:ir.ui.menu-res.group,name:"
msgid "UI Menu - Group"
-msgstr "Menú UI - Grupo"
+msgstr "Menú de la UI - Grupo"
msgctxt "model:res.group,name:"
msgid "Group"
@@ -618,7 +618,7 @@ msgstr "Administrador"
msgctxt "model:res.user,name:user_trigger"
msgid "Cron Trigger"
-msgstr "Disparador del Programador de tareas"
+msgstr "Disparador del programador de tareas"
msgctxt "model:res.user-ir.action,name:"
msgid "User - Action"
@@ -630,7 +630,7 @@ msgstr "Usuario - Grupo"
msgctxt "model:res.user.config.start,name:"
msgid "User Config Init"
-msgstr "Configuración Inicial de Usuario"
+msgstr "Configuración inicial de usuario"
msgctxt "model:res.user.login.attempt,name:"
msgid "Login Attempt"
@@ -638,35 +638,35 @@ msgstr "Intento de Inicio de Sesión"
msgctxt "model:res.user.warning,name:"
msgid "User Warning"
-msgstr "Advertencia al Usuario"
+msgstr "Aviso al 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 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 Instalar"
+msgstr "Marcar para instalar"
msgctxt "view:ir.module.module:"
msgid "Mark for Uninstallation (beta)"
-msgstr "Marcar para Desinstalar (beta)"
+msgstr "Marcar para desinstalar (beta)"
msgctxt "view:ir.module.module:"
msgid "Mark for Upgrade"
-msgstr "Marcar para Actualizar"
+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"
@@ -682,27 +682,27 @@ msgstr "Miembros"
msgctxt "view:res.user.config.start:"
msgid "Add Users"
-msgstr "Agregar Usuarios"
+msgstr "Añadir usuarios"
msgctxt "view:res.user.config.start:"
msgid "Be careful that the login must be unique!"
-msgstr "¡Asegúrese de que el nombre de usuario sea único!"
+msgstr "El identificador debe ser único."
msgctxt "view:res.user.config.start:"
msgid "You can now add some users into the system."
-msgstr "Ahora puede agregar algunos usuarios al sistema."
+msgstr "Si quiere, ahora puede añadir más usuarios en el 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"
@@ -729,12 +729,12 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:res.user.config,user,add:"
msgid "Add"
-msgstr "Añadir"
+msgstr "Agregar"
msgctxt "wizard_button:res.user.config,user,end:"
msgid "End"
diff --git a/trytond/res/locale/es_ES.po b/trytond/res/locale/es_ES.po
index 18997e6..4f7bf0a 100644
--- a/trytond/res/locale/es_ES.po
+++ b/trytond/res/locale/es_ES.po
@@ -8,7 +8,7 @@ msgstr "El nombre del grupo debe ser único."
msgctxt "error:res.user:"
msgid "Wrong password!"
-msgstr "Contraseña incorrecta."
+msgstr "La contraseña es incorrecta."
msgctxt "error:res.user:"
msgid "You can not have two users with the same login!"
@@ -729,7 +729,7 @@ msgid "Cancel"
msgstr "Cancelar"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "Aceptar"
msgctxt "wizard_button:res.user.config,user,add:"
diff --git a/trytond/res/locale/fr_FR.po b/trytond/res/locale/fr_FR.po
index 7ed7195..a6f059d 100644
--- a/trytond/res/locale/fr_FR.po
+++ b/trytond/res/locale/fr_FR.po
@@ -729,8 +729,8 @@ msgid "Cancel"
msgstr "Annuler"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
-msgstr "Ok"
+msgid "OK"
+msgstr "OK"
msgctxt "wizard_button:res.user.config,user,add:"
msgid "Add"
diff --git a/trytond/res/locale/nl_NL.po b/trytond/res/locale/nl_NL.po
index 3bf2b91..bff5213 100644
--- a/trytond/res/locale/nl_NL.po
+++ b/trytond/res/locale/nl_NL.po
@@ -785,7 +785,7 @@ msgstr "Annuleren"
#, fuzzy
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "Oké"
#, fuzzy
diff --git a/trytond/res/locale/ru_RU.po b/trytond/res/locale/ru_RU.po
index 22ebae5..f6cae7f 100644
--- a/trytond/res/locale/ru_RU.po
+++ b/trytond/res/locale/ru_RU.po
@@ -730,7 +730,7 @@ msgid "Cancel"
msgstr "Отменить"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "Ок"
msgctxt "wizard_button:res.user.config,user,add:"
diff --git a/trytond/res/locale/sl_SI.po b/trytond/res/locale/sl_SI.po
index 8b96eb7..c8f9186 100644
--- a/trytond/res/locale/sl_SI.po
+++ b/trytond/res/locale/sl_SI.po
@@ -728,7 +728,7 @@ msgid "Cancel"
msgstr "Prekliči"
msgctxt "wizard_button:res.user.config,start,user:"
-msgid "Ok"
+msgid "OK"
msgstr "V redu"
msgctxt "wizard_button:res.user.config,user,add:"
diff --git a/trytond/res/user.py b/trytond/res/user.py
index 19e1ba8..662d77b 100644
--- a/trytond/res/user.py
+++ b/trytond/res/user.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
"User"
import copy
import string
@@ -202,17 +202,18 @@ class User(ModelSQL, ModelView):
timeout = datetime.timedelta(
seconds=config.getint('session', 'timeout'))
result = dict((u.id, 0) for u in users)
- for sub_ids in grouped_slice(users):
- sessions = Session.search([
- ('create_uid', 'in', sub_ids),
- ], order=[('create_uid', 'ASC')])
-
- def filter_(session):
- timestamp = session.write_date or session.create_date
- return abs(timestamp - now) < timeout
- result.update(dict((i, len(list(g)))
- for i, g in groupby(ifilter(filter_, sessions),
- attrgetter('create_uid.id'))))
+ with Transaction().set_user(0):
+ for sub_ids in grouped_slice(users):
+ sessions = Session.search([
+ ('create_uid', 'in', sub_ids),
+ ], order=[('create_uid', 'ASC')])
+
+ def filter_(session):
+ timestamp = session.write_date or session.create_date
+ return abs(timestamp - now) < timeout
+ result.update(dict((i, len(list(g)))
+ for i, g in groupby(ifilter(filter_, sessions),
+ attrgetter('create_uid.id'))))
return result
@staticmethod
@@ -273,12 +274,14 @@ class User(ModelSQL, ModelView):
@classmethod
def search_rec_name(cls, name, clause):
- users = cls.search([
- ('login', '=', clause[2]),
- ], order=[])
- if len(users) == 1:
- return [('id', '=', users[0].id)]
- return [(cls._rec_name,) + tuple(clause[1:])]
+ if clause[1].startswith('!') or clause[1].startswith('not '):
+ bool_op = 'AND'
+ else:
+ bool_op = 'OR'
+ return [bool_op,
+ ('login',) + tuple(clause[1:]),
+ (cls._rec_name,) + tuple(clause[1:]),
+ ]
@classmethod
def copy(cls, users, default=None):
@@ -301,7 +304,6 @@ class User(ModelSQL, ModelView):
ModelData = pool.get('ir.model.data')
Action = pool.get('ir.action')
ConfigItem = pool.get('ir.module.module.config_wizard.item')
- Config = pool.get('ir.configuration')
res = {}
if context_only:
@@ -314,7 +316,7 @@ class User(ModelSQL, ModelView):
if user.language:
res['language'] = user.language.code
else:
- res['language'] = Config.get_language()
+ res['language'] = None
else:
res[field] = None
if getattr(user, field):
@@ -666,7 +668,7 @@ class UserConfig(Wizard):
start = StateView('res.user.config.start',
'res.user_config_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
- Button('Ok', 'user', 'tryton-ok', default=True),
+ Button('OK', 'user', 'tryton-ok', default=True),
])
user = StateView('res.user',
'res.user_view_form', [
diff --git a/trytond/res/user.xml b/trytond/res/user.xml
index 5d39768..1c2993f 100644
--- a/trytond/res/user.xml
+++ b/trytond/res/user.xml
@@ -101,7 +101,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_user_warning1">
- <field name="domain">[('user', '=', user.id)]</field>
+ <field name="domain"
+ eval="[('user', '=', Eval('user', {}).get('id', -1))]" pyson="1"/>
<field name="rule_group" ref="rule_group_user_warning"/>
</record>
diff --git a/trytond/rpc.py b/trytond/rpc.py
index 4fc1f7a..d346b11 100644
--- a/trytond/rpc.py
+++ b/trytond/rpc.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
__all__ = ['RPC']
@@ -32,9 +32,12 @@ class RPC(object):
else:
context = args.pop()
timestamp = None
- if '_timestamp' in context:
- timestamp = context['_timestamp']
- del context['_timestamp']
+ for key in context.keys():
+ if key == '_timestamp':
+ timestamp = context[key]
+ # Remove all private keyword but _datetime for history
+ if key.startswith('_') and key != '_datetime':
+ del context[key]
if self.check_access:
context['_check_access'] = True
if self.instantiate is not None:
diff --git a/trytond/security.py b/trytond/security.py
index 4e92b00..70d594e 100644
--- a/trytond/security.py
+++ b/trytond/security.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 os
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
try:
import crypt
except ImportError:
@@ -15,7 +14,7 @@ from trytond.exceptions import NotLogged
def _get_pool(dbname):
database_list = Pool.database_list()
pool = Pool(dbname)
- if not dbname in database_list:
+ if dbname not in database_list:
pool.init()
return pool
diff --git a/trytond/server.py b/trytond/server.py
index 7cf414c..b792460 100644
--- a/trytond/server.py
+++ b/trytond/server.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
"""
%prog [options]
"""
@@ -31,16 +31,22 @@ class TrytonServer(object):
logging.getLogger('server').info('using %s as logging '
'configuration file', options.logconf)
else:
- logformat = '[%(asctime)s] %(levelname)s:%(name)s:%(message)s'
- datefmt = '%a %b %d %H:%M:%S %Y'
- logging.basicConfig(level=logging.INFO, format=logformat,
- datefmt=datefmt)
+ logformat = ('%(process)s %(thread)s [%(asctime)s] '
+ '%(levelname)s %(name)s %(message)s')
+ if options.verbose:
+ if options.dev:
+ level = logging.DEBUG
+ else:
+ level = logging.INFO
+ else:
+ level = logging.ERROR
+ logging.basicConfig(level=level, format=logformat)
self.logger = logging.getLogger(__name__)
if options.configfile:
- self.logger.info('using %s as configuration file'
- % options.configfile)
+ self.logger.info('using %s as configuration file',
+ options.configfile)
else:
self.logger.info('using default configuration')
self.logger.info('initialising distributed objects services')
@@ -72,16 +78,21 @@ class TrytonServer(object):
for db_name in self.options.database_names:
init[db_name] = False
- with Transaction().start(db_name, 0) as transaction:
- cursor = transaction.cursor
- if self.options.update:
- if not cursor.test():
- self.logger.info("init db")
- backend.get('Database').init(cursor)
- init[db_name] = True
- cursor.commit()
- elif not cursor.test():
- raise Exception("'%s' is not a Tryton database!" % db_name)
+ try:
+ with Transaction().start(db_name, 0) as transaction:
+ cursor = transaction.cursor
+ if self.options.update:
+ if not cursor.test():
+ self.logger.info("init db")
+ backend.get('Database').init(cursor)
+ init[db_name] = True
+ cursor.commit()
+ elif not cursor.test():
+ raise Exception("'%s' is not a Tryton database!" %
+ db_name)
+ except Exception:
+ self.stop(False)
+ raise
for db_name in self.options.database_names:
if self.options.update:
@@ -149,9 +160,10 @@ class TrytonServer(object):
if not pool.lock.acquire(0):
continue
try:
- if 'ir.cron' not in pool.object_name_list():
+ try:
+ Cron = pool.get('ir.cron')
+ except KeyError:
continue
- Cron = pool.get('ir.cron')
finally:
pool.lock.release()
thread = threading.Thread(
@@ -176,24 +188,24 @@ class TrytonServer(object):
for hostname, port in parse_listen(
config.get('jsonrpc', 'listen')):
self.jsonrpcd.append(JSONRPCDaemon(hostname, port, ssl))
- self.logger.info("starting JSON-RPC%s protocol on %s:%d" %
- (ssl and ' SSL' or '', hostname or '*', port))
+ self.logger.info("starting JSON-RPC%s protocol on %s:%d",
+ ssl and ' SSL' or '', hostname or '*', port)
if config.get('xmlrpc', 'listen'):
from trytond.protocols.xmlrpc import XMLRPCDaemon
for hostname, port in parse_listen(
config.get('xmlrpc', 'listen')):
self.xmlrpcd.append(XMLRPCDaemon(hostname, port, ssl))
- self.logger.info("starting XML-RPC%s protocol on %s:%d" %
- (ssl and ' SSL' or '', hostname or '*', port))
+ self.logger.info("starting XML-RPC%s protocol on %s:%d",
+ ssl and ' SSL' or '', hostname or '*', port)
if config.get('webdav', 'listen'):
from trytond.protocols.webdav import WebDAVServerThread
for hostname, port in parse_listen(
config.get('webdav', 'listen')):
self.webdavd.append(WebDAVServerThread(hostname, port, ssl))
- self.logger.info("starting WebDAV%s protocol on %s:%d" %
- (ssl and ' SSL' or '', hostname or '*', port))
+ self.logger.info("starting WebDAV%s protocol on %s:%d",
+ ssl and ' SSL' or '', hostname or '*', port)
for servers in (self.xmlrpcd, self.jsonrpcd, self.webdavd):
for server in servers:
diff --git a/trytond/test_loader.py b/trytond/test_loader.py
index 22ad30f..81d91fc 100644
--- a/trytond/test_loader.py
+++ b/trytond/test_loader.py
@@ -1,5 +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.
+# 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 unittest import TestLoader
diff --git a/trytond/tests/__init__.py b/trytond/tests/__init__.py
index 6e456ca..9482db7 100644
--- a/trytond/tests/__init__.py
+++ b/trytond/tests/__init__.py
@@ -1,9 +1,10 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 ..pool import Pool
from .test import *
from .model import *
+from .modelview import *
from .mptt import *
from .import_data import *
from .export_data import *
@@ -53,6 +54,9 @@ def register():
TimeDefault,
TimeRequired,
TimeFormat,
+ TimeDelta,
+ TimeDeltaDefault,
+ TimeDeltaRequired,
One2One,
One2OneTarget,
One2OneRelation,
@@ -88,6 +92,7 @@ def register():
Property,
Selection,
SelectionRequired,
+ DictSchema,
Dict,
DictDefault,
DictRequired,
@@ -96,6 +101,7 @@ def register():
BinaryRequired,
Singleton,
URLObject,
+ ModelStorage,
ModelSQLRequiredField,
ModelSQLTimestamp,
Model4Union1,
@@ -107,6 +113,8 @@ def register():
Model4UnionTree1,
Model4UnionTree2,
UnionTree,
+ ModelViewChangedValues,
+ ModelViewChangedValuesTarget,
MPTT,
ImportDataBoolean,
ImportDataInteger,
@@ -150,6 +158,7 @@ def register():
CopyMany2ManyReferenceRelation,
Many2OneTarget,
Many2OneDomainValidation,
+ Many2OneOrderBy,
TestHistory,
TestHistoryLine,
FieldContextChild,
diff --git a/trytond/tests/access.py b/trytond/tests/access.py
index 07e2c3b..36113da 100644
--- a/trytond/tests/access.py
+++ b/trytond/tests/access.py
@@ -1,5 +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.
+# 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__ = [
diff --git a/trytond/tests/copy_.py b/trytond/tests/copy_.py
index d0d92cc..ffcaa0e 100644
--- a/trytond/tests/copy_.py
+++ b/trytond/tests/copy_.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
"Test for copy"
from trytond.model import ModelSQL, fields
diff --git a/trytond/tests/export_data.py b/trytond/tests/export_data.py
index 5f61332..e084c2b 100644
--- a/trytond/tests/export_data.py
+++ b/trytond/tests/export_data.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
"Test for export_data"
from trytond.model import ModelSQL, fields
from trytond.pool import PoolMeta
diff --git a/trytond/tests/history.py b/trytond/tests/history.py
index ed1b24b..6421ad8 100644
--- a/trytond/tests/history.py
+++ b/trytond/tests/history.py
@@ -1,5 +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.
+# 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', 'TestHistoryLine']
diff --git a/trytond/tests/import_data.py b/trytond/tests/import_data.py
index ecc7a94..bebd892 100644
--- a/trytond/tests/import_data.py
+++ b/trytond/tests/import_data.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
"Test for import_data"
from trytond.model import ModelSQL, fields
diff --git a/trytond/tests/model.py b/trytond/tests/model.py
index 1815389..835eb99 100644
--- a/trytond/tests/model.py
+++ b/trytond/tests/model.py
@@ -1,9 +1,11 @@
-#This file is part of Tryton. The COPYRIGHT file at the top level of
-#this repository contains the full copyright notices and license terms.
+# 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 ModelSingleton, ModelSQL, UnionMixin, fields
__all__ = [
- 'Singleton', 'URLObject', 'ModelSQLRequiredField', 'ModelSQLTimestamp',
+ 'Singleton', 'URLObject',
+ 'ModelStorage',
+ 'ModelSQLRequiredField', 'ModelSQLTimestamp',
'Model4Union1', 'Model4Union2', 'Model4Union3', 'Model4Union4',
'Union', 'UnionUnion',
'Model4UnionTree1', 'Model4UnionTree2', 'UnionTree',
@@ -26,6 +28,12 @@ class URLObject(ModelSQL):
name = fields.Char('Name')
+class ModelStorage(ModelSQL):
+ 'Model stored'
+ __name__ = 'test.modelstorage'
+ name = fields.Char('Name')
+
+
class ModelSQLRequiredField(ModelSQL):
'model with a required field'
__name__ = 'test.modelsql'
diff --git a/trytond/tests/modelview.py b/trytond/tests/modelview.py
new file mode 100644
index 0000000..5688021
--- /dev/null
+++ b/trytond/tests/modelview.py
@@ -0,0 +1,32 @@
+# 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 ModelView, fields
+
+
+__all__ = [
+ 'ModelViewChangedValues',
+ 'ModelViewChangedValuesTarget',
+ ]
+
+
+class ModelViewChangedValues(ModelView):
+ 'ModelView Changed Values'
+ __name__ = 'test.modelview.changed_values'
+ name = fields.Char('Name')
+ target = fields.Many2One('test.modelview.changed_values.target', 'Target')
+ ref_target = fields.Reference('Target Reference', [
+ ('test.modelview.changed_values.target', 'Target'),
+ ])
+ targets = fields.One2Many('test.modelview.changed_values.target', 'model',
+ 'Targets')
+ m2m_targets = fields.Many2Many('test.modelview.changed_values.target',
+ None, None, 'Targets')
+
+
+class ModelViewChangedValuesTarget(ModelView):
+ 'ModelView Changed Values Target'
+ __name__ = 'test.modelview.changed_values.target'
+ name = fields.Char('Name')
+ parent = fields.Many2One('test.modelview.changed_values', 'Parent')
diff --git a/trytond/tests/mptt.py b/trytond/tests/mptt.py
index ee9486d..62e8ee2 100644
--- a/trytond/tests/mptt.py
+++ b/trytond/tests/mptt.py
@@ -1,6 +1,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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
"Test for Tree"
+from sql import Null
+
from trytond.model import ModelView, ModelSQL, fields
__all__ = [
@@ -27,7 +29,7 @@ class MPTT(ModelSQL, ModelView):
@staticmethod
def order_sequence(tables):
table, _ = tables[None]
- return [table.sequence == None, table.sequence]
+ return [table.sequence == Null, table.sequence]
@staticmethod
def default_active():
diff --git a/trytond/tests/run-tests.py b/trytond/tests/run-tests.py
index f6dd242..1bbc3a9 100755
--- a/trytond/tests/run-tests.py
+++ b/trytond/tests/run-tests.py
@@ -1,6 +1,6 @@
#!/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.
+# 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
@@ -20,6 +20,8 @@ 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("--no-doctest", action="store_false", dest="doctest",
+ default=True, help="Don't run doctest")
parser.add_argument("-v", action="count", default=0, dest="verbosity",
help="Increase verbosity")
parser.add_argument('tests', metavar='test', nargs='*')
@@ -39,6 +41,6 @@ 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)
+ suite = modules_suite(opt.tests, doc=opt.doctest)
result = unittest.TextTestRunner(verbosity=opt.verbosity).run(suite)
sys.exit(not result.wasSuccessful())
diff --git a/trytond/tests/test.py b/trytond/tests/test.py
index 5f812f2..8b4b201 100644
--- a/trytond/tests/test.py
+++ b/trytond/tests/test.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.
+# 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 datetime
from decimal import Decimal
-from trytond.model import ModelSQL, fields
+from trytond.model import ModelSQL, DictSchemaMixin, fields
from trytond.pyson import Eval
__all__ = [
@@ -16,6 +16,7 @@ __all__ = [
'Date', 'DateDefault', 'DateRequired',
'DateTime', 'DateTimeDefault', 'DateTimeRequired', 'DateTimeFormat',
'Time', 'TimeDefault', 'TimeRequired', 'TimeFormat',
+ 'TimeDelta', 'TimeDeltaDefault', 'TimeDeltaRequired',
'One2One', 'One2OneTarget', 'One2OneRelation', 'One2OneRequired',
'One2OneRequiredRelation', 'One2OneDomain', 'One2OneDomainRelation',
'One2Many', 'One2ManyTarget',
@@ -32,9 +33,9 @@ __all__ = [
'Reference', 'ReferenceTarget', 'ReferenceRequired',
'Property',
'Selection', 'SelectionRequired',
- 'Dict', 'DictDefault', 'DictRequired',
+ 'DictSchema', 'Dict', 'DictDefault', 'DictRequired',
'Binary', 'BinaryDefault', 'BinaryRequired',
- 'Many2OneDomainValidation', 'Many2OneTarget',
+ 'Many2OneDomainValidation', 'Many2OneTarget', 'Many2OneOrderBy',
]
@@ -315,6 +316,31 @@ class TimeFormat(ModelSQL):
time = fields.Time(string='Time', format='%H:%M')
+class TimeDelta(ModelSQL):
+ 'TimeDelta'
+ __name__ = 'test.timedelta'
+ timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta',
+ required=False)
+
+
+class TimeDeltaDefault(ModelSQL):
+ 'TimeDelta Default'
+ __name__ = 'test.timedelta_default'
+ timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta',
+ required=False)
+
+ @staticmethod
+ def default_timedelta():
+ return datetime.timedelta(seconds=3600)
+
+
+class TimeDeltaRequired(ModelSQL):
+ 'TimeDelta Required'
+ __name__ = 'test.timedelta_required'
+ timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta',
+ required=True)
+
+
class One2One(ModelSQL):
'One2One'
__name__ = 'test.one2one'
@@ -629,10 +655,17 @@ class SelectionRequired(ModelSQL):
'Selection', required=True)
+class DictSchema(DictSchemaMixin, ModelSQL):
+ 'Dict Schema'
+ __name__ = 'test.dict.schema'
+
+
class Dict(ModelSQL):
'Dict'
__name__ = 'test.dict'
- dico = fields.Dict(None, 'Test Dict')
+ dico = fields.Dict('test.dict.schema', 'Test Dict')
+ dico_string = dico.translated('dico')
+ dico_string_keys = dico.translated('dico', 'keys')
class DictDefault(ModelSQL):
@@ -664,7 +697,7 @@ class BinaryDefault(ModelSQL):
@staticmethod
def default_binary():
- return buffer('default')
+ return b'default'
class BinaryRequired(ModelSQL):
@@ -676,6 +709,7 @@ class BinaryRequired(ModelSQL):
class Many2OneTarget(ModelSQL):
"Many2One Domain Validation Target"
__name__ = 'test.many2one_target'
+ _order_name = 'value'
active = fields.Boolean('Active')
value = fields.Integer('Value')
@@ -694,3 +728,9 @@ class Many2OneDomainValidation(ModelSQL):
('value', '>', 5),
])
dummy = fields.Char('Dummy')
+
+
+class Many2OneOrderBy(ModelSQL):
+ "Many2One OrderBy"
+ __name__ = 'test.many2one_orderby'
+ many2one = fields.Many2One('test.many2one_target', 'many2one')
diff --git a/trytond/tests/test_access.py b/trytond/tests/test_access.py
index 3251a77..fc780c5 100644
--- a/trytond/tests/test_access.py
+++ b/trytond/tests/test_access.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
install_module
diff --git a/trytond/tests/test_cache.py b/trytond/tests/test_cache.py
index 9aa859c..236885e 100644
--- a/trytond/tests/test_cache.py
+++ b/trytond/tests/test_cache.py
@@ -1,5 +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.
+# 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
diff --git a/trytond/tests/test_copy.py b/trytond/tests/test_copy.py
index d6c3156..ceb642c 100644
--- a/trytond/tests/test_copy.py
+++ b/trytond/tests/test_copy.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
install_module
diff --git a/trytond/tests/test_descriptors.py b/trytond/tests/test_descriptors.py
new file mode 100644
index 0000000..0bcca2b
--- /dev/null
+++ b/trytond/tests/test_descriptors.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.
+import unittest
+import doctest
+
+from trytond.model import descriptors
+
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(descriptors))
+ return suite
diff --git a/trytond/tests/test_exportdata.py b/trytond/tests/test_exportdata.py
index 417b52b..d95921c 100644
--- a/trytond/tests/test_exportdata.py
+++ b/trytond/tests/test_exportdata.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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 sys
try:
import cdecimal
diff --git a/trytond/tests/test_fields.py b/trytond/tests/test_fields.py
index 2c436f6..bda99ba 100644
--- a/trytond/tests/test_fields.py
+++ b/trytond/tests/test_fields.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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 sys
try:
import cdecimal
@@ -16,6 +16,7 @@ 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.model import fields
class FieldsTestCase(unittest.TestCase):
@@ -67,6 +68,10 @@ class FieldsTestCase(unittest.TestCase):
self.time_required = POOL.get('test.time_required')
self.time_format = POOL.get('test.time_format')
+ self.timedelta = POOL.get('test.timedelta')
+ self.timedelta_default = POOL.get('test.timedelta_default')
+ self.timedelta_required = POOL.get('test.timedelta_required')
+
self.one2one = POOL.get('test.one2one')
self.one2one_target = POOL.get('test.one2one.target')
self.one2one_required = POOL.get('test.one2one_required')
@@ -102,6 +107,7 @@ class FieldsTestCase(unittest.TestCase):
self.selection_required = POOL.get('test.selection_required')
self.dict_ = POOL.get('test.dict')
+ self.dict_schema = POOL.get('test.dict.schema')
self.dict_default = POOL.get('test.dict_default')
self.dict_required = POOL.get('test.dict_required')
@@ -110,6 +116,7 @@ class FieldsTestCase(unittest.TestCase):
self.binary_required = POOL.get('test.binary_required')
self.m2o_domain_validation = POOL.get('test.many2one_domainvalidation')
+ self.m2o_orderby = POOL.get('test.many2one_orderby')
self.m2o_target = POOL.get('test.many2one_target')
def test0010boolean(self):
@@ -2269,6 +2276,21 @@ class FieldsTestCase(unittest.TestCase):
'name': 'one2one5',
'one2one': target5.id,
}])
+ targets = self.one2one_target.create([{
+ 'name': 'multiple1',
+ }, {
+ 'name': 'multiple2',
+ }])
+ one2ones = self.one2one.create([{
+ 'name': 'origin6',
+ 'one2one': targets[0].id,
+ }, {
+ 'name': 'origin7',
+ 'one2one': targets[1].id,
+ }])
+ for one2one, target in zip(one2ones, targets):
+ self.assert_(one2one)
+ self.assertEqual(one2one.one2one, target)
transaction.cursor.rollback()
@@ -2648,7 +2670,7 @@ class FieldsTestCase(unittest.TestCase):
}])
self.assert_(origin3_id)
- size_targets = self.many2many_size_target.create([{
+ self.many2many_size_target.create([{
'name': str(i),
} for i in range(6)])
@@ -3108,6 +3130,23 @@ class FieldsTestCase(unittest.TestCase):
'Test Dict'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
+
+ self.dict_schema.create([{
+ 'name': 'a',
+ 'string': 'A',
+ 'type_': 'integer',
+ }, {
+ 'name': 'b',
+ 'string': 'B',
+ 'type_': 'integer',
+ }, {
+ 'name': 'type',
+ 'string': 'Type',
+ 'type_': 'selection',
+ 'selection': ('arabic: Arabic\n'
+ 'hexa: Hexadecimal'),
+ }])
+
dict1, = self.dict_.create([{
'dico': {'a': 1, 'b': 2},
}])
@@ -3116,6 +3155,21 @@ class FieldsTestCase(unittest.TestCase):
self.dict_.write([dict1], {'dico': {'z': 26}})
self.assert_(dict1.dico == {'z': 26})
+ dict1.dico = {
+ 'a': 1,
+ 'type': 'arabic',
+ }
+ dict1.save()
+ self.assertEqual(dict1.dico, {'a': 1, 'type': 'arabic'})
+ self.assertEqual(dict1.dico_string, {
+ 'a': 1,
+ 'type': 'Arabic',
+ })
+ self.assertEqual(dict1.dico_string_keys, {
+ 'a': 'A',
+ 'type': 'Type',
+ })
+
dict2, = self.dict_.create([{}])
self.assert_(dict2.dico is None)
@@ -3137,32 +3191,34 @@ class FieldsTestCase(unittest.TestCase):
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
bin1, = self.binary.create([{
- 'binary': buffer('foo'),
+ 'binary': fields.Binary.cast(b'foo'),
}])
- self.assert_(bin1.binary == buffer('foo'))
+ self.assert_(bin1.binary == fields.Binary.cast(b'foo'))
- self.binary.write([bin1], {'binary': buffer('bar')})
- self.assert_(bin1.binary == buffer('bar'))
+ self.binary.write([bin1], {'binary': fields.Binary.cast(b'bar')})
+ self.assert_(bin1.binary == fields.Binary.cast(b'bar'))
with transaction.set_context({'test.binary.binary': 'size'}):
bin1_size = self.binary(bin1.id)
- self.assert_(bin1_size.binary == len('bar'))
- self.assert_(bin1_size.binary != buffer('bar'))
+ self.assert_(bin1_size.binary == len(b'bar'))
+ self.assert_(bin1_size.binary != fields.Binary.cast(b'bar'))
bin2, = self.binary.create([{}])
self.assert_(bin2.binary is None)
bin3, = self.binary_default.create([{}])
- self.assert_(bin3.binary == buffer('default'))
+ self.assert_(bin3.binary == fields.Binary.cast(b'default'))
self.assertRaises(UserError, self.binary_required.create, [{}])
transaction.cursor.rollback()
- bin4, = self.binary_required.create([{'binary': buffer('baz')}])
- self.assert_(bin4.binary == buffer('baz'))
+ bin4, = self.binary_required.create([{
+ 'binary': fields.Binary.cast(b'baz'),
+ }])
+ self.assert_(bin4.binary == fields.Binary.cast(b'baz'))
self.assertRaises(UserError, self.binary_required.create,
- [{'binary': buffer('')}])
+ [{'binary': fields.Binary.cast(b'')}])
transaction.cursor.rollback()
@@ -3188,6 +3244,241 @@ class FieldsTestCase(unittest.TestCase):
domain.dummy = 'Dummy'
domain.save()
+ # Testing order_by
+ for value in (5, 3, 2):
+ m2o, = self.m2o_target.create([{'value': value}])
+ self.m2o_orderby.create([{'many2one': m2o}])
+
+ search = self.m2o_orderby.search([], order=[('many2one', 'ASC')])
+ self.assertTrue(all(x.many2one.value <= y.many2one.value
+ for x, y in zip(search, search[1:])))
+
+ search = self.m2o_orderby.search([],
+ order=[('many2one.id', 'ASC')])
+ self.assertTrue(all(x.many2one.id <= y.many2one.id
+ for x, y in zip(search, search[1:])))
+
+ search = self.m2o_orderby.search([],
+ order=[('many2one.value', 'ASC')])
+ self.assertTrue(all(x.many2one.value <= y.many2one.value
+ for x, y in zip(search, search[1:])))
+
+ transaction.cursor.rollback()
+
+ def test0200timedelta(self):
+ 'Test timedelta'
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+
+ minute = datetime.timedelta(minutes=1)
+ hour = datetime.timedelta(hours=1)
+ day = datetime.timedelta(days=1)
+ default_timedelta = datetime.timedelta(seconds=3600)
+
+ timedelta1, = self.timedelta.create([{
+ 'timedelta': hour,
+ }])
+ self.assert_(timedelta1)
+ self.assertEqual(timedelta1.timedelta, hour)
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '=', hour),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '=', day),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '=', None),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '!=', day),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '!=', None),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [hour]),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [day]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [minute]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [None]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', []),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', [hour]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', [day]),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', [None]),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', []),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<', day),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<', minute),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<', hour),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<=', hour),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<=', minute),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<=', day),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>', day),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>', minute),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>', hour),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>=', day),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>=', minute),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>=', hour),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta2, = self.timedelta.create([{
+ 'timedelta': minute,
+ }])
+ self.assert_(timedelta2)
+ self.assertEqual(timedelta2.timedelta, minute)
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '=', minute),
+ ])
+ self.assertEqual(timedelta, [timedelta2])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [minute, hour]),
+ ])
+ self.assertEqual(timedelta, [timedelta1, timedelta2])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', [minute, hour]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta3, = self.timedelta.create([{}])
+ self.assert_(timedelta3)
+ self.assertEqual(timedelta3.timedelta, None)
+
+ timedelta4, = self.timedelta_default.create([{}])
+ self.assert_(timedelta4)
+ self.assertEqual(timedelta4.timedelta, default_timedelta)
+
+ self.timedelta.write([timedelta1], {
+ 'timedelta': minute,
+ })
+ self.assertEqual(timedelta1.timedelta, minute)
+
+ self.timedelta.write([timedelta2], {
+ 'timedelta': day,
+ })
+ self.assertEqual(timedelta2.timedelta, day)
+
+ self.assertRaises(Exception, self.timedelta.create, [{
+ 'timedelta': 'test',
+ }])
+
+ self.assertRaises(Exception, self.timedelta.write, [timedelta1], {
+ 'timedelta': 'test',
+ })
+
+ self.assertRaises(Exception, self.timedelta.create, [{
+ 'timedelta': 1,
+ }])
+
+ self.assertRaises(Exception, self.timedelta.write, [timedelta1], {
+ 'timedelta': 1,
+ })
+
+ self.assertRaises(UserError, self.timedelta_required.create, [{}])
+ transaction.cursor.rollback()
+
+ timedelta6, = self.timedelta_required.create([{
+ 'timedelta': day,
+ }])
+ self.assert_(timedelta6)
+
+ timedelta7, = self.timedelta.create([{
+ 'timedelta': None,
+ }])
+ self.assert_(timedelta7)
+
transaction.cursor.rollback()
diff --git a/trytond/tests/test_history.py b/trytond/tests/test_history.py
index 74c9176..d0f6e7a 100644
--- a/trytond/tests/test_history.py
+++ b/trytond/tests/test_history.py
@@ -1,5 +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.
+# 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
@@ -193,6 +193,40 @@ class HistoryTestCase(unittest.TestCase):
History.restore_history([history_id], datetime.datetime.max)
self.assertRaises(UserError, History.read, [history_id])
+ def test0041restore_history_before(self):
+ 'Test restore history before'
+ 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
+
+ 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()
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ History.restore_history_before([history_id], second)
+ history = History(history_id)
+ self.assertEqual(history.value, 1)
+
@unittest.skipIf(backend.name() in ('sqlite', 'mysql'),
'now() is not the start of the transaction')
def test0045restore_history_same_timestamp(self):
@@ -382,6 +416,63 @@ class HistoryTestCase(unittest.TestCase):
self.assertEqual(history.value, 2)
self.assertEqual([l.name for l in history.lines], ['c'])
+ def test0080_search_cursor_max(self):
+ 'Test search with number of history entries at cursor.IN_MAX'
+ History = POOL.get('test.history')
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ cursor = transaction.cursor
+
+ history = History(value=-1)
+ history.save()
+
+ for history.value in range(cursor.IN_MAX + 1):
+ history.save()
+
+ with transaction.set_context(_datetime=datetime.datetime.max):
+ record, = History.search([])
+
+ self.assertEqual(record.value, cursor.IN_MAX)
+
+ def test0090_search_cursor_max_entries(self):
+ 'Test search for skipping first history entries at cursor.IN_MAX'
+ History = POOL.get('test.history')
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ cursor = transaction.cursor
+
+ for i in xrange(0, 2):
+ history = History(value=-1)
+ history.save()
+
+ for history.value in range(cursor.IN_MAX + 1):
+ history.save()
+
+ with transaction.set_context(_datetime=datetime.datetime.max):
+ records = History.search([])
+
+ self.assertEqual({r.value for r in records}, {cursor.IN_MAX})
+ self.assertEqual(len(records), 2)
+
+ def test0100_search_cursor_max_histories(self):
+ 'Test search with number of histories at cursor.IN_MAX'
+ History = POOL.get('test.history')
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ cursor = transaction.cursor
+
+ n = cursor.IN_MAX + 1
+ History.create([{'value': 1}] * n)
+
+ with transaction.set_context(_datetime=datetime.datetime.max):
+ records = History.search([])
+
+ self.assertEqual({r.value for r in records}, {1})
+ self.assertEqual(len(records), n)
+
def suite():
return unittest.TestLoader().loadTestsFromTestCase(HistoryTestCase)
diff --git a/trytond/tests/test_importdata.py b/trytond/tests/test_importdata.py
index 8718032..594b34f 100644
--- a/trytond/tests/test_importdata.py
+++ b/trytond/tests/test_importdata.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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
diff --git a/trytond/tests/test_ir.py b/trytond/tests/test_ir.py
new file mode 100644
index 0000000..7079397
--- /dev/null
+++ b/trytond/tests/test_ir.py
@@ -0,0 +1,14 @@
+# 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 .test_tryton import ModuleTestCase
+
+
+class IrTestCase(ModuleTestCase):
+ 'Test ir module'
+ module = 'ir'
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(IrTestCase)
diff --git a/trytond/tests/test_mixins.py b/trytond/tests/test_mixins.py
index a699f7b..087e40d 100644
--- a/trytond/tests/test_mixins.py
+++ b/trytond/tests/test_mixins.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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 urllib
diff --git a/trytond/tests/test_modelsingleton.py b/trytond/tests/test_modelsingleton.py
index 673e668..ff4d135 100644
--- a/trytond/tests/test_modelsingleton.py
+++ b/trytond/tests/test_modelsingleton.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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 datetime import datetime
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
diff --git a/trytond/tests/test_modelstorage.py b/trytond/tests/test_modelstorage.py
new file mode 100644
index 0000000..1be6597
--- /dev/null
+++ b/trytond/tests/test_modelstorage.py
@@ -0,0 +1,39 @@
+# 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.pool import Pool
+from trytond.tests.test_tryton import DB_NAME, USER, CONTEXT, install_module
+
+
+class ModelStorageTestCase(unittest.TestCase):
+ 'Test ModelStorage'
+
+ def setUp(self):
+ install_module('tests')
+
+ def test_search_read_order(self):
+ 'Test search_read order'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ pool = Pool()
+ ModelStorage = pool.get('test.modelstorage')
+
+ ModelStorage.create([{'name': i} for i in ['foo', 'bar', 'test']])
+
+ rows = ModelStorage.search_read([])
+ self.assertTrue(
+ all(x['id'] < y['id'] for x, y in zip(rows, rows[1:])))
+
+ rows = ModelStorage.search_read([], order=[('name', 'ASC')])
+ self.assertTrue(
+ all(x['name'] <= y['name'] for x, y in zip(rows, rows[1:])))
+
+ rows = ModelStorage.search_read([], order=[('name', 'DESC')])
+ self.assertTrue(
+ all(x['name'] >= y['name'] for x, y in zip(rows, rows[1:])))
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(ModelStorageTestCase)
diff --git a/trytond/tests/test_modelview.py b/trytond/tests/test_modelview.py
new file mode 100644
index 0000000..300978e
--- /dev/null
+++ b/trytond/tests/test_modelview.py
@@ -0,0 +1,81 @@
+# 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.tests.test_tryton import DB_NAME, USER, CONTEXT, install_module
+from trytond.pool import Pool
+from trytond.transaction import Transaction
+
+
+class ModelView(unittest.TestCase):
+ "Test ModelView"
+
+ def setUp(self):
+ install_module('tests')
+
+ def test_changed_values(self):
+ "Test ModelView._changed_values"
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ pool = Pool()
+ Model = pool.get('test.modelview.changed_values')
+ Target = pool.get('test.modelview.changed_values.target')
+
+ record = Model()
+
+ self.assertEqual(record._changed_values, {})
+
+ record.name = 'foo'
+ record.target = Target(1)
+ record.ref_target = Target(2)
+ record.targets = [Target(name='bar')]
+ self.assertEqual(record._changed_values, {
+ 'name': 'foo',
+ 'target': 1,
+ 'ref_target': 'test.modelview.changed_values.target,2',
+ 'targets': {
+ 'add': [
+ (0, {'name': 'bar'}),
+ ],
+ },
+ })
+
+ record = Model(name='test', target=1, targets=[
+ {'id': 1, 'name': 'foo'},
+ {'id': 2},
+ ], m2m_targets=[5, 6, 7])
+
+ self.assertEqual(record._changed_values, {})
+
+ target = record.targets[0]
+ target.name = 'bar'
+ record.targets = [target]
+ record.m2m_targets = [Target(9), Target(10)]
+ self.assertEqual(record._changed_values, {
+ 'targets': {
+ 'update': [{'id': 1, 'name': 'bar'}],
+ 'remove': [2],
+ },
+ 'm2m_targets': [9, 10],
+ })
+
+ # change only one2many record
+ record = Model(targets=[{'id': 1, 'name': 'foo'}])
+ self.assertEqual(record._changed_values, {})
+
+ target, = record.targets
+ target.name = 'bar'
+ record.targets = record.targets
+ self.assertEqual(record._changed_values, {
+ 'targets': {
+ 'update': [{'id': 1, 'name': 'bar'}],
+ },
+ })
+
+
+def suite():
+ func = unittest.TestLoader().loadTestsFromTestCase
+ suite = unittest.TestSuite()
+ for testcase in (ModelView,):
+ suite.addTests(func(testcase))
+ return suite
diff --git a/trytond/tests/test_mptt.py b/trytond/tests/test_mptt.py
index 6bbd37b..5cc3373 100644
--- a/trytond/tests/test_mptt.py
+++ b/trytond/tests/test_mptt.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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 sys
import unittest
from mock import patch
diff --git a/trytond/tests/test_protocols.py b/trytond/tests/test_protocols.py
index ee445ca..28ae666 100644
--- a/trytond/tests/test_protocols.py
+++ b/trytond/tests/test_protocols.py
@@ -1,5 +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.
+# 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 json
@@ -30,9 +30,10 @@ class JSONTestCase(unittest.TestCase):
'Test time'
self.dumps_loads(datetime.datetime.now().time())
- def test_buffer(self):
- 'Test buffer'
- self.dumps_loads(buffer('foo'))
+ def test_bytes(self):
+ 'Test bytes'
+ self.dumps_loads(bytes(b'foo'))
+ self.dumps_loads(bytearray(b'foo'))
def test_decimal(self):
'Test Decimal'
@@ -52,9 +53,10 @@ class XMLTestCase(unittest.TestCase):
'Test Decimal'
self.dumps_loads(Decimal('3.141592653589793'))
- def test_buffer(self):
- 'Test buffer'
- self.dumps_loads(buffer('foo'))
+ def test_bytes(self):
+ 'Test bytes'
+ self.dumps_loads(bytes(b'foo'))
+ self.dumps_loads(bytearray(b'foo'))
def test_date(self):
'Test date'
diff --git a/trytond/tests/test_pyson.py b/trytond/tests/test_pyson.py
index d295b98..5cf65c2 100644
--- a/trytond/tests/test_pyson.py
+++ b/trytond/tests/test_pyson.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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
@@ -12,34 +12,37 @@ class PYSONTestCase(unittest.TestCase):
def test0010Eval(self):
'Test pyson.Eval'
- self.assert_(pyson.Eval('test').pyson() == {
+ self.assertEqual(pyson.Eval('test').pyson(), {
'__class__': 'Eval',
'v': 'test',
'd': '',
})
- self.assert_(pyson.Eval('test', 'foo').pyson() == {
+ self.assertEqual(pyson.Eval('test', 'foo').pyson(), {
'__class__': 'Eval',
'v': 'test',
'd': 'foo',
})
- self.assert_(pyson.Eval('test', 'foo').types() == set([type('foo')]))
- self.assert_(pyson.Eval('test', 1).types() == set([type(1)]))
+ self.assertEqual(pyson.Eval('test', 'foo').types(), set([basestring]))
+ self.assertEqual(pyson.Eval('test', 1).types(), set([int]))
eval = pyson.PYSONEncoder().encode(pyson.Eval('test', 0))
- self.assert_(pyson.PYSONDecoder({'test': 1}).decode(eval) == 1)
- self.assert_(pyson.PYSONDecoder().decode(eval) == 0)
+ self.assertEqual(pyson.PYSONDecoder({'test': 1}).decode(eval), 1)
+ self.assertEqual(pyson.PYSONDecoder().decode(eval), 0)
+
+ self.assertEqual(repr(pyson.Eval('test', 'foo')),
+ "Eval('test', 'foo')")
def test0020Not(self):
'Test pyson.Not'
- self.assert_(pyson.Not(True).pyson() == {
+ self.assertEqual(pyson.Not(True).pyson(), {
'__class__': 'Not',
'v': True,
})
self.assertRaises(AssertionError, pyson.Not, 'foo')
- self.assert_(pyson.Not(True).types() == set([bool]))
+ self.assertEqual(pyson.Not(True).types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.Not(True))
self.assertFalse(pyson.PYSONDecoder().decode(eval))
@@ -47,14 +50,16 @@ class PYSONTestCase(unittest.TestCase):
eval = pyson.PYSONEncoder().encode(pyson.Not(False))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
+ self.assertEqual(repr(pyson.Not(True)), 'Not(True)')
+
def test0030Bool(self):
'Test pyson.Bool'
- self.assert_(pyson.Bool('test').pyson() == {
+ self.assertEqual(pyson.Bool('test').pyson(), {
'__class__': 'Bool',
'v': 'test',
})
- self.assert_(pyson.Bool('test').types() == set([bool]))
+ self.assertEqual(pyson.Bool('test').types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.Bool(True))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
@@ -86,9 +91,11 @@ class PYSONTestCase(unittest.TestCase):
eval = pyson.PYSONEncoder().encode(pyson.Bool({}))
self.assertFalse(pyson.PYSONDecoder().decode(eval))
+ self.assertEqual(repr(pyson.Bool('test')), "Bool('test')")
+
def test0040And(self):
'Test pyson.And'
- self.assert_(pyson.And(True, False).pyson() == {
+ self.assertEqual(pyson.And(True, False).pyson(), {
'__class__': 'And',
's': [True, False],
})
@@ -99,7 +106,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.And, True)
self.assertRaises(AssertionError, pyson.And)
- self.assert_(pyson.And(True, False).types() == set([bool]))
+ self.assertEqual(pyson.And(True, False).types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.And(True, True))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
@@ -128,9 +135,12 @@ class PYSONTestCase(unittest.TestCase):
eval = pyson.PYSONEncoder().encode(pyson.And(False, False, True))
self.assertFalse(pyson.PYSONDecoder().decode(eval))
+ self.assertEqual(repr(pyson.And(False, True, True)),
+ 'And(False, True, True)')
+
def test0050Or(self):
'Test pyson.Or'
- self.assert_(pyson.Or(True, False).pyson() == {
+ self.assertEqual(pyson.Or(True, False).pyson(), {
'__class__': 'Or',
's': [True, False],
})
@@ -141,7 +151,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.Or, True)
self.assertRaises(AssertionError, pyson.Or)
- self.assert_(pyson.Or(True, False).types() == set([bool]))
+ self.assertEqual(pyson.Or(True, False).types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.Or(True, True))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
@@ -170,9 +180,12 @@ class PYSONTestCase(unittest.TestCase):
eval = pyson.PYSONEncoder().encode(pyson.Or(False, False, True))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
+ self.assertEqual(repr(pyson.Or(False, True, True)),
+ 'Or(False, True, True)')
+
def test0060Equal(self):
'Test pyson.Equal'
- self.assert_(pyson.Equal('test', 'test').pyson() == {
+ self.assertEqual(pyson.Equal('test', 'test').pyson(), {
'__class__': 'Equal',
's1': 'test',
's2': 'test',
@@ -180,7 +193,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.Equal, 'test', True)
- self.assert_(pyson.Equal('test', 'test').types() == set([bool]))
+ self.assertEqual(pyson.Equal('test', 'test').types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.Equal('test', 'test'))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
@@ -188,9 +201,12 @@ class PYSONTestCase(unittest.TestCase):
eval = pyson.PYSONEncoder().encode(pyson.Equal('foo', 'bar'))
self.assertFalse(pyson.PYSONDecoder().decode(eval))
+ self.assertEqual(repr(pyson.Equal('foo', 'bar')),
+ "Equal('foo', 'bar')")
+
def test0070Greater(self):
'Test pyson.Greater'
- self.assert_(pyson.Greater(1, 0).pyson() == {
+ self.assertEqual(pyson.Greater(1, 0).pyson(), {
'__class__': 'Greater',
's1': 1,
's2': 0,
@@ -201,7 +217,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.Greater, 1, 'test')
self.assertRaises(AssertionError, pyson.Greater, 1, 0, 'test')
- self.assert_(pyson.Greater(1, 0).types() == set([bool]))
+ self.assertEqual(pyson.Greater(1, 0).types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.Greater(1, 0))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
@@ -221,9 +237,11 @@ class PYSONTestCase(unittest.TestCase):
eval = pyson.PYSONEncoder().encode(pyson.Greater(1, 1, True))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
+ self.assertEqual(repr(pyson.Greater(1, 0)), 'Greater(1, 0, False)')
+
def test0080Less(self):
'Test pyson.Less'
- self.assert_(pyson.Less(0, 1).pyson() == {
+ self.assertEqual(pyson.Less(0, 1).pyson(), {
'__class__': 'Less',
's1': 0,
's2': 1,
@@ -234,7 +252,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.Less, 0, 'test')
self.assertRaises(AssertionError, pyson.Less, 0, 1, 'test')
- self.assert_(pyson.Less(0, 1).types() == set([bool]))
+ self.assertEqual(pyson.Less(0, 1).types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.Less(0, 1))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
@@ -254,9 +272,11 @@ class PYSONTestCase(unittest.TestCase):
eval = pyson.PYSONEncoder().encode(pyson.Less(1, 1, True))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
+ self.assertEqual(repr(pyson.Less(0, 1)), 'Less(0, 1, False)')
+
def test0090If(self):
'Test pyson.If'
- self.assert_(pyson.If(True, 'foo', 'bar').pyson() == {
+ self.assertEqual(pyson.If(True, 'foo', 'bar').pyson(), {
'__class__': 'If',
'c': True,
't': 'foo',
@@ -267,18 +287,21 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.If, True, 'foo', False)
self.assertEqual(pyson.If(True, 'foo', 'bar').types(),
- set([type('foo')]))
- self.assert_(pyson.If(True, False, True).types() == set([bool]))
+ set([basestring]))
+ self.assertEqual(pyson.If(True, False, True).types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.If(True, 'foo', 'bar'))
- self.assert_(pyson.PYSONDecoder().decode(eval) == 'foo')
+ self.assertEqual(pyson.PYSONDecoder().decode(eval), 'foo')
eval = pyson.PYSONEncoder().encode(pyson.If(False, 'foo', 'bar'))
- self.assert_(pyson.PYSONDecoder().decode(eval) == 'bar')
+ self.assertEqual(pyson.PYSONDecoder().decode(eval), 'bar')
+
+ self.assertEqual(repr(pyson.If(True, 'foo', 'bar')),
+ "If(True, 'foo', 'bar')")
def test0100Get(self):
'Test pyson.Get'
- self.assert_(pyson.Get({'foo': 'bar'}, 'foo', 'default').pyson() == {
+ self.assertEqual(pyson.Get({'foo': 'bar'}, 'foo', 'default').pyson(), {
'__class__': 'Get',
'v': {'foo': 'bar'},
'k': 'foo',
@@ -288,24 +311,28 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.Get, 'test', 'foo', 'default')
self.assertRaises(AssertionError, pyson.Get, {}, 1, 'default')
- self.assert_(pyson.Get({}, 'foo', 'default').types() == set([str]))
- self.assert_(pyson.Get({}, 'foo', True).types() == set([bool]))
+ self.assertEqual(pyson.Get({}, 'foo', 'default').types(),
+ set([basestring]))
+ self.assertEqual(pyson.Get({}, 'foo', True).types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.Get(
{'foo': 'bar'}, 'foo', 'default'))
- self.assert_(pyson.PYSONDecoder().decode(eval) == 'bar')
+ self.assertEqual(pyson.PYSONDecoder().decode(eval), 'bar')
eval = pyson.PYSONEncoder().encode(pyson.Get(
{'foo': 'bar'}, 'test', 'default'))
- self.assert_(pyson.PYSONDecoder().decode(eval) == 'default')
+ self.assertEqual(pyson.PYSONDecoder().decode(eval), 'default')
eval = pyson.PYSONEncoder().encode(pyson.Get(
{}, 'foo', 'default'))
- self.assert_(pyson.PYSONDecoder().decode(eval) == 'default')
+ self.assertEqual(pyson.PYSONDecoder().decode(eval), 'default')
+
+ self.assertEqual(repr(pyson.Get({'foo': 'bar'}, 'foo', 'default')),
+ "Get({'foo': 'bar'}, 'foo', 'default')")
def test0110In(self):
'Test pyson.In'
- self.assert_(pyson.In('foo', {'foo': 'bar'}).pyson() == {
+ self.assertEqual(pyson.In('foo', {'foo': 'bar'}).pyson(), {
'__class__': 'In',
'k': 'foo',
'v': {'foo': 'bar'},
@@ -314,7 +341,7 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.In, object(), {})
self.assertRaises(AssertionError, pyson.In, 'test', 'foo')
- self.assert_(pyson.In('foo', {}).types() == set([bool]))
+ self.assertEqual(pyson.In('foo', {}).types(), set([bool]))
eval = pyson.PYSONEncoder().encode(pyson.In('foo', {'foo': 'bar'}))
self.assertTrue(pyson.PYSONDecoder().decode(eval))
@@ -346,9 +373,12 @@ class PYSONTestCase(unittest.TestCase):
eval = pyson.PYSONEncoder().encode(pyson.In('test', []))
self.assertFalse(pyson.PYSONDecoder().decode(eval))
+ self.assertEqual(repr(pyson.In('foo', ['foo', 'bar'])),
+ "In('foo', ['foo', 'bar'])")
+
def test0120Date(self):
'Test pyson.Date'
- self.assert_(pyson.Date(2010, 1, 12, -1, 12, -7).pyson() == {
+ self.assertEqual(pyson.Date(2010, 1, 12, -1, 12, -7).pyson(), {
'__class__': 'Date',
'y': 2010,
'M': 1,
@@ -371,37 +401,40 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.Date, 2010, 1, 12, -1, 12,
'test')
- self.assert_(pyson.Date(2010, 1, 12, -1, 12, -7).types()
- == set([datetime.date]))
+ self.assertEqual(pyson.Date(2010, 1, 12, -1, 12, -7).types(),
+ set([datetime.date]))
eval = pyson.PYSONEncoder().encode(pyson.Date())
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.date.today())
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.date.today())
eval = pyson.PYSONEncoder().encode(pyson.Date(2010, 1, 12))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.date(2010, 1, 12))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.date(2010, 1, 12))
eval = pyson.PYSONEncoder().encode(pyson.Date(2010, 1, 12, -1))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.date(2009, 1, 12))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.date(2009, 1, 12))
eval = pyson.PYSONEncoder().encode(pyson.Date(2010, 1, 12, 0, 12))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.date(2011, 1, 12))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.date(2011, 1, 12))
eval = pyson.PYSONEncoder().encode(pyson.Date(2010, 1, 12, 0, 0, -7))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.date(2010, 1, 5))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.date(2010, 1, 5))
eval = pyson.PYSONEncoder().encode(datetime.date(2010, 2, 22))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.date(2010, 2, 22))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.date(2010, 2, 22))
+
+ self.assertEqual(repr(pyson.Date(2010, 1, 12, -1, 12, -7)),
+ 'Date(2010, 1, 12, -1, 12, -7)')
def test0130DateTime(self):
'Test pyson.DateTime'
- self.assert_(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0,
- -1, 12, -7, 2, 15, 30, 1).pyson() == {
+ self.assertEqual(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0,
+ -1, 12, -7, 2, 15, 30, 1).pyson(), {
'__class__': 'DateTime',
'y': 2010,
'M': 1,
@@ -448,64 +481,68 @@ class PYSONTestCase(unittest.TestCase):
self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30,
20, 0, -1, 12, -7, 2, 15, 30, 'test')
- self.assert_(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0,
- -1, 12, -7, 2, 15, 30, 1).types() == set([datetime.datetime]))
+ self.assertEqual(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0,
+ -1, 12, -7, 2, 15, 30, 1).types(), set([datetime.datetime]))
eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
10, 30, 20, 0))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.datetime(2010, 1, 12, 10, 30, 20, 0))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.datetime(2010, 1, 12, 10, 30, 20, 0))
eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
10, 30, 20, 0, -1))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.datetime(2009, 1, 12, 10, 30, 20, 0))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.datetime(2009, 1, 12, 10, 30, 20, 0))
eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
10, 30, 20, 0, 0, 12))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.datetime(2011, 1, 12, 10, 30, 20, 0))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.datetime(2011, 1, 12, 10, 30, 20, 0))
eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
10, 30, 20, 0, 0, 0, -7))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.datetime(2010, 1, 5, 10, 30, 20, 0))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.datetime(2010, 1, 5, 10, 30, 20, 0))
eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
10, 30, 20, 0, 0, 0, 0, 12))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.datetime(2010, 1, 12, 22, 30, 20, 0))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.datetime(2010, 1, 12, 22, 30, 20, 0))
eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
10, 30, 20, 0, 0, 0, 0, 0, -30))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.datetime(2010, 1, 12, 10, 0, 20, 0))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.datetime(2010, 1, 12, 10, 0, 20, 0))
eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
10, 30, 20, 0, 0, 0, 0, 0, 0, 30))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.datetime(2010, 1, 12, 10, 30, 50, 0))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.datetime(2010, 1, 12, 10, 30, 50, 0))
eval = pyson.PYSONEncoder().encode(pyson.DateTime(2010, 1, 12,
10, 30, 20, 0, 0, 0, 0, 0, 0, 0, 200))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.datetime(2010, 1, 12, 10, 30, 20, 200))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.datetime(2010, 1, 12, 10, 30, 20, 200))
eval = pyson.PYSONEncoder().encode(datetime.datetime(
2010, 2, 22, 10, 30, 20, 200))
- self.assert_(pyson.PYSONDecoder().decode(eval)
- == datetime.datetime(2010, 2, 22, 10, 30, 20, 200))
+ self.assertEqual(pyson.PYSONDecoder().decode(eval),
+ datetime.datetime(2010, 2, 22, 10, 30, 20, 200))
+
+ self.assertEqual(repr(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0,
+ -1, 12, -7, 2, 15, 30, 1)),
+ 'DateTime(2010, 1, 12, 10, 30, 20, 0, -1, 12, -7, 2, 15, 30, 1)')
def test0140Len(self):
'Test pyson.Len'
- self.assert_(pyson.Len([1, 2, 3]).pyson() == {
+ self.assertEqual(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]))
+ self.assertEqual(pyson.Len([1, 2, 3]).types(), set([int]))
eval = pyson.PYSONEncoder().encode(pyson.Len([1, 2, 3]))
self.assertEqual(pyson.PYSONDecoder().decode(eval), 3)
@@ -516,15 +553,44 @@ class PYSONTestCase(unittest.TestCase):
eval = pyson.PYSONEncoder().encode(pyson.Len('foo bar'))
self.assertEqual(pyson.PYSONDecoder().decode(eval), 7)
+ self.assertEqual(repr(pyson.Len([1, 2, 3])), 'Len([1, 2, 3])')
+
def test0900Composite(self):
'Test Composite'
- eval = pyson.PYSONEncoder().encode(['id', pyson.If(pyson.Not(
- pyson.In('company', pyson.Eval('context', {}))), '=', '!='),
+ expr = pyson.If(pyson.Not(
+ pyson.In('company', pyson.Eval('context', {}))), '=', '!=')
+ eval = pyson.PYSONEncoder().encode(['id', expr,
pyson.Get(pyson.Eval('context', {}), 'company', -1)])
- self.assert_(pyson.PYSONDecoder({'context': {'company': 1}}
- ).decode(eval) == ['id', '!=', 1])
- self.assert_(pyson.PYSONDecoder({'context': {}}
- ).decode(eval) == ['id', '=', -1])
+ self.assertEqual(pyson.PYSONDecoder({'context': {'company': 1}}
+ ).decode(eval), ['id', '!=', 1])
+ self.assertEqual(pyson.PYSONDecoder({'context': {}}
+ ).decode(eval), ['id', '=', -1])
+
+ self.assertEqual(repr(expr),
+ "If(Not(In('company', Eval('context', {}))), '=', '!=')")
+
+ def test_noeval(self):
+ decoder = pyson.PYSONDecoder(noeval=True)
+ encoder = pyson.PYSONEncoder()
+
+ for instance in [
+ pyson.Eval('test', 0),
+ pyson.Not(True),
+ pyson.Bool('test'),
+ pyson.And(True, False, True),
+ pyson.Or(False, True, True),
+ pyson.Equal('foo', 'bar'),
+ pyson.Greater(1, 0),
+ pyson.Less(0, 1),
+ pyson.If(True, 'foo', 'bar'),
+ pyson.Get({'foo': 'bar'}, 'foo', 'default'),
+ pyson.In('foo', ['foo', 'bar']),
+ pyson.Date(),
+ pyson.DateTime(),
+ pyson.Len([1, 2, 3]),
+ ]:
+ self.assertEqual(decoder.decode(encoder.encode(instance)).pyson(),
+ instance.pyson())
def suite():
diff --git a/trytond/tests/test_res.py b/trytond/tests/test_res.py
new file mode 100644
index 0000000..46db413
--- /dev/null
+++ b/trytond/tests/test_res.py
@@ -0,0 +1,14 @@
+# 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 .test_tryton import ModuleTestCase
+
+
+class ResTestCase(ModuleTestCase):
+ 'Test res module'
+ module = 'res'
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(ResTestCase)
diff --git a/trytond/tests/test_sequence.py b/trytond/tests/test_sequence.py
index 802372d..e4aa58f 100644
--- a/trytond/tests/test_sequence.py
+++ b/trytond/tests/test_sequence.py
@@ -1,6 +1,6 @@
# -*- 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.
+# 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, \
diff --git a/trytond/tests/test_tools.py b/trytond/tests/test_tools.py
index 3a5c3ad..28d77e8 100644
--- a/trytond/tests/test_tools.py
+++ b/trytond/tests/test_tools.py
@@ -1,14 +1,15 @@
# -*- 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.
+# 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 doctest
import datetime
import sql
import sql.operators
-from trytond.tools import reduce_ids, safe_eval, datetime_strftime, \
- reduce_domain
+from trytond.tools import reduce_ids, datetime_strftime, \
+ reduce_domain, decimal_, is_instance_method
class ToolsTestCase(unittest.TestCase):
@@ -58,24 +59,6 @@ class ToolsTestCase(unittest.TestCase):
| (self.table.id.in_([15.0, 18.0, 19.0, 21.0]))))
self.assertRaises(AssertionError, reduce_ids, self.table.id, [1.1])
- def test0060safe_eval_builtin(self):
- 'Attempt to access a unsafe builtin'
- self.assertRaises(NameError, safe_eval, "open('test.txt', 'w')")
-
- def test0061safe_eval_getattr(self):
- 'Attempt to get arround direct attr access'
- self.assertRaises(NameError, safe_eval, "getattr(int, 'real')")
-
- def test0062safe_eval_func_globals(self):
- 'Attempt to access global enviroment where fun was defined'
- self.assertRaises(SyntaxError, safe_eval,
- "def x(): pass; print x.func_globals")
-
- def test0063safe_eval_lowlevel(self):
- "Lowlevel tricks to access 'object'"
- self.assertRaises(ValueError, safe_eval,
- "().__class__.mro()[1].__subclasses__()")
-
def test0070datetime_strftime(self):
'Test datetime_strftime'
self.assert_(datetime_strftime(datetime.date(2005, 3, 2),
@@ -111,10 +94,31 @@ class ToolsTestCase(unittest.TestCase):
self.assertEqual(reduce_domain(i), j,
'%s -> %s != %s' % (i, reduce_domain(i), j))
+ def test_is_instance_method(self):
+ 'Test is_instance_method'
+
+ class Foo(object):
+
+ @staticmethod
+ def static():
+ pass
+
+ @classmethod
+ def klass(cls):
+ pass
+
+ def instance(self):
+ pass
+
+ self.assertFalse(is_instance_method(Foo, 'static'))
+ self.assertFalse(is_instance_method(Foo, 'klass'))
+ self.assertTrue(is_instance_method(Foo, 'instance'))
+
def suite():
func = unittest.TestLoader().loadTestsFromTestCase
suite = unittest.TestSuite()
for testcase in (ToolsTestCase,):
suite.addTests(func(testcase))
+ suite.addTest(doctest.DocTestSuite(decimal_))
return suite
diff --git a/trytond/tests/test_transaction.py b/trytond/tests/test_transaction.py
index 311d806..10aef42 100644
--- a/trytond/tests/test_transaction.py
+++ b/trytond/tests/test_transaction.py
@@ -1,5 +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.
+# 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.tests.test_tryton import DB_NAME, USER, CONTEXT, install_module
from trytond.transaction import Transaction
diff --git a/trytond/tests/test_trigger.py b/trytond/tests/test_trigger.py
index 200aedd..4f24741 100644
--- a/trytond/tests/test_trigger.py
+++ b/trytond/tests/test_trigger.py
@@ -1,9 +1,9 @@
# -*- 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.
+# 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
+import datetime
from itertools import combinations
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
@@ -11,6 +11,7 @@ from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
from trytond.tests.trigger import TRIGGER_LOGS
from trytond.transaction import Transaction
from trytond.exceptions import UserError
+from trytond.pyson import PYSONEncoder, Eval
class TriggerTestCase(unittest.TestCase):
@@ -37,7 +38,7 @@ class TriggerTestCase(unittest.TestCase):
'name': 'Test',
'model': model.id,
'on_time': True,
- 'condition': 'True',
+ 'condition': 'true',
'action_model': action_model.id,
'action_function': 'test',
}
@@ -80,7 +81,7 @@ class TriggerTestCase(unittest.TestCase):
'name': 'Test',
'model': model.id,
'on_create': True,
- 'condition': 'True',
+ 'condition': 'true',
'action_model': action_model.id,
'action_function': 'trigger',
}])
@@ -93,8 +94,10 @@ class TriggerTestCase(unittest.TestCase):
TRIGGER_LOGS.pop()
# Trigger with condition
+ condition = PYSONEncoder().encode(
+ Eval('self', {}).get('name') == 'Bar')
self.trigger.write([trigger], {
- 'condition': 'self.name == "Bar"',
+ 'condition': condition,
})
# Matching condition
@@ -112,7 +115,7 @@ class TriggerTestCase(unittest.TestCase):
# With limit number
self.trigger.write([trigger], {
- 'condition': 'True',
+ 'condition': 'true',
'limit_number': 1,
})
triggered, = self.triggered.create([{
@@ -124,7 +127,7 @@ class TriggerTestCase(unittest.TestCase):
# With minimum delay
self.trigger.write([trigger], {
'limit_number': 0,
- 'minimum_delay': 1,
+ 'minimum_time_delay': datetime.timedelta(hours=1),
})
triggered, = self.triggered.create([{
'name': 'Test',
@@ -151,7 +154,7 @@ class TriggerTestCase(unittest.TestCase):
'name': 'Test',
'model': model.id,
'on_write': True,
- 'condition': 'True',
+ 'condition': 'true',
'action_model': action_model.id,
'action_function': 'trigger',
}])
@@ -166,8 +169,10 @@ class TriggerTestCase(unittest.TestCase):
self.assertEqual(TRIGGER_LOGS, [])
# Trigger with condition
+ condition = PYSONEncoder().encode(
+ Eval('self', {}).get('name') == 'Bar')
self.trigger.write([trigger], {
- 'condition': 'self.name == "Bar"',
+ 'condition': condition,
})
# Matching condition
@@ -190,8 +195,10 @@ class TriggerTestCase(unittest.TestCase):
self.assertEqual(TRIGGER_LOGS, [])
# With limit number
+ condition = PYSONEncoder().encode(
+ Eval('self', {}).get('name') == 'Bar')
self.trigger.write([trigger], {
- 'condition': 'self.name == "Bar"',
+ 'condition': condition,
'limit_number': 1,
})
triggered, = self.triggered.create([{
@@ -212,7 +219,7 @@ class TriggerTestCase(unittest.TestCase):
# With minimum delay
self.trigger.write([trigger], {
'limit_number': 0,
- 'minimum_delay': MAXINT,
+ 'minimum_time_delay': datetime.timedelta.max,
})
triggered, = self.triggered.create([{
'name': 'Foo',
@@ -225,7 +232,7 @@ class TriggerTestCase(unittest.TestCase):
TRIGGER_LOGS.pop()
self.trigger.write([trigger], {
- 'minimum_delay': 0.02,
+ 'minimum_time_delay': datetime.timedelta(seconds=1),
})
triggered, = self.triggered.create([{
'name': 'Foo',
@@ -266,7 +273,7 @@ class TriggerTestCase(unittest.TestCase):
'name': 'Test',
'model': model.id,
'on_delete': True,
- 'condition': 'True',
+ 'condition': 'true',
'action_model': action_model.id,
'action_function': 'trigger',
}])
@@ -277,8 +284,10 @@ class TriggerTestCase(unittest.TestCase):
Transaction().delete = {}
# Trigger with condition
+ condition = PYSONEncoder().encode(
+ Eval('self', {}).get('name') == 'Bar')
self.trigger.write([trigger], {
- 'condition': 'self.name == "Bar"',
+ 'condition': condition,
})
triggered, = self.triggered.create([{
@@ -306,7 +315,7 @@ class TriggerTestCase(unittest.TestCase):
# With limit number
self.trigger.write([trigger], {
- 'condition': 'True',
+ 'condition': 'true',
'limit_number': 1,
})
self.triggered.delete([triggered])
@@ -325,7 +334,7 @@ class TriggerTestCase(unittest.TestCase):
# With minimum delay
self.trigger.write([trigger], {
'limit_number': 0,
- 'minimum_delay': 1,
+ 'minimum_time_delay': datetime.timedelta(hours=1),
})
self.triggered.delete([triggered])
self.assertEqual(TRIGGER_LOGS, [([triggered], trigger)])
@@ -351,7 +360,7 @@ class TriggerTestCase(unittest.TestCase):
'name': 'Test',
'model': model.id,
'on_time': True,
- 'condition': 'True',
+ 'condition': 'true',
'action_model': action_model.id,
'action_function': 'trigger',
}])
@@ -364,8 +373,10 @@ class TriggerTestCase(unittest.TestCase):
TRIGGER_LOGS.pop()
# Trigger with condition
+ condition = PYSONEncoder().encode(
+ Eval('self', {}).get('name') == 'Bar')
self.trigger.write([trigger], {
- 'condition': 'self.name == "Bar"',
+ 'condition': condition,
})
# Matching condition
@@ -385,7 +396,7 @@ class TriggerTestCase(unittest.TestCase):
# With limit number
self.trigger.write([trigger], {
- 'condition': 'True',
+ 'condition': 'true',
'limit_number': 1,
})
self.trigger.trigger_time()
@@ -401,7 +412,7 @@ class TriggerTestCase(unittest.TestCase):
# With minimum delay
self.trigger.write([trigger], {
'limit_number': 0,
- 'minimum_delay': MAXINT,
+ 'minimum_time_delay': datetime.timedelta.max,
})
self.trigger.trigger_time()
self.trigger.trigger_time()
@@ -415,7 +426,7 @@ class TriggerTestCase(unittest.TestCase):
]))
self.trigger.write([trigger], {
- 'minimum_delay': 0.02,
+ 'minimum_time_delay': datetime.timedelta(seconds=1),
})
self.trigger.trigger_time()
time.sleep(1.2)
diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py
index 94bfab9..ec91336 100644
--- a/trytond/tests/test_tryton.py
+++ b/trytond/tests/test_tryton.py
@@ -1,22 +1,25 @@
# -*- 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.
+# 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 sys
import unittest
import doctest
+from itertools import chain
+import operator
from lxml import etree
from trytond.pool import Pool
from trytond import backend
+from trytond.model import Workflow
+from trytond.model.fields import get_eval_fields
from trytond.protocols.dispatcher import create, drop
+from trytond.tools import is_instance_method
from trytond.transaction import Transaction
-from trytond.pyson import PYSONEncoder, Eval
-from trytond.exceptions import UserError
from trytond import security
__all__ = ['POOL', 'DB_NAME', 'USER', 'USER_PASSWORD', 'CONTEXT',
- 'install_module', 'test_view', 'test_depends',
+ 'install_module', 'ModuleTestCase',
'doctest_setup', 'doctest_teardown',
'suite', 'all_suite', 'modules_suite']
@@ -31,45 +34,6 @@ POOL = Pool(DB_NAME)
security.check_super = lambda *a, **k: True
-class ModelViewTestCase(unittest.TestCase):
- 'Test ModelView'
-
- def setUp(self):
- install_module('ir')
- install_module('res')
- install_module('webdav')
-
- def test0000test(self):
- 'Test test'
- self.assertRaises(UserError, install_module, 'nosuchmodule')
- self.assertRaises(UserError, test_view, 'nosuchmodule')
-
- def test0010ir(self):
- 'Test ir'
- test_view('ir')
-
- def test0020res(self):
- 'Test res'
- test_view('res')
-
- def test0040webdav(self):
- 'Test webdav'
- test_view('webdav')
-
-
-class FieldDependsTestCase(unittest.TestCase):
- 'Test Field depends'
-
- def setUp(self):
- install_module('ir')
- install_module('res')
- install_module('webdav')
-
- def test0010depends(self):
- 'Test depends'
- test_depends()
-
-
def install_module(name):
'''
Install module for the tested database
@@ -82,7 +46,7 @@ def install_module(name):
modules = Module.search([
('name', '=', name),
])
- assert modules
+ assert modules, "%s not found" % name
modules = Module.search([
('name', '=', name),
@@ -104,73 +68,152 @@ def install_module(name):
transaction.cursor.commit()
-def test_view(module_name):
- '''
- Test validity of all views of the module
- '''
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
- View = POOL.get('ir.ui.view')
- views = View.search([
- ('module', '=', module_name),
- ('model', '!=', ''),
- ])
- assert views, "No views for %s" % module_name
- for view in views:
- view_id = view.inherit and view.inherit.id or view.id
- model = view.model
- Model = POOL.get(model)
- res = Model.fields_view_get(view_id)
- assert res['model'] == model
- tree = etree.fromstring(res['arch'])
- tree_root = tree.getroottree().getroot()
-
- for element in tree_root.iter():
- if element.tag in ('field', 'label', 'separator', 'group'):
- for attr in ('name', 'icon'):
- field = element.get(attr)
- if field:
- assert field in res['fields'], ('Missing field: %s'
- % field)
- transaction.cursor.rollback()
-
-
-def test_depends():
- '''
- Test for missing depends
- '''
- class Encoder(PYSONEncoder):
-
- def __init__(self, *args, **kwargs):
- super(Encoder, self).__init__(*args, **kwargs)
- self.fields = set()
-
- def default(self, obj):
- if isinstance(obj, Eval):
- fname = obj._value
- if not fname.startswith('_parent_'):
- self.fields.add(fname)
- return super(Encoder, self).default(obj)
-
- with Transaction().start(DB_NAME, USER, context=CONTEXT):
- for mname, model in Pool().iterobject():
- for fname, field in model._fields.iteritems():
- encoder = Encoder()
- encoder.encode(field.domain)
- if hasattr(field, 'digits'):
- encoder.encode(field.digits)
- if hasattr(field, 'add_remove'):
- encoder.encode(field.add_remove)
- encoder.fields.discard(fname)
- encoder.fields.discard('context')
- encoder.fields.discard('_user')
- depends = set(field.depends)
- assert encoder.fields <= depends, (
- 'Missing depends %s in "%s"."%s"' % (
- list(encoder.fields - depends), mname, fname))
- assert depends <= set(model._fields), (
- 'Unknown depends %s in "%s"."%s"' % (
- list(depends - set(model._fields)), mname, fname))
+class ModuleTestCase(unittest.TestCase):
+ 'Trytond Test Case'
+ module = None
+
+ def setUp(self):
+ install_module(self.module)
+
+ def test_rec_name(self):
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ for mname, model in Pool().iterobject():
+ # Don't test model not registered by the module
+ if not any(issubclass(model, cls) for cls in
+ Pool().classes['model'].get(self.module, [])):
+ continue
+ # Skip testing default value even if the field doesn't exist
+ # as there is a fallback to id
+ if model._rec_name == 'name':
+ continue
+ assert model._rec_name in model._fields, (
+ 'Wrong _rec_name "%s" for %s'
+ % (model._rec_name, mname))
+
+ def test_view(self):
+ 'Test validity of all views of the module'
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ View = POOL.get('ir.ui.view')
+ views = View.search([
+ ('module', '=', self.module),
+ ('model', '!=', ''),
+ ])
+ for view in views:
+ view_id = view.inherit and view.inherit.id or view.id
+ model = view.model
+ Model = POOL.get(model)
+ res = Model.fields_view_get(view_id)
+ assert res['model'] == model
+ tree = etree.fromstring(res['arch'])
+ tree_root = tree.getroottree().getroot()
+
+ for element in tree_root.iter():
+ if element.tag in ('field', 'label', 'separator', 'group'):
+ for attr in ('name', 'icon'):
+ field = element.get(attr)
+ if field:
+ assert field in res['fields'], (
+ 'Missing field: %s' % field)
+ transaction.cursor.rollback()
+
+ def test_depends(self):
+ 'Test for missing depends'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ for mname, model in Pool().iterobject():
+ # Don't test model not registered by the module
+ if not any(issubclass(model, cls) for cls in
+ Pool().classes['model'].get(self.module, [])):
+ continue
+ for fname, field in model._fields.iteritems():
+ fields = set()
+ fields |= get_eval_fields(field.domain)
+ if hasattr(field, 'digits'):
+ fields |= get_eval_fields(field.digits)
+ if hasattr(field, 'add_remove'):
+ fields |= get_eval_fields(field.add_remove)
+ fields.discard(fname)
+ fields.discard('context')
+ fields.discard('_user')
+ depends = set(field.depends)
+ assert fields <= depends, (
+ 'Missing depends %s in "%s"."%s"' % (
+ list(fields - depends), mname, fname))
+ assert depends <= set(model._fields), (
+ 'Unknown depends %s in "%s"."%s"' % (
+ list(depends - set(model._fields)), mname, fname))
+
+ def test_menu_action(self):
+ 'Test that menu actions are accessible to menu\'s group'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ pool = Pool()
+ Menu = pool.get('ir.ui.menu')
+ ModelData = pool.get('ir.model.data')
+
+ module_menus = ModelData.search([
+ ('model', '=', 'ir.ui.menu'),
+ ('module', '=', self.module),
+ ])
+ menus = Menu.browse([mm.db_id for mm in module_menus])
+ for menu, module_menu in zip(menus, module_menus):
+ if not menu.action_keywords:
+ continue
+ menu_groups = set(menu.groups)
+ actions_groups = reduce(operator.or_,
+ (set(k.action.groups) for k in menu.action_keywords
+ if k.keyword == 'tree_open'))
+ if not actions_groups:
+ continue
+ assert menu_groups <= actions_groups, (
+ 'Menu "%(menu_xml_id)s" actions are not accessible to '
+ '%(groups)s' % {
+ 'menu_xml_id': module_menu.fs_id,
+ 'groups': ','.join(g.name
+ for g in menu_groups - actions_groups),
+ })
+
+ def test_model_access(self):
+ 'Test missing default model access'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ Access = POOL.get('ir.model.access')
+ no_groups = {a.model.name for a in Access.search([
+ ('group', '=', None),
+ ])}
+ with_groups = {a.model.name for a in Access.search([
+ ('group', '!=', None),
+ ])}
+
+ assert no_groups >= with_groups, (
+ 'Model "%(models)s" are missing a default access' % {
+ 'models': list(with_groups - no_groups),
+ })
+
+ def test_workflow_transitions(self):
+ 'Test all workflow transitions exist'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ for mname, model in Pool().iterobject():
+ # Don't test model not registered by the module
+ if not any(issubclass(model, cls) for cls in
+ Pool().classes['model'].get(self.module, [])):
+ continue
+ if not issubclass(model, Workflow):
+ continue
+ field = getattr(model, model._transition_state)
+ if isinstance(field.selection, (tuple, list)):
+ values = field.selection
+ else:
+ # instance method may not return all the possible values
+ if is_instance_method(model, field.selection):
+ continue
+ values = getattr(model, field.selection)()
+ states = set(dict(values))
+ transition_states = set(chain(*model._transitions))
+ assert transition_states <= states, (
+ ('Unknown transition states "%(states)s" '
+ 'in model "%(model)s". ') % {
+ 'states': list(transition_states - states),
+ 'model': model.__name__,
+ })
def db_exist():
@@ -193,7 +236,7 @@ def drop_db():
def drop_create():
- if db_exist:
+ if db_exist():
drop_db()
create_db()
@@ -224,7 +267,7 @@ def all_suite(modules=None):
return suite_
-def modules_suite(modules=None):
+def modules_suite(modules=None, doc=True):
'''
Return all tests suite of all modules
'''
@@ -265,7 +308,8 @@ def modules_suite(modules=None):
doc_tests = []
for test in suite_:
if isinstance(test, doctest.DocTestCase):
- doc_tests.append(test)
+ if doc:
+ doc_tests.append(test)
else:
tests.append(test)
tests.extend(doc_tests)
diff --git a/trytond/tests/test_union.py b/trytond/tests/test_union.py
index 723c38c..fd05707 100644
--- a/trytond/tests/test_union.py
+++ b/trytond/tests/test_union.py
@@ -1,5 +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.
+# 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.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
diff --git a/trytond/tests/test_webdav.py b/trytond/tests/test_webdav.py
new file mode 100644
index 0000000..4957b36
--- /dev/null
+++ b/trytond/tests/test_webdav.py
@@ -0,0 +1,14 @@
+# 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 .test_tryton import ModuleTestCase
+
+
+class WebDAVTestCase(ModuleTestCase):
+ 'Test ir module'
+ module = 'webdav'
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(WebDAVTestCase)
diff --git a/trytond/tests/trigger.py b/trytond/tests/trigger.py
index 3ec71e6..977fbf9 100644
--- a/trytond/tests/trigger.py
+++ b/trytond/tests/trigger.py
@@ -1,5 +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.
+# 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__ = [
diff --git a/trytond/tests/wizard.py b/trytond/tests/wizard.py
index 5ba5485..c4760b7 100644
--- a/trytond/tests/wizard.py
+++ b/trytond/tests/wizard.py
@@ -1,5 +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.
+# 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 ModelView, ModelSQL, fields
from trytond.wizard import Wizard, StateView, StateTransition, StateAction, \
Button
@@ -15,7 +15,7 @@ class TestWizardStart(ModelSQL, ModelView):
__name__ = 'test.test_wizard.start'
name = fields.Char('Test me')
user = fields.Many2One('res.user', 'User')
- groups = fields.One2Many('res.group', None, 'Groups')
+ groups = fields.Many2Many('res.group', None, None, 'Groups')
@staticmethod
def default_user():
diff --git a/trytond/tools/StringMatcher.py b/trytond/tools/StringMatcher.py
index 2258a99..ff785f7 100644
--- a/trytond/tools/StringMatcher.py
+++ b/trytond/tools/StringMatcher.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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
# Code come from python-Levenshtein
-from Levenshtein import *
+from Levenshtein import opcodes, editops, matching_blocks, ratio, distance
from warnings import warn
diff --git a/trytond/tools/__init__.py b/trytond/tools/__init__.py
index e209cff..c5c9170 100644
--- a/trytond/tools/__init__.py
+++ b/trytond/tools/__init__.py
@@ -1,7 +1,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.
+# 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 .misc import *
from .datetime_strftime import *
+from .decimal_ import *
class ClassProperty(property):
diff --git a/trytond/tools/datetime_strftime.py b/trytond/tools/datetime_strftime.py
index 2f7f3d4..9bd01fc 100644
--- a/trytond/tools/datetime_strftime.py
+++ b/trytond/tools/datetime_strftime.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.
-#Copyright (c) 2002-2007 John D. Hunter; All Rights Reserved
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+# Copyright (c) 2002-2007 John D. Hunter; All Rights Reserved
import time
diff --git a/trytond/tools/decimal_.py b/trytond/tools/decimal_.py
new file mode 100644
index 0000000..ee156c0
--- /dev/null
+++ b/trytond/tools/decimal_.py
@@ -0,0 +1,36 @@
+# 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 tokenize
+from io import StringIO
+
+# code snippet taken from http://docs.python.org/library/tokenize.html
+
+
+def decistmt(s):
+ """Substitute Decimals for floats in a string of statements.
+
+ >>> from decimal import Decimal
+ >>> s = 'print +21.3e-5*-.1234/81.7'
+ >>> decistmt(s)
+ u"print +Decimal (u'21.3e-5')*-Decimal (u'.1234')/Decimal (u'81.7')"
+
+ >>> exec(s)
+ -3.21716034272e-07
+ >>> exec(decistmt(s))
+ -3.217160342717258261933904529E-7
+ """
+ result = []
+ # tokenize the string
+ g = tokenize.generate_tokens(StringIO(s.decode('utf-8')).readline)
+ for toknum, tokval, _, _, _ in g:
+ # replace NUMBER tokens
+ if toknum == tokenize.NUMBER and '.' in tokval:
+ result.extend([
+ (tokenize.NAME, 'Decimal'),
+ (tokenize.OP, '('),
+ (tokenize.STRING, repr(tokval)),
+ (tokenize.OP, ')')
+ ])
+ else:
+ result.append((toknum, tokval))
+ return tokenize.untokenize(result)
diff --git a/trytond/tools/misc.py b/trytond/tools/misc.py
index ebe7c66..a590395 100644
--- a/trytond/tools/misc.py
+++ b/trytond/tools/misc.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.
# -*- 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.
"""
Miscelleanous tools used by tryton
"""
@@ -9,10 +9,10 @@ import sys
import subprocess
from threading import local
import smtplib
-import dis
-from decimal import Decimal
from array import array
from itertools import islice
+import types
+
from sql import Literal
from sql.operators import Or
@@ -333,58 +333,6 @@ def reduce_ids(field, ids):
sql.append(field.in_(discontinue_list))
return sql
-_ALLOWED_CODES = set(dis.opmap[x] for x in [
- 'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP', 'BUILD_LIST',
- 'BUILD_MAP', 'BUILD_TUPLE', 'LOAD_CONST', 'RETURN_VALUE',
- 'STORE_SUBSCR', 'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
- 'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY', 'BINARY_DIVIDE',
- 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE', 'BINARY_MODULO',
- 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_LSHIFT', 'BINARY_RSHIFT',
- 'BINARY_AND', 'BINARY_XOR', 'BINARY_OR', 'STORE_MAP', 'LOAD_NAME',
- 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR', 'STORE_NAME', 'GET_ITER',
- 'FOR_ITER', 'LIST_APPEND', 'JUMP_ABSOLUTE', 'DELETE_NAME',
- 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_IF_FALSE_OR_POP',
- 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE', 'POP_JUMP_IF_TRUE',
- 'BINARY_SUBSCR', 'JUMP_FORWARD',
- ] if x in dis.opmap)
-
-
- at memoize(1000)
-def _compile_source(source):
- comp = compile(source, '', 'eval')
- codes = []
- co_code = comp.co_code
- i = 0
- while i < len(co_code):
- code = ord(co_code[i])
- codes.append(code)
- if code >= dis.HAVE_ARGUMENT:
- i += 3
- else:
- i += 1
- for code in codes:
- if code not in _ALLOWED_CODES:
- raise ValueError('opcode %s not allowed' % dis.opname[code])
- return comp
-
-
-def safe_eval(source, data=None):
- if '__' in source:
- raise ValueError('Double underscores not allowed')
-
- comp = _compile_source(source)
- return eval(comp, {'__builtins__': {
- 'True': True,
- 'False': False,
- 'str': str,
- 'globals': locals,
- 'locals': locals,
- 'bool': bool,
- 'dict': dict,
- 'round': round,
- 'Decimal': Decimal,
- }}, data)
-
def reduce_domain(domain):
'''
@@ -402,10 +350,10 @@ def reduce_domain(domain):
(isinstance(arg, list) and
len(arg) > 2 and
arg[1] in OPERATORS)):
- #clause
+ # clause
result.append(arg)
elif isinstance(arg, list) and arg:
- #sub-domain
+ # sub-domain
sub_domain = reduce_domain(arg)
sub_operator = sub_domain[0]
if sub_operator == operator:
@@ -423,3 +371,10 @@ def grouped_slice(records, count=None):
count = Transaction().cursor.IN_MAX
for i in xrange(0, len(records), count):
yield islice(records, i, i + count)
+
+
+def is_instance_method(cls, method):
+ for klass in cls.__mro__:
+ type_ = klass.__dict__.get(method)
+ if type_ is not None:
+ return isinstance(type_, types.FunctionType)
diff --git a/trytond/tools/singleton.py b/trytond/tools/singleton.py
index 78de8f0..0066990 100644
--- a/trytond/tools/singleton.py
+++ b/trytond/tools/singleton.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
class Singleton(type):
diff --git a/trytond/transaction.py b/trytond/transaction.py
index 39b5aab..ef4ca63 100644
--- a/trytond/transaction.py
+++ b/trytond/transaction.py
@@ -1,5 +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.
+# 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 threading import local
from sql import Flavor
diff --git a/trytond/url.py b/trytond/url.py
index be17878..2e40fad 100644
--- a/trytond/url.py
+++ b/trytond/url.py
@@ -1,5 +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.
+# 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 encodings.idna
import urllib
diff --git a/trytond/version.py b/trytond/version.py
deleted file mode 100644
index d6fb88c..0000000
--- a/trytond/version.py
+++ /dev/null
@@ -1,6 +0,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.
-PACKAGE = "trytond"
-VERSION = "3.4.3"
-LICENSE = "GPL-3"
-WEBSITE = "http://www.tryton.org/"
diff --git a/trytond/webdav/__init__.py b/trytond/webdav/__init__.py
index 424fe2c..c1fe716 100644
--- a/trytond/webdav/__init__.py
+++ b/trytond/webdav/__init__.py
@@ -1,5 +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.
+# 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 ..pool import Pool
from .webdav import *
diff --git a/trytond/webdav/locale/bg_BG.po b/trytond/webdav/locale/bg_BG.po
index 1703c6f..3c7cb47 100644
--- a/trytond/webdav/locale/bg_BG.po
+++ b/trytond/webdav/locale/bg_BG.po
@@ -4,7 +4,7 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"You can not create an attachment named \"%(attachment)s\" in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
diff --git a/trytond/webdav/locale/ca_ES.po b/trytond/webdav/locale/ca_ES.po
index cd15ec2..8eff949 100644
--- a/trytond/webdav/locale/ca_ES.po
+++ b/trytond/webdav/locale/ca_ES.po
@@ -4,10 +4,10 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"You can not create an attachment named \"%(attachment)s\" in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
-"No podeu crear un adjunt anomenat \"%(attachment)s al directori "
+"No podeu crear un adjunt anomenat \"%(attachment)s\" al directori "
"\"%(collection)s\" perquè ja existeix un altre amb aquest nom."
msgctxt "error:webdav.collection:"
diff --git a/trytond/webdav/locale/cs_CZ.po b/trytond/webdav/locale/cs_CZ.po
index eebd971..d437783 100644
--- a/trytond/webdav/locale/cs_CZ.po
+++ b/trytond/webdav/locale/cs_CZ.po
@@ -4,7 +4,7 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"You can not create an attachment named \"%(attachment)s\" in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
diff --git a/trytond/webdav/locale/de_DE.po b/trytond/webdav/locale/de_DE.po
index bbfea9c..8335794 100644
--- a/trytond/webdav/locale/de_DE.po
+++ b/trytond/webdav/locale/de_DE.po
@@ -4,11 +4,11 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"You can not create an attachment named \"%(attachment)s\" in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
-"Anhang \"%(attachment)s\" in Sammlung \"%(collection)s\" kann nicht angelegt"
-" werden, da es bereits eine Sammlung mit diesem Namen gibt."
+"Erstellung von Anhang \"%(attachment)s\" in Sammlung \"%(collection)s\" "
+"nicht möglich, weil es bereits eine Sammlung mit diesem Namen gibt"
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
diff --git a/trytond/webdav/locale/es_AR.po b/trytond/webdav/locale/es_AR.po
index 593b79d..3174fa4 100644
--- a/trytond/webdav/locale/es_AR.po
+++ b/trytond/webdav/locale/es_AR.po
@@ -4,7 +4,7 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"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 archivo adjunto llamado «%(attachment)s» en la colección "
@@ -92,7 +92,7 @@ msgstr "Usuario creación"
msgctxt "field:webdav.share,expiration_date:"
msgid "Expiration Date"
-msgstr "Fecha expiración"
+msgstr "Fecha vencimiento"
msgctxt "field:webdav.share,id:"
msgid "ID"
diff --git a/trytond/webdav/locale/es_CO.po b/trytond/webdav/locale/es_CO.po
index c2d65ff..3e97429 100644
--- a/trytond/webdav/locale/es_CO.po
+++ b/trytond/webdav/locale/es_CO.po
@@ -4,11 +4,11 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"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\" en la colección "
-"\"%(collections)s\" con el porque hay una colección con el ese nombre."
+"No puede crear un adjunto nombrado \"%(attachments)s\" en el repositorio "
+"\"%(collections)s\" porque hay actualmente un repositorio con ese nombre."
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
diff --git a/trytond/webdav/locale/es_EC.po b/trytond/webdav/locale/es_EC.po
index cac92a0..eeadcc9 100644
--- a/trytond/webdav/locale/es_EC.po
+++ b/trytond/webdav/locale/es_EC.po
@@ -4,11 +4,11 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"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\" en la colección "
-"\"%(collections)s\" porque ya existe una colección con este nombre."
+"\"%(collection)s\" porque ya existe una colección con ese nombre."
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
@@ -28,7 +28,7 @@ msgstr "Ruta"
msgctxt "field:ir.attachment,shares:"
msgid "Shares"
-msgstr "Recursos Compartidos"
+msgstr "Recursos compartidos"
msgctxt "field:ir.attachment,url:"
msgid "URL"
@@ -40,15 +40,15 @@ 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"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:webdav.collection,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:webdav.collection,domain:"
msgid "Domain"
@@ -76,23 +76,23 @@ msgstr "Nombre"
msgctxt "field:webdav.collection,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:webdav.collection,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "field:webdav.share,create_date:"
msgid "Create Date"
-msgstr "Fecha de Creación"
+msgstr "Fecha de creación"
msgctxt "field:webdav.share,create_uid:"
msgid "Create User"
-msgstr "Creado por Usuario"
+msgstr "Creado por usuario"
msgctxt "field:webdav.share,expiration_date:"
msgid "Expiration Date"
-msgstr "Fecha de Expiración"
+msgstr "Fecha de expiración"
msgctxt "field:webdav.share,id:"
msgid "ID"
@@ -124,11 +124,11 @@ msgstr "Usuario"
msgctxt "field:webdav.share,write_date:"
msgid "Write Date"
-msgstr "Fecha de Modificación"
+msgstr "Fecha de modificación"
msgctxt "field:webdav.share,write_uid:"
msgid "Write User"
-msgstr "Modificado por Usuario"
+msgstr "Modificado por usuario"
msgctxt "model:ir.action,name:act_collection_list"
msgid "Collections"
@@ -140,7 +140,7 @@ msgstr "Colecciones"
msgctxt "model:ir.action,name:act_share_list"
msgid "Shares"
-msgstr "Recursos Compartidos"
+msgstr "Recursos compartidos"
msgctxt "model:ir.ui.menu,name:menu_collection_list"
msgid "Collections"
@@ -152,7 +152,7 @@ msgstr "Colecciones"
msgctxt "model:ir.ui.menu,name:menu_share_list"
msgid "Shares"
-msgstr "Recursos Compartidos"
+msgstr "Recursos compartidos"
msgctxt "model:ir.ui.menu,name:menu_webdav"
msgid "WebDAV"
@@ -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,8 +180,8 @@ msgstr "Colecciones"
msgctxt "view:webdav.share:"
msgid "Share"
-msgstr "Recurso Compartido"
+msgstr "Recurso compartido"
msgctxt "view:webdav.share:"
msgid "Shares"
-msgstr "Recursos Compartidos"
+msgstr "Recursos compartidos"
diff --git a/trytond/webdav/locale/es_ES.po b/trytond/webdav/locale/es_ES.po
index 3b7312a..2dbda01 100644
--- a/trytond/webdav/locale/es_ES.po
+++ b/trytond/webdav/locale/es_ES.po
@@ -4,11 +4,11 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"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 en el directorio "
-"\"%(collection)s\" porque ya existe otro con este nombre."
+"No puede crear un adjunto llamado \"%(attachment)s\" en el directorio "
+"\"%(collection)s\" porque ya existe un directorio con este nombre."
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
diff --git a/trytond/webdav/locale/fr_FR.po b/trytond/webdav/locale/fr_FR.po
index 1d0062d..512036e 100644
--- a/trytond/webdav/locale/fr_FR.po
+++ b/trytond/webdav/locale/fr_FR.po
@@ -4,10 +4,10 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"You can not create an attachment named \"%(attachment)s\" in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
-"Vous ne pouvez créer une pièce jointe nommée « %(attachment)s » dans la "
+"Vous ne pouvez pas créer un attachement nommé « %(attachment)s » dans la "
"collection « %(collection)s » car il y a déjà une collection avec ce nom."
msgctxt "error:webdav.collection:"
diff --git a/trytond/webdav/locale/nl_NL.po b/trytond/webdav/locale/nl_NL.po
index 91d0c52..139c028 100644
--- a/trytond/webdav/locale/nl_NL.po
+++ b/trytond/webdav/locale/nl_NL.po
@@ -4,7 +4,7 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"You can not create an attachment named \"%(attachment)s\" in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
diff --git a/trytond/webdav/locale/ru_RU.po b/trytond/webdav/locale/ru_RU.po
index 153c248..8cda1ed 100644
--- a/trytond/webdav/locale/ru_RU.po
+++ b/trytond/webdav/locale/ru_RU.po
@@ -4,11 +4,9 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"You can not create an attachment named \"%(attachment)s\" in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
-"Вы не можете создать вложение \"%(attachment)s\" в коллекции "
-"\"%(collection)s\" так как существует коллекция с таким именем."
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
diff --git a/trytond/webdav/locale/sl_SI.po b/trytond/webdav/locale/sl_SI.po
index b1f2bb1..ffa39d4 100644
--- a/trytond/webdav/locale/sl_SI.po
+++ b/trytond/webdav/locale/sl_SI.po
@@ -4,11 +4,11 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:ir.attachment:"
msgid ""
-"You can not create an attachment named \"%(attachment)s in collection "
+"You can not create an attachment named \"%(attachment)s\" in collection "
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
-"Priloge z imenom \"%(attachment)s\" v zbirki \"%(collection)s\" ni možno "
-"izdelati, ker že obstaja zbirka z istim imenom."
+"Priloge \"%(attachment)s\" ni možno pripeti v zbirko \"%(collection)s\", ker"
+" že obstaja zbirka s tem imenom."
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
diff --git a/trytond/webdav/webdav.py b/trytond/webdav/webdav.py
index 9817f49..f34561c 100644
--- a/trytond/webdav/webdav.py
+++ b/trytond/webdav/webdav.py
@@ -1,5 +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.
+# 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
import urllib
@@ -709,7 +709,7 @@ class Attachment(ModelSQL, ModelView):
super(Attachment, cls).__setup__()
cls._error_messages.update({
'collection_attachment_name': ('You can not create an '
- 'attachment named "%(attachment)s in collection '
+ 'attachment named "%(attachment)s" in collection '
'"%(collection)s" because there is already a collection '
'with that name.')
})
diff --git a/trytond/webdav/webdav.xml b/trytond/webdav/webdav.xml
index 2a6ab87..5345da9 100644
--- a/trytond/webdav/webdav.xml
+++ b/trytond/webdav/webdav.xml
@@ -32,7 +32,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">Collections</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">webdav.collection</field>
- <field name="domain">[('parent', '=', None)]</field>
+ <field name="domain" eval="[('parent', '=', None)]" pyson="1"/>
</record>
<record model="ir.action.act_window.view"
id="act_collection_tree_view1">
@@ -116,7 +116,9 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.rule" id="rule_share">
- <field name="domain">[('user', '=', user.id)]</field>
+ <field name="domain"
+ eval="[('user', '=', Eval('user', {}).get('id', -1))]"
+ pyson="1"/>
<field name="rule_group" ref="rule_group_share"/>
</record>
diff --git a/trytond/wizard/__init__.py b/trytond/wizard/__init__.py
index 47c219d..79a16bd 100644
--- a/trytond/wizard/__init__.py
+++ b/trytond/wizard/__init__.py
@@ -1,5 +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.
+# 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 .wizard import Wizard, StateView, StateTransition, StateAction, Button
__all__ = ['Wizard', 'StateView', 'StateTransition', 'StateAction', 'Button']
diff --git a/trytond/wizard/wizard.py b/trytond/wizard/wizard.py
index e92c35c..6d92ed3 100644
--- a/trytond/wizard/wizard.py
+++ b/trytond/wizard/wizard.py
@@ -1,5 +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.
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
__all__ = ['Wizard', 'StateView', 'StateTransition', 'StateAction', 'Button']
@@ -67,14 +67,17 @@ class StateView(State):
assert len(self.buttons) == len(set(b.state for b in self.buttons))
assert len([b for b in self.buttons if b.default]) <= 1
- def get_view(self):
+ def get_view(self, wizard, state_name):
'''
Returns the view definition
'''
Model_ = Pool().get(self.model_name)
ModelData = Pool().get('ir.model.data')
- module, fs_id = self.view.split('.')
- view_id = ModelData.get_id(module, fs_id)
+ if self.view:
+ module, fs_id = self.view.split('.')
+ view_id = ModelData.get_id(module, fs_id)
+ else:
+ view_id = None
return Model_.fields_view_get(view_id=view_id, view_type='form')
def get_defaults(self, wizard, state_name, fields):
@@ -256,7 +259,7 @@ class Wizard(WarningErrorMixin, URLMixin, PoolBase):
result = {}
if isinstance(state, StateView):
- view = state.get_view()
+ view = state.get_view(self, state_name)
defaults = state.get_defaults(self, state_name,
view['fields'].keys())
buttons = state.get_buttons(self, state_name)
--
tryton-server
More information about the tryton-debian-vcs
mailing list