[tryton-debian-vcs] tryton-server branch debian updated. debian/3.2.3-1-16-gafc4ae8
Mathias Behrle
tryton-debian-vcs at alioth.debian.org
Thu Oct 23 12:19:46 UTC 2014
The following commit has been merged in the debian branch:
https://alioth.debian.org/plugins/scmgit/cgi-bin/gitweb.cgi/?p=tryton/tryton-server.git;a=commitdiff;h=debian/3.2.3-1-16-gafc4ae8
commit afc4ae869477e5cda284ad3cd2f04f7c39df847a
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Thu Oct 23 13:32:12 2014 +0200
Releasing debian version 3.4.0-1.
Signed-off-by: Mathias Behrle <mathiasb at m9s.biz>
diff --git a/debian/changelog b/debian/changelog
index 1c0427c..689b660 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,23 @@
+tryton-server (3.4.0-1) unstable; urgency=medium
+
+ * Merging upstream version 3.4.0.
+ * Adding a default configuration file.
+ * Removing the configuration patches, no more needed.
+ * Updating copyright, the backport of orderddict was removed.
+ * Updating man page.
+ * Adding a logging configuration file needed for version 3.4.
+ * Correcting the SSL settings in trytond.conf to keep old behavior.
+ * Removing old logrotate configuration, no more needed.
+ * Using the subdirectory /etc/tryton for all configuration files.
+ * Updating README.Debian for version 3.4.
+ * Adding NEWS for the new version 3.4.
+ * Adding 01_migrate_obsolete_ldap_connection patch.
+ * Setting ownership and permissions for the logging configuration file.
+ * Removing also dpkg-statoverrides for trytond_log.conf on purge.
+ * Adapting tryton-server.default to the new setup.
+
+ -- Mathias Behrle <mathiasb at m9s.biz> Thu, 23 Oct 2014 13:22:34 +0200
+
tryton-server (3.2.3-1) unstable; urgency=high
* Adding actual upstream signing key.
commit 40653b71a4586c1a4215a2da66c5342bec7057be
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Thu Oct 23 13:21:35 2014 +0200
Adapting tryton-server.default to the new setup.
diff --git a/debian/tryton-server.default b/debian/tryton-server.default
index 9a86a49..5e4f327 100644
--- a/debian/tryton-server.default
+++ b/debian/tryton-server.default
@@ -8,15 +8,15 @@
# Specify the user name (Default: tryton).
DAEMONUSER="tryton"
-# Specify an alternate config file (Default: /etc/trytond.conf).
-CONFIGFILE="/etc/trytond.conf"
+# Specify an alternate config file (Default: /etc/tryton/trytond.conf).
+CONFIGFILE="/etc/tryton/trytond.conf"
-# Specify the log file (Default: /var/log/tryton/trytond.log).
-LOGFILE="/var/log/tryton/trytond.log"
+# Specify the log configuration file (Default: /etc/tryton/trytond_log.conf).
+LOGCONF="/etc/tryton/trytond_log.conf"
# Specify the locale for the server to run (Default: en_US).
#LANG="de_DE.UTF-8"
# Additional options that are passed to the Daemon.
# i.e. to increase the verbosity of the server log add -v
-DAEMON_OPTS=" --config=${CONFIGFILE} --logfile=${LOGFILE}"
+DAEMON_OPTS=" --config ${CONFIGFILE} --logconf ${LOGCONF}"
commit 1b1b3001c8af8d01842718c28dee4c1a5c34f41d
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Thu Oct 23 12:50:45 2014 +0200
Removing also dpkg-statoverrides for trytond_log.conf on purge.
diff --git a/debian/tryton-server.postrm b/debian/tryton-server.postrm
index 6ccab76..1ee15cf 100644
--- a/debian/tryton-server.postrm
+++ b/debian/tryton-server.postrm
@@ -5,13 +5,14 @@ set -e
TRYTON_USER="tryton"
TRYTON_OLDCONFFILE="/etc/trytond.conf"
TRYTON_CONFFILE="/etc/tryton/trytond.conf"
+TRYTON_LOGCONFFILE="/etc/tryton/trytond_log.conf"
TRYTON_LOGDIR="/var/log/tryton"
TRYTON_HOMEDIR="/var/lib/tryton"
case "${1}" in
purge)
# Removing evtl. dpkg-statoverrides
- for _ITEM in "${TRYTON_OLDCONFFILE}" "${TRYTON_CONFFILE}" "${TRYTON_HOMEDIR}" "${TRYTON_LOGDIR}"
+ for _ITEM in "${TRYTON_OLDCONFFILE}" "${TRYTON_CONFFILE}" "${TRYTON_LOGCONFFILE}" "${TRYTON_HOMEDIR}" "${TRYTON_LOGDIR}"
do
dpkg-statoverride --force --remove "${_ITEM}" > /dev/null 2>&1 || true
done
commit e440577ad0f8d6fc8ece70fe4ac22bc90880ab79
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Thu Oct 23 12:45:36 2014 +0200
Setting ownership and permissions for the logging configuration file.
trytond_log.conf should get the same ownership and permissions as
trytond.conf.
diff --git a/debian/tryton-server.postinst b/debian/tryton-server.postinst
index 47e348c..f7714e9 100644
--- a/debian/tryton-server.postinst
+++ b/debian/tryton-server.postinst
@@ -4,6 +4,7 @@ set -e
TRYTON_USER="tryton"
TRYTON_CONFFILE="/etc/tryton/trytond.conf"
+TRYTON_LOGCONFFILE="/etc/tryton/trytond_log.conf"
TRYTON_LOGDIR="/var/log/tryton"
TRYTON_HOMEDIR="/var/lib/tryton"
@@ -23,10 +24,11 @@ case "${1}" in
# Setting ownership and permissions on configuration file
# trytond uses internal defaults, if it cannot read the
# configuration file.
- chown ${TRYTON_USER}:${TRYTON_USER} ${TRYTON_CONFFILE}
+ chown ${TRYTON_USER}:${TRYTON_USER} ${TRYTON_CONFFILE} ${TRYTON_LOGCONFFILE}
if ! dpkg-statoverride --list "${TRYTON_CONFFILE}" > /dev/null 2>&1
then
chmod 0440 "${TRYTON_CONFFILE}"
+ chmod 0440 "${TRYTON_LOGCONFFILE}"
fi
# Restricting access to home and log directories for security reasons (private information)
commit 29b060e6ebd1a9a55b9cece5a1d7a290bfa2a5ae
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Wed Oct 22 23:58:22 2014 +0200
Adding 01_migrate_obsolete_ldap_connection patch.
diff --git a/debian/patches/01_migrate_obsolete_ldap_connection b/debian/patches/01_migrate_obsolete_ldap_connection
new file mode 100644
index 0000000..8a256dd
--- /dev/null
+++ b/debian/patches/01_migrate_obsolete_ldap_connection
@@ -0,0 +1,22 @@
+Description: Migration for obsolete module ldap_connection
+ tryton-modules-ldap-connection was merged into
+ tryton-modules-ldap-authentication. The server fails to start with a
+ missing module, so we remove it from the modules table.
+Author: Mathias Behrle <mathiasb at m9s.biz>
+Bug: http://bugs.tryton.org/issue4280
+Forwarded: https://bugs.tryton.org/issue4280
+
+--- tryton-server-3.4.0.orig/trytond/modules/__init__.py
++++ tryton-server-3.4.0/trytond/modules/__init__.py
+@@ -365,8 +365,10 @@ def load_modules(database_name, pool, up
+ cursor = Transaction().cursor
+ if update:
+ # Migration from 2.2: workflow module removed
++ # Migration from 3.2: module ldap_connection removed
++ obsolete_modules = ['workflow', 'ldap_connection']
+ cursor.execute(*ir_module.delete(
+- where=(ir_module.name == 'workflow')))
++ where=(ir_module.name.in_(obsolete_modules))))
+ cursor.execute(*ir_module.select(ir_module.name,
+ where=ir_module.state.in_(('installed', 'to install',
+ 'to upgrade', 'to remove'))))
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 0000000..f9bd601
--- /dev/null
+++ b/debian/patches/series
@@ -0,0 +1 @@
+01_migrate_obsolete_ldap_connection
commit 1ec29889054ffc6ed3c949718f316c2743960b9e
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Wed Oct 22 23:24:14 2014 +0200
Adding NEWS for the new version 3.4.
diff --git a/debian/NEWS b/debian/NEWS
index 2a5ccde..6ba6f48 100644
--- a/debian/NEWS
+++ b/debian/NEWS
@@ -1,3 +1,21 @@
+tryton-server (3.4.0-1) unstable; urgency=medium
+
+ This is the new major release 3.4.0.
+
+ The configuration files have moved to the new subdirectory /etc/tryton/.
+ The format of the configuration file changed to a simple ini style
+ format. There is no automatic migration of old configuration settings.
+ So be sure to adapt /etc/tryton/trytond.conf (or whatever configuration
+ file you may use) to the new format.
+
+ As for each major release don't forget to backup your database(s) and
+ then run the database update with
+ # trytond --all -d <your_database_name>
+ and restart the server with
+ # service tryton-server restart
+
+ -- Mathias Behrle <mathiasb at m9s.biz> Tue, 22 Oct 2014 17:37:36 +0200
+
tryton-server (3.2.0-1) unstable; urgency=medium
This is the new major release 3.2.0.
commit de1fc5ffe09c3cdbb4c281dfc4fcf28c61690e3a
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Wed Oct 22 23:18:25 2014 +0200
Updating README.Debian for version 3.4.
diff --git a/debian/tryton-server.README.Debian b/debian/tryton-server.README.Debian
index d94a010..bf81015 100644
--- a/debian/tryton-server.README.Debian
+++ b/debian/tryton-server.README.Debian
@@ -15,7 +15,7 @@ achieve this (you need to execute all commands as root):
* Making sure, PostgreSQL is running:
- # /etc/init.d/postgresql* restart
+ # service postgresql* restart
Note: If PostgreSQL runs on another machine than the Tryton server, make sure
you have setup database password authentication. Please refer to the
@@ -30,53 +30,48 @@ achieve this (you need to execute all commands as root):
You have to enter
* a password for the future database user (this will be used later in the
- setup of /etc/trytond.conf as db_password)
+ setup of the database URI in /etc/tryton/trytond.conf as password)
* confirm it
* and finally enter the password of the postgres superuser.
Note: If you want to run the database as another user than 'tryton', you
need to replace 'tryton' above with the user you want to use instead.
- You need to adjust in the same way 'db_user = tryton' in /etc/trytond.conf.
Preparing the Tryton server
---------------------------
* Setting up the Tryton server (trytond):
- Adjust /etc/trytond.conf to reflect the setup of your system and use the
- database user and password from step 1 for db_user and db_password.
+ Adjust /etc/tryton/trytond.conf to reflect the setup of your system by using
+ the database user and password from step 1 for the database URI.
- In case, that the PostgreSQL database runs on another machine (i.e. not
- localhost), also edit db_host and db_port to point to your PostgreSQL
- database server.
-
- * If the Tryton server shall listen on some external interface (i.e. be
+ * If the Tryton server shall listen on some external interface (i.e. shall be
available for clients connecting from other machines), change the jsonrpc
protocol to listen accordingly.
- Examples (listening on alll interfaces):
- jsonrpc = 0.0.0.0:8000 (for IPv4)
- jsonrpc = *:8000 (for IPv6)
- jsonrpc = 0.0.0.0:8000,*:8000 (for both)
-
* If the Tryton server is listening on external interfaces, it is highly
- recommended to enable SSL for the connection:
- ssl_jsonrpc = True
+ recommended to enable SSL for the connection.
- Note: The package is prepared to use the snakeoil certfifcates from
+ Note: The package is prepared to use the snakeoil certfificates from
the ssl-cert package. If you are installing the ssl-cert package after
the tryton-server package, take care to add the tryton user to the
- ssl-cert group.
+ ssl-cert group with
# adduser tryton ssl-cert
* Restarting trytond:
- # /etc/init.d/tryton-server restart
+ # service tryton-server restart
-Now you are ready to connect with a client, e.g. tryton-client.
+ Note: The fingerprint of connected servers is stored in the clients
+ known_hosts file. When a server is changed for its SSL usage, the client
+ will refuse to reconnect to this server. Removing the according line
+ from ~/.config/tryton/<version>/known_hosts will allow to connect to
+ the server again.
+Now you are ready to connect with a client, e.g. tryton-client.
+
Creating the database
---------------------
@@ -92,7 +87,7 @@ Client and are not mandatory to be done on the command line.
* Initializing the database:
- # /usr/bin/trytond -i all -d tryton
+ # /usr/bin/trytond -u res -d tryton
Note: Use the database name you chose in the previous step (here as default: tryton).
You will be asked for the admin password for this database.
@@ -106,7 +101,7 @@ Upgrade
version string) you have to update your database(s).
After the categorically recommended backup do:
- # /usr/bin/trytond -u all -d tryton
+ # /usr/bin/trytond --all -d tryton
Remember to replace tryton with the name of your database.
@@ -114,11 +109,12 @@ Upgrade
Notes
-----
-Now, you're finished. Please be aware of the following things:
+Now, you're finished with the system setup. Please be aware of the following things:
* trytond has one default account for server administration:
- - User: admin; password: admin (or the one you have configured in
- trytond.conf)
+ - User: admin; password: the one you have configured in trytond.conf
+ as super_pwd. This user is the one used for database management tasks
+ from the client.
Note: Each Tryton database will have its own admin with login password
stored in the database itself (not to be confound with the admin of the
@@ -127,29 +123,20 @@ Now, you're finished. Please be aware of the following things:
* trytond must have read access to its configuration file, otherwise it will
start with internal defaults. The postinst script will (re)set ownership to
the system user running trytond and correct the permissions on the standard
- configuration file (/etc/tryond.conf), if not otherwise stated by means of
- dpkg-statoverride.
+ configuration file (/etc/tryton/trytond.conf), if not otherwise stated by
+ means of dpkg-statoverride.
* trytond listens by default on port 8000 (jsonrpc). If you need to change
- this, edit /etc/trytond.conf and replace 'jsonrpc = <interface>:8000' with
- 'jsonrpc = <interface>:<your port>'.
+ this, edit /etc/tryton/trytond.conf in the section [jsonrpc].
* trytond in its upstream configuration listens by default to the localhost
interface. If you want to change this default to listen on all interfaces,
- edit /etc/trytond.conf and change the listening interface to something like
- 'jsonrpc = *:<port>' (IPv6) or 'jsonrpc = 0.0.0.0:<port>' (IPv4)
- according to your needs.
+ edit /etc/tryton/trytond.conf in the section [jsonrpc].
* Installation of modules into the database can be done from the
Administration Panel of the client. Under Modules you can select from the
modules packages (trytond-modules*) you have installed on your system.
- To install just all from command line, run a second time:
-
- # /usr/bin/trytond -i all -d tryton
-
- Remember to replace tryton with the name of your database.
-
* Only the same major version of Tryton client and Tryton server can connect.
- -- Mathias Behrle <mathiasb at m9s.biz> Tue, 10 Jun 2014 16:45:00 +0200
+ -- Mathias Behrle <mathiasb at m9s.biz> Tue, 22 Oct 2014 16:45:00 +0200
commit 674cf079d5e1b6699138c07851379872a57b7683
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Wed Oct 22 22:58:19 2014 +0200
Using the subdirectory /etc/tryton for all configuration files.
- Adding the logging configuration to installed files.
- Adapting all files to point to the new location and to use
the new logging configuration.
diff --git a/debian/tryton-server.init b/debian/tryton-server.init
index 1d25d68..bf17039 100644
--- a/debian/tryton-server.init
+++ b/debian/tryton-server.init
@@ -23,10 +23,10 @@ DESC="Tryton Application Platform"
DAEMONUSER="tryton"
PIDDIR="/var/run/${NAME}"
PIDFILE="${PIDDIR}/${NAME}.pid"
-LOGFILE="/var/log/tryton/${NAME}.log"
+LOGCONF="/etc/tryton/${NAME}_log.conf"
DEFAULTS="/etc/default/tryton-server"
-CONFIGFILE="/etc/${NAME}.conf"
-DAEMON_OPTS="--config=${CONFIGFILE} --logfile=${LOGFILE}"
+CONFIGFILE="/etc/tryton/${NAME}.conf"
+DAEMON_OPTS="--config ${CONFIGFILE} --logconf ${LOGCONF}"
# Include tryton-server defaults if available
if [ -r "${DEFAULTS}" ]
diff --git a/debian/tryton-server.install b/debian/tryton-server.install
index 0e7acdb..b00b559 100644
--- a/debian/tryton-server.install
+++ b/debian/tryton-server.install
@@ -1 +1,2 @@
-debian/trytond.conf /etc
+debian/trytond.conf /etc/tryton
+debian/trytond_log.conf /etc/tryton
diff --git a/debian/tryton-server.postinst b/debian/tryton-server.postinst
index dc6edd5..47e348c 100644
--- a/debian/tryton-server.postinst
+++ b/debian/tryton-server.postinst
@@ -3,7 +3,7 @@
set -e
TRYTON_USER="tryton"
-TRYTON_CONFFILE="/etc/trytond.conf"
+TRYTON_CONFFILE="/etc/tryton/trytond.conf"
TRYTON_LOGDIR="/var/log/tryton"
TRYTON_HOMEDIR="/var/lib/tryton"
diff --git a/debian/tryton-server.postrm b/debian/tryton-server.postrm
index 8122e05..6ccab76 100644
--- a/debian/tryton-server.postrm
+++ b/debian/tryton-server.postrm
@@ -3,14 +3,15 @@
set -e
TRYTON_USER="tryton"
-TRYTON_CONFFILE="/etc/trytond.conf"
+TRYTON_OLDCONFFILE="/etc/trytond.conf"
+TRYTON_CONFFILE="/etc/tryton/trytond.conf"
TRYTON_LOGDIR="/var/log/tryton"
TRYTON_HOMEDIR="/var/lib/tryton"
case "${1}" in
purge)
# Removing evtl. dpkg-statoverrides
- for _ITEM in "${TRYTON_CONFFILE}" "${TRYTON_HOMEDIR}" "${TRYTON_LOGDIR}"
+ for _ITEM in "${TRYTON_OLDCONFFILE}" "${TRYTON_CONFFILE}" "${TRYTON_HOMEDIR}" "${TRYTON_LOGDIR}"
do
dpkg-statoverride --force --remove "${_ITEM}" > /dev/null 2>&1 || true
done
diff --git a/debian/tryton-server.service b/debian/tryton-server.service
index 38b5e66..e9ce0c0 100644
--- a/debian/tryton-server.service
+++ b/debian/tryton-server.service
@@ -4,12 +4,12 @@ After=remote-fs.target
After=network.target
After=postgresql.service
After=mysql.service
-ConditionPathExists=/etc/trytond.conf
+ConditionPathExists=/etc/tryton/trytond.conf
[Service]
User=tryton
Group=tryton
-ExecStart=/usr/bin/trytond --config=/etc/trytond.conf --logfile=/var/log/tryton/trytond.log
+ExecStart=/usr/bin/trytond --config /etc/tryton/trytond.conf --logconf /etc/tryton/trytond_log.conf
Restart=on-failure
[Install]
diff --git a/debian/trytond.conf b/debian/trytond.conf
index da4069a..fd28e1f 100644
--- a/debian/trytond.conf
+++ b/debian/trytond.conf
@@ -1,4 +1,4 @@
-# /etc/trytond.conf - Configuration file for Tryton Server (trytond)
+# /etc/tryton/trytond.conf - Configuration file for Tryton Server (trytond)
#
# This file contains the most common settings for trytond (Defaults
# are commented).
commit cbb9d4242ed28d74969cca37584edbc3e128389a
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Wed Oct 22 22:54:58 2014 +0200
Removing old logrotate configuration, no more needed.
diff --git a/debian/tryton-server.logrotate b/debian/tryton-server.logrotate
deleted file mode 100644
index d42ed22..0000000
--- a/debian/tryton-server.logrotate
+++ /dev/null
@@ -1,11 +0,0 @@
-/var/log/trytond.log {
- daily
- missingok
- rotate 7
- postrotate
- invoke-rc.d --quiet tryton-server restart > /dev/null
- endscript
- compress
- notifempty
- create 640 tryton adm
-}
commit 27b39bbf51089973b534cc77039e0e7cd077f30a
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Wed Oct 22 22:36:51 2014 +0200
Correcting the SSL settings in trytond.conf to keep old behavior.
diff --git a/debian/trytond.conf b/debian/trytond.conf
index fc03052..da4069a 100644
--- a/debian/trytond.conf
+++ b/debian/trytond.conf
@@ -35,15 +35,15 @@ path = /var/lib/tryton
[ssl]
# SSL settings
-
# Activation of SSL for all available protocols.
-#ssl = False
+# Uncomment the following settings for key and certificate
+# to enable SSL.
# The path to the private key
-privatekey = /etc/ssl/private/ssl-cert-snakeoil.key
+#privatekey = /etc/ssl/private/ssl-cert-snakeoil.key
# The path to the certificate
-certificate = /etc/ssl/certs/ssl-cert-snakeoil.pem
+#certificate = /etc/ssl/certs/ssl-cert-snakeoil.pem
[jsonrpc]
# Settings for the JSON-RPC network interface
commit 10cc12e783c37d19561f27f4f3caf48afefd862d
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Wed Oct 22 22:30:40 2014 +0200
Adding a logging configuration file needed for version 3.4.
diff --git a/debian/trytond_log.conf b/debian/trytond_log.conf
new file mode 100644
index 0000000..2fe628e
--- /dev/null
+++ b/debian/trytond_log.conf
@@ -0,0 +1,31 @@
+# /etc/tryton/trytond.conf - Logging configuration file for Tryton Server (trytond)
+#
+# This file contains settings for trytond to rotate logs daily and
+# to keep 30 logs.
+# For more information install the tryton-server-doc package and read
+# /usr/share/doc/tryton-server-doc/html/index.html
+# and accordingly
+# /usr/share/doc/tryton-server-doc/html/topics/logs.html
+
+[database]
+[formatters]
+keys: simple
+
+[handlers]
+keys: rotate
+
+[loggers]
+keys: root
+
+[formatter_simple]
+format: %(asctime)s] %(levelname)s:%(name)s:%(message)s
+datefmt: %a %b %d %H:%M:%S %Y
+
+[handler_rotate]
+class: handlers.TimedRotatingFileHandler
+args: ('/var/log/tryton/trytond.log', 'D', 1, 30)
+formatter: simple
+
+[logger_root]
+level: INFO
+handlers: rotate
commit d9f2dc0b3766ace3cb57d0a7753b07ddce8e1bba
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Wed Oct 22 22:07:06 2014 +0200
Updating man page.
diff --git a/debian/manpages/trytond.1 b/debian/manpages/trytond.1
index 8526e0f..2b620db 100644
--- a/debian/manpages/trytond.1
+++ b/debian/manpages/trytond.1
@@ -1,4 +1,4 @@
-.TH TRYTOND 1 "2014\-04\-24" "3.2" "Tryton Application Platform"
+.TH TRYTOND 1 "2014\-10\-21" "3.4" "Tryton Application Platform"
.SH NAME
trytond \- Tryton Application Platform (Server)
@@ -24,34 +24,35 @@ show this help message and exit
.B \-c CONFIG, --config=CONFIG
specify config file
.TP
-.B \--debug
-enable debug mode (start post-mortem debugger if
-exceptions occur)
+.B \--dev
+enable development mode (caching disabled)
.TP
.B \-v, --verbose
enable verbose mode
.TP
-.B \-d DB_NAME, --database=DB_NAME
+.B \-d DATABASE [DATABASE ...], --database DATABASE [DATABASE ...]
specify the database name
.TP
-.B \-i INIT, --init=MODULE_NAME
-init a module (use "all" for all modules)
+.B \-u MODULE [MODULE ...], --update MODULE [MODULE ...]
+update a module
+.TP
+.B \--all
+update all installed modules
-The first time a database is initialized with "\-i" the admin password is read from
+The first time a database is initialized the admin password is read from
a file defined by the TRYTONPASSFILE environment variable or interactively asked from the user.
+The config file can be specified in the TRYTOND_CONFIG environment variable. The
+database URI can be specified in the TRYTOND_DATABASE_URI environment variable.
.TP
-.B \-u UPDATE, --update=MODULE_NAME
-update a module (use "all" for all modules)
-.TP
-.B \--pidfile=PIDFILE
+.B \--pidfile FILE
file where the server pid will be stored
.TP
-.B \--logfile=LOGFILE
-file where the server log will be stored
+.B \--logconf FILE
+logging configuration file (ConfigParser format)
.TP
-.B \--disable-cron
-disable internal cron
+.B \--cron
+enable internal cron
.SH FILES
\fB/etc/trytond.conf\fR
commit a466673c3ed6cd4b54008d08152a6070432f9390
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Wed Oct 22 22:06:21 2014 +0200
Updating copyright, the backport of orderddict was removed.
diff --git a/debian/copyright b/debian/copyright
index 801be74..9c7f74f 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -8,10 +8,6 @@ Copyright: 2004-2008 Tiny SPRL
2011 Openlabs Technologies & Consulting (P) Ltd
License: GPL-3+
-Files: trytond/tools/ordereddict.py
-Copyright: 2009 Raymond Hettinger
-License: MIT
-
Files: doc/*
Copyright: 2008-2011 Bertrand Chenal
2008-2011 Cédric Krier
@@ -45,25 +41,6 @@ License: GPL-3+
The complete text of the GNU General Public License
can be found in /usr/share/common-licenses/GPL-3 file.
-License: MIT
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- .
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
- .
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
-
License: public-domain
The icons used are generally taken from
http://tango.freedesktop.org/releases/tango-icon-theme-0.8.90.tar.gz
commit c9a82f0d959b554b56f558a1440a45cf43f21cc6
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Tue Oct 21 17:36:02 2014 +0200
Removing the configuration patches, no more needed.
diff --git a/debian/patches/01-debian-data-dir.patch b/debian/patches/01-debian-data-dir.patch
deleted file mode 100644
index 13a15d0..0000000
--- a/debian/patches/01-debian-data-dir.patch
+++ /dev/null
@@ -1,14 +0,0 @@
-Author: Mathias Behrle <mathiasb at m9s.biz>
-Description: Set the data path of the server to the home of the Debian tryton user.
-Forwarded: not-needed
---- tryton-server.orig/etc/trytond.conf 2013-11-24 18:24:26.350839912 +0100
-+++ tryton-server/etc/trytond.conf 2013-11-24 18:24:26.346840051 +0100
-@@ -61,7 +61,7 @@
- #smtp_default_from_email = False
-
- # Configure the path to store attachments and sqlite database
--#data_path = /var/lib/trytond
-+data_path = /var/lib/tryton
-
- # Allow to run more than one instance of trytond
- #multi_server = False
diff --git a/debian/patches/02-snakeoil-certs.patch b/debian/patches/02-snakeoil-certs.patch
deleted file mode 100644
index b587015..0000000
--- a/debian/patches/02-snakeoil-certs.patch
+++ /dev/null
@@ -1,16 +0,0 @@
-Author: Mathias Behrle <mathiasb at m9s.biz>
-Description: Prepare the conf file for the usage of ssl snakeoil certs.
-Forwarded: not-needed
---- tryton-server.orig/etc/trytond.conf 2013-12-01 12:57:37.388363898 +0100
-+++ tryton-server/etc/trytond.conf 2013-12-01 13:08:43.649163290 +0100
-@@ -48,8 +48,8 @@
- #pidfile = False
- #logfile = False
-
--#privatekey = server.pem
--#certificate = server.pem
-+privatekey = /etc/ssl/private/ssl-cert-snakeoil.key
-+certificate = /etc/ssl/certs/ssl-cert-snakeoil.pem
-
- # Configure the SMTP connection
- #smtp_server = localhost
diff --git a/debian/patches/series b/debian/patches/series
deleted file mode 100644
index 2c10589..0000000
--- a/debian/patches/series
+++ /dev/null
@@ -1,2 +0,0 @@
-01-debian-data-dir.patch
-02-snakeoil-certs.patch
commit 82c3bd7ed612119ff8034ec8fa1b65827647f2a8
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Tue Oct 21 17:32:15 2014 +0200
Adding a default configuration file.
The package doesn't provide any more a default configuration file, so we
provide one including the most common settings.
diff --git a/debian/tryton-server.install b/debian/tryton-server.install
index c7c68db..0e7acdb 100644
--- a/debian/tryton-server.install
+++ b/debian/tryton-server.install
@@ -1 +1 @@
-etc/trytond.conf /etc
+debian/trytond.conf /etc
diff --git a/debian/trytond.conf b/debian/trytond.conf
new file mode 100644
index 0000000..fc03052
--- /dev/null
+++ b/debian/trytond.conf
@@ -0,0 +1,120 @@
+# /etc/trytond.conf - Configuration file for Tryton Server (trytond)
+#
+# This file contains the most common settings for trytond (Defaults
+# are commented).
+# For more information install the tryton-server-doc package and read
+# /usr/share/doc/tryton-server-doc/html/index.html
+# and accordingly
+# /usr/share/doc/tryton-server-doc/html/topics/configuration.html
+
+[database]
+# Database related settings
+
+# The URI to connect to the SQL database (following RFC-3986)
+# uri = database://username:password@host:port/
+#
+# PostgreSQL via TCP/IP
+#uri = postgresql://tryton:tryton@localhost:5432/
+# PostgreSQL via Unix domain sockets
+#uri = postgresql://tryton:tryton@/
+
+# The path to the directory where the Tryton Server stores files.
+# The server must have write permissions to this directory.
+path = /var/lib/tryton
+
+# Shall available databases be listed in the client?
+#list = True
+
+# The number of retries of the Tryton Server when there are errors
+# in a request to the database
+#retry = 5
+
+# The primary language, that is used to store entries in translatable
+# fields into the database.
+#language = en_US
+
+[ssl]
+# SSL settings
+
+# Activation of SSL for all available protocols.
+#ssl = False
+
+# The path to the private key
+privatekey = /etc/ssl/private/ssl-cert-snakeoil.key
+
+# The path to the certificate
+certificate = /etc/ssl/certs/ssl-cert-snakeoil.pem
+
+[jsonrpc]
+# Settings for the JSON-RPC network interface
+
+# The IP/host and port number of the interface
+#listen = localhost:8000
+#
+# Listen on all interfaces (IPv4 and IPv6)
+#listen = 0.0.0.0:8000,*:8000
+
+# The hostname for this interface
+#hostname =
+
+# The root path to retrieve data for GET requests
+#data = jsondata
+
+[xmlrpc]
+# Settings for the XML-RPC network interface
+
+# The IP/host and port number of the interface
+#listen = localhost:8069
+
+[webdav]
+# Settings for the WebDAV network interface
+
+# The IP/host and port number of the interface
+#listen = localhost:8080
+
+[session]
+# Session settings
+
+# The time (in seconds) until an inactive session expires
+#timeout = 3600
+
+# The server administration password used by the client for
+# the execution of database management tasks. It is encrypted
+# using using the Unix crypt(3) routine. A password can be
+# generated using the following command line (on one line):
+# $ python -c 'import getpass,crypt,random,string; \
+# print crypt.crypt(getpass.getpass(), \
+# "".join(random.sample(string.ascii_letters + string.digits, 8)))'
+# Example password with 'admin'
+#super_pwd = jkUbZGvFNeugk
+
+[email]
+# Mail settings
+
+# The URI to connect to the SMTP server.
+# Available protocols are:
+# - smtp: simple SMTP
+# - smtp+tls: SMTP with STARTTLS
+# - smtps: SMTP with SSL
+#uri = smtp://localhost:25
+
+# The From address used by the Tryton Server to send emails.
+#from = tryton at localhost
+
+[report]
+# Report settings
+
+# Unoconv parameters for connection to the unoconv service.
+#unoconv = pipe,name=trytond;urp;StarOffice.ComponentContext
+
+# Module settings
+#
+# Some modules are reading configuration parameters from this
+# configuration file. These settings only apply when those modules
+# are installed.
+#
+#[ldap_authentication]
+# The URI to connect to the LDAP server.
+#uri = ldap://host:port/dn?attributes?scope?filter?extensions
+# A basic default URL could look like
+#uri = ldap://localhost:389/
commit 233ffb70f6c3aef18bb75d5db6b491986ef16e50
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Tue Oct 21 11:29:26 2014 +0200
Merging upstream version 3.4.0.
diff --git a/CHANGELOG b/CHANGELOG
index be6f898..6a2811c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,13 +1,28 @@
-Version 3.2.3 - 2014-09-29
+Version 3.4.0 - 2014-10-20
* Bug fixes (see mercurial logs for details)
* Use literal_eval instead of safe_eval (CVE-2014-6633)
* Prevent double underscore in safe_eval (CVE-2014-6633)
-
-Version 3.2.2 - 2014-08-03
-* Bug fixes (see mercurial logs for details)
-
-Version 3.2.1 - 2014-07-01
-* Bug fixes (see mercurial logs for details)
+* Add pre-validation on button
+* Model and Field access checked only if _check_access is set
+* Add check_access to RPC
+* Add check_access to Wizard and Report
+* Add support for domain_<field name> method
+* Refactor configuration file and command line
+* Use the context of the relation field for instanciation
+* Use a configuration field for logging
+* Add translated descriptor for Selection field
+* Add tree_state attribute on tree view
+* Allow to sync XML data
+* Remove on_change calls in Model.default_get
+* Add group call to on_change
+* Add UnionMixin
+* Allow to disable sorting of dictionary field's selection
+* Add active field to views of action window
+* Make global cache depends on explicit context keys
+* Don't add to global cache Binary fields
+* Add MatchMixin
+* Add image widget to tree
+* Remove context, current_date and time from record rule evaluation
Version 3.2.0 - 2014-04-21
* Bug fixes (see mercurial logs for details)
diff --git a/MANIFEST.in b/MANIFEST.in
index 15940c4..b5f249f 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -8,7 +8,6 @@ include doc/*
recursive-include doc *.rst
recursive-include doc *.po
recursive-include doc *.pot
-include etc/*
include trytond/backend/*/init.sql
include trytond/ir/tryton.cfg
include trytond/ir/*.xml
diff --git a/PKG-INFO b/PKG-INFO
index 40f97fa..c6ba62e 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,12 +1,12 @@
Metadata-Version: 1.1
Name: trytond
-Version: 3.2.3
+Version: 3.4.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.2/
+Download-URL: http://downloads.tryton.org/3.4/
Description: trytond
=======
diff --git a/bin/trytond b/bin/trytond
index d8b84ef..6f03fe7 100755
--- a/bin/trytond
+++ b/bin/trytond
@@ -21,72 +21,39 @@ def parse_commandline():
parser.add_argument('--version', action='version',
version='%(prog)s ' + VERSION)
- parser.add_argument("-c", "--config", dest="config",
- help="specify config file")
- parser.add_argument('--debug', dest='debug_mode', action='store_true',
- help='enable debug mode (start post-mortem debugger if exceptions'
- ' occur)')
+ 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',
+ help='enable development mode')
parser.add_argument("-v", "--verbose", action="store_true",
dest="verbose", help="enable verbose mode")
- parser.add_argument("-d", "--database", dest="db_name",
- help="specify the database name")
- parser.add_argument("-i", "--init", dest="init",
- help="init a module (use \"all\" for all modules)")
- parser.add_argument("-u", "--update", dest="update",
- help="update a module (use \"all\" for all modules)")
+ parser.add_argument("-d", "--database", dest="database_names", nargs='+',
+ default=[], metavar='DATABASE', help="specify the database name")
+ parser.add_argument("-u", "--update", dest="update", nargs='+', default=[],
+ metavar='MODULE', help="update a module")
+ parser.add_argument("--all", dest="update", action="append_const",
+ const="ir", help="update all installed modules")
- parser.add_argument("--pidfile", dest="pidfile",
+ parser.add_argument("--pidfile", dest="pidfile", metavar='FILE',
help="file where the server pid will be stored")
- parser.add_argument("--logfile", dest="logfile",
- help="file where the server log will be stored")
- parser.add_argument("--disable-cron", dest="cron",
- action="store_false", help="disable cron")
-
- parser.epilog = ('The first time a database is initialized with "-i" admin'
- ' password is read from file defined by TRYTONPASSFILE '
- 'environment variable or interactively ask user. '
+ parser.add_argument("--logconf", dest="logconf", metavar='FILE',
+ help="logging configuration file (ConfigParser format)")
+ parser.add_argument("--cron", dest="cron", action="store_true",
+ help="enable cron")
+
+ parser.epilog = ('The first time a database is initialized admin '
+ 'password is read from file defined by TRYTONPASSFILE '
+ 'environment variable or interactively ask user.\n'
'The config file can be specified in the TRYTOND_CONFIG '
+ 'environment variable.\n'
+ 'The database URI can be specified in the TRYTOND_DATABASE_URI '
'environment variable.')
- opt = parser.parse_args()
-
- if opt.config:
- options['configfile'] = opt.config
- else:
- # No config file speficified, it will be guessed
- options['configfile'] = None
-
- for arg in (
- 'verbose',
- 'debug_mode',
- 'pidfile',
- 'logfile',
- 'cron',
- ):
- if getattr(opt, arg) is not None:
- options[arg] = getattr(opt, arg)
-
- db_name = []
- if opt.db_name:
- for i in opt.db_name.split(','):
- db_name.append(i)
- options['db_name'] = db_name
-
- init = {}
- if opt.init:
- for i in opt.init.split(','):
- if i != 'test':
- init[i] = 1
- options['init'] = init
-
- update = {}
- if opt.update:
- for i in opt.update.split(','):
- if i != 'test':
- update[i] = 1
- options['update'] = update
+ options = parser.parse_args()
+ if not options.database_names and options.update:
+ parser.error('Missing database option')
return options
diff --git a/doc/conf.py b/doc/conf.py
index 71c121e..18e62c7 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -48,9 +48,9 @@ copyright = (u'2008-2011, Bertrand Chenal, Cédric Krier, Ian Wilson, '
# built documents.
#
# The short X.Y version.
-version = '3.2'
+version = '3.4'
# The full version, including alpha/beta/rc tags.
-release = '3.2'
+release = '3.4'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/doc/index.rst b/doc/index.rst
index aa1c79b..5ee2f0d 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -24,7 +24,9 @@ First steps
===========
* **Installation:**
- :ref:`Installation <topics-install>`
+ :ref:`Installation <topics-install>` |
+ :ref:`Configuration <topics-configuration>` |
+ :ref:`Setup a database <topics-setup-database>`
The model layer
===============
@@ -32,7 +34,8 @@ The model layer
* **Models:**
:ref:`Model syntax <topics-models>` |
:ref:`Field types <ref-models-fields>` |
- :ref:`Domain syntax <topics-domain>`
+ :ref:`Domain syntax <topics-domain>` |
+ :ref:`Access rights <topics-access_rights>`
The view layer
==============
diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst
index 85fd0ca..82b4b1e 100644
--- a/doc/ref/models/fields.rst
+++ b/doc/ref/models/fields.rst
@@ -129,8 +129,12 @@ client will also read these fields even if they are not defined on the view.
.. attribute:: Field.context
-A dictionary which will update the current context when opening a *relation
-field*.
+A dictionary which will update the current context for *relation field*.
+
+.. warning::
+ The context could only depend on direct field of the record and without
+ context.
+..
``loading``
-----------
@@ -184,6 +188,18 @@ Default value
See :ref:`default value <topics-fields_default_value>`
+Searching
+=========
+
+A class method could be defined for each field which must return a SQL
+expression for the given domain instead of the default one.
+The method signature is::
+
+ domain_<field name>(domain, tables)
+
+Where ``domain`` is the simple :ref:`domain <topics-domain>` clause and
+``tables`` is a nested dictionary, see :meth:`~Field.convert_domain`.
+
Ordering
========
@@ -413,6 +429,13 @@ A string field with limited values to choice.
If true, the human-readable values will be translated. Default value is
``True``.
+Instance methods:
+
+.. method:: Selection.translated([name])
+
+ Returns a descriptor for the translated value of the field. The descriptor
+ must be used on the same class as the field.
+
Reference
---------
@@ -600,6 +623,10 @@ This field accepts as written value a list of tuples like the :class:`One2Many`.
An integer or a PYSON expression denoting the maximum number of records
allowed in the relation.
+.. attribute:: Many2Many.add_remove
+
+ An alias to the :attr:`domain` for compatibility with the :class:`One2Many`.
+
Instance methods:
.. method:: Many2Many.get_target()
diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst
index 5ac7918..110636e 100644
--- a/doc/ref/models/models.rst
+++ b/doc/ref/models/models.rst
@@ -76,13 +76,12 @@ Class methods:
warning states by users.
..
-.. classmethod:: Model.default_get(fields_names[, with_rec_name[, with_on_change]])
+.. classmethod:: Model.default_get(fields_names[, with_rec_name])
- Return a dictionary with the default values for each field in
+ Returns a dictionary with the default values for each field in
``fields_names``. Default values are defined by the returned value of each
instance method with the pattern ``default_`field_name`()``.
``with_rec_name`` allow to add `rec_name` value for each many2one field.
- ``with_on_change`` allow to add ``on_change`` value for each default value.
.. classmethod:: Model.fields_get([fields_names])
@@ -90,6 +89,15 @@ Class methods:
Instance methods:
+.. method:: Model.on_change(fieldnames)
+
+ Returns the list of changes by calling `on_change` method of each field.
+
+.. method:: Model.on_change_with(fieldnames)
+
+ Returns the new values of all fields by calling `on_change_with` method of
+ each field.
+
.. method:: Model.pre_validate()
This method is called by the client to validate the instance.
@@ -301,6 +309,8 @@ CLass methods:
Return a list of list of values for each ``records``.
The list of values follows ``fields_names``.
Relational fields are defined with ``/`` at any depth.
+ Descriptor on fields are available by appending ``.`` and the name of the
+ method on the field that returns the descriptor.
.. classmethod:: ModelStorage.import_data(fields_names, data)
@@ -520,6 +530,10 @@ Class attributes are:
couple of key and label when the type is `selection`.
The format is a key/label separated by ":" per line.
+.. attribute:: DictSchemaMixin.selection_sorted
+
+ If the :attr:`selection` must be sorted on label by the client.
+
.. attribute:: DictSchemaMixin.selection_json
The definition of the :class:`trytond.model.fields.Function` field to
@@ -543,5 +557,58 @@ Instance methods:
Getter for the :attr:`selection_json`.
+==========
+MatchMixin
+==========
+
+.. class:: MatchMixin
+
+A mixin_ to add to a :class:`Model` a match method on pattern.
+The pattern is a dictionary with field name as key and the value to compare.
+The record matches the pattern if for all dictionary entries, the value of the
+record is equal or not defined.
+
+Instance methods:
+
+.. method:: MatchMixin.match(pattern)
+
+ Return if the instance match the pattern
+
+==========
+UnionMixin
+==========
+
+.. class:: UnionMixin
+
+A mixin_ to create a :class:`ModelSQL` which is the UNION_ of some
+:class:`ModelSQL`'s. The ids of each models are sharded to be unique.
+
+Static methods:
+
+.. staticmethod:: UnionMixin.union_models()
+
+ Return the list of :class:`ModelSQL`'s names
+
+Class methods:
+
+.. classmethod:: UnionMixin.union_shard(column, model)
+
+ Return a SQL expression that shards the column containing record id of
+ model name.
+
+.. classmethod:: UnionMixin.union_unshard(record_id)
+
+ Return the original instance of the record for the sharded id.
+
+.. classmethod:: UnionMixin.union_column(name, field, table, Model)
+
+ Return the SQL column that corresponds to the field on the union model.
+
+.. classmethod:: UnionMixin.union_columns(model)
+
+ Return the SQL table and columns to use for the UNION for the model name.
+
+
.. _mixin: http://en.wikipedia.org/wiki/Mixin
.. _JSON: http://en.wikipedia.org/wiki/Json
+.. _UNION: http://en.wikipedia.org/wiki/Union_(SQL)#UNION_operator
diff --git a/doc/ref/rpc.rst b/doc/ref/rpc.rst
index ba0e6f9..d34e26f 100644
--- a/doc/ref/rpc.rst
+++ b/doc/ref/rpc.rst
@@ -5,7 +5,7 @@
RPC
===
-.. class:: RPC([readonly[, instantiate[, result]]])
+.. class:: RPC([readonly[, instantiate[, result[, check_access]]]])
RPC is an object to define the behavior of Remote Procedur Call.
@@ -22,3 +22,8 @@ Instance attributes are:
.. attribute:: RPC.result
The function to transform the result
+
+.. attribute:: RPC.check_access
+
+ Set `_check_access` in the context to activate the access right on model
+ and field. Default is `True`.
diff --git a/doc/topics/access_rights.rst b/doc/topics/access_rights.rst
new file mode 100644
index 0000000..96c0d22
--- /dev/null
+++ b/doc/topics/access_rights.rst
@@ -0,0 +1,35 @@
+.. _topics-access_rights:
+
+=============
+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.
+
+Model Access
+============
+
+They are defined by records of `ir.model.access` which define for each couple
+of model and group, the read, write, create and delete permission. If any group
+of the user has the permission activated, then the user is granted this
+permission.
+
+Field Access
+============
+
+Same as for model access but applied on the field. It uses records of
+`ir.model.field.access`.
+
+Button
+======
+
+For each button of a model the records of 'ir.model.button` define the list of
+groups that are allowed to call it.
+
+Record Rule
+===========
+
+.. TODO
diff --git a/doc/topics/configuration.rst b/doc/topics/configuration.rst
new file mode 100644
index 0000000..a187c0b
--- /dev/null
+++ b/doc/topics/configuration.rst
@@ -0,0 +1,193 @@
+.. _topics-configuration:
+
+=============================
+Configuration file for Tryton
+=============================
+
+The configuration file control 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:
+
+ [database]
+ uri = postgresql://user:password@localhost/
+ path = /var/lib/trytond
+
+For more information see ConfigParser_.
+
+.. _ConfigParser: http://docs.python.org/2/library/configparser.html
+
+Sections
+========
+
+This section describes the different main sections that may appear in a Tryton
+configuration file, the purpose of each section, its possible keys, and their
+possible values.
+Some modules could request the usage of other sections for which the guideline
+asks them to be named like their module.
+
+jsonrpc
+-------
+
+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`.
+
+hostname
+~~~~~~~~
+
+Defines the hostname for this network interface.
+
+data
+~~~~
+
+Defines the root path to retrieve data for `GET` request.
+
+xmlrpc
+------
+
+Defines the behavior of the XML-RPC_ network interface.
+
+listen
+~~~~~~
+
+Same as for `jsonrpc` except it have no default value.
+
+webdav
+------
+
+Define the behavior of the WebDAV_ network interface.
+
+listen
+~~~~~~
+
+Same as for `jsonrpc` except it have no default value.
+
+database
+--------
+
+Defines how database is managed.
+
+uri
+~~~
+
+Contains the URI to connect to the SQL database. The URI follows the RFC-3986_.
+The typical form is:
+
+ database://username:password@host:port/
+
+The default available databases are:
+
+PostgreSQL
+**********
+
+`pyscopg2` supports two type of connections:
+
+ - TCP/IP connection: `postgresql://user:password@localhost:5432/`
+ - Unix domain connection: `postgresql://username:password@/`
+
+SQLite
+******
+
+The only possible URI is: `sqlite://`
+
+MySQL
+*****
+
+Same as for PostgreSQL.
+
+path
+~~~~
+
+The directory where Tryton should store files and so the user running `trytond`
+must have write access on this directory.
+The default value is `/var/lib/trytond/`.
+
+list
+~~~~
+
+A boolean value (default: `True`) to list available databases.
+
+retry
+~~~~~
+
+The number of retries when a database operation error occurs during a request.
+
+language
+~~~~~~~~
+
+The main language (default: `en_US`) of the database that will be stored in the
+main table for translatable fields.
+
+ssl
+---
+
+Activates the SSL_ on all network protocol.
+
+privatekey
+~~~~~~~~~~
+
+The path to the private key.
+
+certificate
+~~~~~~~~~~~
+
+The path to the certificate.
+
+email
+-----
+
+uri
+~~~
+
+The SMTP-URL_ to connect to the SMTP server which is extended to support SSL_
+and STARTTLS_.
+The available protocols are:
+
+ - `smtp`: simple SMTP
+ - `smtp+tls`: SMTP with STARTTLS
+ - `smtps`: SMTP with SSL
+
+The default value is: `smtp://localhost:25`
+
+from
+~~~~
+
+Defines the default `From` address when Tryton send emails.
+
+session
+-------
+
+timeout
+~~~~~~~
+
+The time in second before a session expires.
+
+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::
+
+ python -c 'import getpass,crypt,random,string; print crypt.crypt(getpass.getpass(), "".join(random.sample(string.ascii_letters + string.digits, 8)))'
+
+report
+------
+
+unoconv
+~~~~~~~
+
+The parameters for `unoconv`.
+
+.. _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
+.. _STARTTLS: http://en.wikipedia.org/wiki/STARTTLS
diff --git a/doc/topics/index.rst b/doc/topics/index.rst
index bae4107..939dacd 100644
--- a/doc/topics/index.rst
+++ b/doc/topics/index.rst
@@ -10,11 +10,15 @@ Introduction to all the key parts of trytond:
:maxdepth: 1
install
+ configuration
+ setup_database
+ logs
models/index
models/fields_default_value
models/fields_on_change
domain
pyson
+ access_rights
actions
views/index
views/extension
diff --git a/doc/topics/install.rst b/doc/topics/install.rst
index a5eb993..0836c30 100644
--- a/doc/topics/install.rst
+++ b/doc/topics/install.rst
@@ -8,7 +8,7 @@ Prerequisites
=============
* Python 2.7 or later (http://www.python.org/)
- * lxml 2.0 or later (http://codespeak.net/lxml/)
+ * lxml 2.0 or later (http://lxml.de/)
* relatorio 0.2.0 or later (http://code.google.com/p/python-relatorio/)
* python-dateutil (http://labix.org/python-dateutil)
* polib (https://bitbucket.org/izi/polib/wiki/Home)
diff --git a/doc/topics/logs.rst b/doc/topics/logs.rst
new file mode 100644
index 0000000..ccd4e6d
--- /dev/null
+++ b/doc/topics/logs.rst
@@ -0,0 +1,49 @@
+.. _topics-logs:
+
+=====================
+Logging configuration
+=====================
+
+Without any configuration, trytond write INFO messages to standard output.
+
+Logs can be configured using a `configparser-format`_ file. The filename can
+be specified using trytond ``logconf`` parameter.
+
+.. _`configparser-format`: https://docs.python.org/2/library/logging.config.html#configuration-file-format
+
+Example
+=======
+
+This example allows to write INFO messages on standard output and on a disk log
+file rotated every day.
+
+.. highlight:: ini
+
+::
+
+ [formatters]
+ keys: simple
+
+ [handlers]
+ keys: rotate, console
+
+ [loggers]
+ keys: root
+
+ [formatter_simple]
+ format: %(asctime)s] %(levelname)s:%(name)s:%(message)s
+ datefmt: %a %b %d %H:%M:%S %Y
+
+ [handler_rotate]
+ class: handlers.TimedRotatingFileHandler
+ args: ('/tmp/tryton.log', 'D', 1, 30)
+ formatter: simple
+
+ [handler_console]
+ class: StreamHandler
+ formatter: simple
+ args: (sys.stdout,)
+
+ [logger_root]
+ level: INFO
+ handlers: rotate, console
diff --git a/doc/topics/setup_database.rst b/doc/topics/setup_database.rst
new file mode 100644
index 0000000..c1fa543
--- /dev/null
+++ b/doc/topics/setup_database.rst
@@ -0,0 +1,26 @@
+.. _topics-setup-database:
+
+=======================
+How to setup a database
+=======================
+
+The database section of the `configuration <topics-configuration>` must be set
+before starting.
+
+Create a database
+=================
+
+Depending of the database backend choosen, you must create a database (see the
+documentation of the choosen backend). The user running `trytond` must be
+granted the priviledge to create tables. For backend that has the option, the
+encoding of the database must be set to `UTF-8`.
+
+Initialize a database
+=====================
+
+A database can be initialized using this command line::
+
+ trytond -c <config file> -d <database name> --all
+
+At the end of the process, `trytond` will ask to set the password for the
+`admin` user.
diff --git a/doc/topics/views/index.rst b/doc/topics/views/index.rst
index 04203c5..0f79504 100644
--- a/doc/topics/views/index.rst
+++ b/doc/topics/views/index.rst
@@ -113,6 +113,9 @@ List of attributes shared by many form elements:
* ``icon``: Only for button, it must return the icon name to use or
False.
+ * ``pre_validate``: Only for button, it contains a domain to apply
+ on the record before calling the button.
+
.. _common-attributes-help:
* ``help``: The string that will be displayed when the cursor hovers over
@@ -491,6 +494,9 @@ Each tree view must start with this tag.
* ``keyword_open``: A boolean to specify if the client should look for a
tree_open action on double click instead of switching view.
+ * ``tree_state``: A boolean to specify if the client should save the state
+ of the tree.
+
field
^^^^^
diff --git a/etc/trytond.conf b/etc/trytond.conf
deleted file mode 100644
index f70a6d6..0000000
--- a/etc/trytond.conf
+++ /dev/null
@@ -1,88 +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.
-[options]
-
-# Activate the json-rpc protocol
-jsonrpc = localhost:8000
-#ssl_jsonrpc = False
-
-# This is the hostname used when generating tryton URI
-#hostname_jsonrpc =
-
-# Configure the path of json-rpc data
-#jsondata_path = /var/www/localhost/tryton
-
-# Activate the xml-rpc protocol
-#xmlrpc = *:8069
-#ssl_xmlrpc = False
-
-# Activate the webdav protocol
-#webdav = *:8080
-#ssl_webdav = False
-
-# This is the hostname used when generating WebDAV URI
-#hostname_webdav =
-
-# Configure the database type
-# allowed values are postgresql, sqlite, mysql
-#db_type = postgresql
-
-# Configure the database connection
-## Note: Only databases owned by db_user will be displayed in the connection dialog
-## of the Tryton client. db_user must have create permission for new databases
-## to be able to use automatic database creation with the Tryton client.
-#db_host = False
-#db_port = False
-#db_user = False
-#db_password = False
-#db_minconn = 1
-#db_maxconn = 64
-
-# Configure the postgresql path for the executable
-#pg_path = None
-
-# Configure the Tryton server password
-#admin_passwd = admin
-
-# Configure the path of the files for the pid and the logs
-#pidfile = False
-#logfile = False
-
-#privatekey = server.pem
-#certificate = server.pem
-
-# Configure the SMTP connection
-#smtp_server = localhost
-#smtp_port = 25
-#smtp_ssl = False
-#smtp_tls = False
-#smtp_password = False
-#smtp_user = False
-#smtp_default_from_email = False
-
-# Configure the path to store attachments and sqlite database
-#data_path = /var/lib/trytond
-
-# Allow to run more than one instance of trytond
-#multi_server = False
-
-# Configure the session timeout (inactivity of the client in sec)
-#session_timeout = 600
-
-# Enable auto-reload of modules if changed
-#auto_reload = True
-
-# Prevent database listing
-#prevent_dblist = False
-
-# Enable cron
-# cron = True
-
-# unoconv connection
-#unoconv = pipe,name=trytond;urp;StarOffice.ComponentContext
-
-# Number of retries on database operational error
-# retry = 5
-
-# Default language code
-# language = en_US
diff --git a/setup.py b/setup.py
index 793dac8..8fee48d 100644
--- a/setup.py
+++ b/setup.py
@@ -91,4 +91,5 @@ setup(name=PACKAGE,
zip_safe=False,
test_suite='trytond.tests',
test_loader='trytond.test_loader:Loader',
+ tests_require=['mock'],
)
diff --git a/trytond.egg-info/PKG-INFO b/trytond.egg-info/PKG-INFO
index 40f97fa..c6ba62e 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.2.3
+Version: 3.4.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.2/
+Download-URL: http://downloads.tryton.org/3.4/
Description: trytond
=======
diff --git a/trytond.egg-info/SOURCES.txt b/trytond.egg-info/SOURCES.txt
index d3ca803..f5a045e 100644
--- a/trytond.egg-info/SOURCES.txt
+++ b/trytond.egg-info/SOURCES.txt
@@ -21,11 +21,15 @@ doc/ref/models/fields.rst
doc/ref/models/index.rst
doc/ref/models/models.rst
doc/ref/tools/singleton.rst
+doc/topics/access_rights.rst
doc/topics/actions.rst
+doc/topics/configuration.rst
doc/topics/domain.rst
doc/topics/index.rst
doc/topics/install.rst
+doc/topics/logs.rst
doc/topics/pyson.rst
+doc/topics/setup_database.rst
doc/topics/wizard.rst
doc/topics/models/fields_default_value.rst
doc/topics/models/fields_on_change.rst
@@ -34,7 +38,6 @@ doc/topics/modules/index.rst
doc/topics/reports/index.rst
doc/topics/views/extension.rst
doc/topics/views/index.rst
-etc/trytond.conf
trytond/__init__.py
trytond/cache.py
trytond/config.py
@@ -110,6 +113,7 @@ trytond/ir/locale/cs_CZ.po
trytond/ir/locale/de_DE.po
trytond/ir/locale/es_AR.po
trytond/ir/locale/es_CO.po
+trytond/ir/locale/es_EC.po
trytond/ir/locale/es_ES.po
trytond/ir/locale/fr_FR.po
trytond/ir/locale/nl_NL.po
@@ -177,6 +181,8 @@ trytond/ir/view/model_access_form.xml
trytond/ir/view/model_access_list.xml
trytond/ir/view/model_button_form.xml
trytond/ir/view/model_button_list.xml
+trytond/ir/view/model_data_form.xml
+trytond/ir/view/model_data_list.xml
trytond/ir/view/model_field_access_form.xml
trytond/ir/view/model_field_access_list.xml
trytond/ir/view/model_field_form.xml
@@ -230,11 +236,13 @@ trytond/ir/view/ui_view_tree_width_form.xml
trytond/ir/view/ui_view_tree_width_list.xml
trytond/model/__init__.py
trytond/model/dictschema.py
+trytond/model/match.py
trytond/model/model.py
trytond/model/modelsingleton.py
trytond/model/modelsql.py
trytond/model/modelstorage.py
trytond/model/modelview.py
+trytond/model/union.py
trytond/model/workflow.py
trytond/model/fields/__init__.py
trytond/model/fields/binary.py
@@ -281,6 +289,7 @@ trytond/res/locale/cs_CZ.po
trytond/res/locale/de_DE.po
trytond/res/locale/es_AR.po
trytond/res/locale/es_CO.po
+trytond/res/locale/es_EC.po
trytond/res/locale/es_ES.po
trytond/res/locale/fr_FR.po
trytond/res/locale/nl_NL.po
@@ -299,6 +308,7 @@ trytond/tests/__init__.py
trytond/tests/access.py
trytond/tests/copy_.py
trytond/tests/export_data.py
+trytond/tests/field_context.py
trytond/tests/history.py
trytond/tests/import_data.py
trytond/tests/import_data.xml
@@ -311,6 +321,7 @@ trytond/tests/test_access.py
trytond/tests/test_cache.py
trytond/tests/test_copy.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
@@ -318,12 +329,14 @@ trytond/tests/test_mixins.py
trytond/tests/test_modelsingleton.py
trytond/tests/test_modelsql.py
trytond/tests/test_mptt.py
+trytond/tests/test_protocols.py
trytond/tests/test_pyson.py
trytond/tests/test_sequence.py
trytond/tests/test_tools.py
trytond/tests/test_transaction.py
trytond/tests/test_trigger.py
trytond/tests/test_tryton.py
+trytond/tests/test_union.py
trytond/tests/test_user.py
trytond/tests/test_wizard.py
trytond/tests/test_workflow.py
@@ -348,6 +361,7 @@ trytond/webdav/locale/cs_CZ.po
trytond/webdav/locale/de_DE.po
trytond/webdav/locale/es_AR.po
trytond/webdav/locale/es_CO.po
+trytond/webdav/locale/es_EC.po
trytond/webdav/locale/es_ES.po
trytond/webdav/locale/fr_FR.po
trytond/webdav/locale/nl_NL.po
diff --git a/trytond/backend/__init__.py b/trytond/backend/__init__.py
index 5620291..6b9abc4 100644
--- a/trytond/backend/__init__.py
+++ b/trytond/backend/__init__.py
@@ -1,15 +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.
import sys
+import urlparse
-from trytond.config import CONFIG
+from trytond.config import config
-__all__ = ['get']
+__all__ = ['name', 'get']
-def get(name):
- db_type = CONFIG['db_type']
+def name():
+ return urlparse.urlparse(config.get('database', 'uri', '')).scheme
+
+
+def get(prop):
+ db_type = name()
modname = 'trytond.backend.%s' % db_type
__import__(modname)
module = sys.modules[modname]
- return getattr(module, name)
+ return getattr(module, prop)
diff --git a/trytond/backend/database.py b/trytond/backend/database.py
index 2979c66..dae7908 100644
--- a/trytond/backend/database.py
+++ b/trytond/backend/database.py
@@ -105,29 +105,19 @@ class CursorInterface(object):
Define generic interface for database cursor
'''
IN_MAX = 1000
+ cache_keys = {'language', 'fuzzy_translation', '_datetime'}
def __init__(self):
self.cache = {}
- def get_cache(self, context=None):
- '''
- Return cache for the context
-
- :param context: the context
- :return: the cache dictionary
- '''
+ def get_cache(self):
from trytond.cache import LRUDict
from trytond.transaction import Transaction
user = Transaction().user
- if context is None:
- context = {}
- cache_ctx = context.copy()
- for i in ('_timestamp', '_delete', '_create_records',
- '_delete_records'):
- if i in cache_ctx:
- del cache_ctx[i]
- return self.cache.setdefault((user, repr(cache_ctx)),
- LRUDict(MODEL_CACHE_SIZE))
+ 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))
def execute(self, sql, params=None):
'''
diff --git a/trytond/backend/mysql/database.py b/trytond/backend/mysql/database.py
index f61136f..96627d5 100644
--- a/trytond/backend/mysql/database.py
+++ b/trytond/backend/mysql/database.py
@@ -1,7 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from trytond.backend.database import DatabaseInterface, CursorInterface
-from trytond.config import CONFIG
+from trytond.config import config, parse_uri
import MySQLdb
import MySQLdb.cursors
import MySQLdb.converters
@@ -91,14 +91,16 @@ class Database(DatabaseInterface):
'charset': 'utf8',
'conv': conv,
}
- if CONFIG['db_host']:
- args['host'] = CONFIG['db_host']
- if CONFIG['db_port']:
- args['port'] = int(CONFIG['db_port'])
- if CONFIG['db_user']:
- args['user'] = CONFIG['db_user']
- if CONFIG['db_password']:
- args['passwd'] = CONFIG['db_password']
+ uri = parse_uri(config.get('database', 'uri'))
+ assert uri.scheme == 'mysql'
+ if uri.hostname:
+ args['host'] = uri.hostname
+ if uri.port:
+ args['port'] = uri.port
+ if uri.username:
+ args['user'] = uri.username
+ if uri.password:
+ args['passwd'] = uri.password
conn = MySQLdb.connect(**args)
cursor = Cursor(conn, self.database_name)
cursor.execute('SET time_zone = `UTC`')
@@ -120,20 +122,21 @@ class Database(DatabaseInterface):
@staticmethod
def dump(database_name):
- from trytond.tools import exec_pg_command_pipe
+ from trytond.tools import exec_command_pipe
cmd = ['mysqldump', '--no-create-db']
- if CONFIG['db_user']:
- cmd.append('--user=' + CONFIG['db_user'])
- if CONFIG['db_host']:
- cmd.append('--host=' + CONFIG['db_host'])
- if CONFIG['db_port']:
- cmd.append('--port=' + CONFIG['db_port'])
- if CONFIG['db_password']:
- cmd.append('--password=' + CONFIG['db_password'])
+ uri = parse_uri(config.get('database', 'uri'))
+ if uri.username:
+ cmd.append('--user=' + uri.username)
+ if uri.hostname:
+ cmd.append('--host=' + uri.hostname)
+ if uri.port:
+ cmd.append('--port=' + uri.port)
+ if uri.password:
+ cmd.append('--password=' + uri.password)
cmd.append(database_name)
- pipe = exec_pg_command_pipe(*tuple(cmd))
+ pipe = exec_command_pipe(*tuple(cmd))
pipe.stdin.close()
data = pipe.stdout.read()
res = pipe.wait()
@@ -143,7 +146,7 @@ class Database(DatabaseInterface):
@staticmethod
def restore(database_name, data):
- from trytond.tools import exec_pg_command_pipe
+ from trytond.tools import exec_command_pipe
database = Database().connect()
cursor = database.cursor(autocommit=True)
@@ -152,14 +155,15 @@ class Database(DatabaseInterface):
cursor.close()
cmd = ['mysql']
- if CONFIG['db_user']:
- cmd.append('--user=' + CONFIG['db_user'])
- if CONFIG['db_host']:
- cmd.append('--host=' + CONFIG['db_host'])
- if CONFIG['db_port']:
- cmd.append('--port=' + CONFIG['db_port'])
- if CONFIG['db_password']:
- cmd.append('--password=' + CONFIG['db_password'])
+ uri = parse_uri(config.get('database', 'uri'))
+ if uri.username:
+ cmd.append('--user=' + uri.username)
+ if uri.hostname:
+ cmd.append('--host=' + uri.hostname)
+ if uri.port:
+ cmd.append('--port=' + uri.port)
+ if uri.password:
+ cmd.append('--password=' + uri.password)
cmd.append(database_name)
fd, file_name = tempfile.mkstemp()
@@ -171,7 +175,7 @@ class Database(DatabaseInterface):
args2 = tuple(cmd)
- pipe = exec_pg_command_pipe(*args2)
+ pipe = exec_command_pipe(*args2)
pipe.stdin.close()
res = pipe.wait()
os.remove(file_name)
@@ -192,7 +196,7 @@ class Database(DatabaseInterface):
@staticmethod
def list(cursor):
now = time.time()
- timeout = int(CONFIG['session_timeout'])
+ timeout = config.getint('session', 'timeout')
res = Database._list_cache
if res and abs(Database._list_cache_timestamp - now) < timeout:
return res
@@ -338,4 +342,4 @@ class Cursor(CursorInterface):
def update_auto_increment(self, table, value):
self.cursor.execute('ALTER TABLE `%s` AUTO_INCREMENT = %%s' % table,
- value)
+ (value,))
diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py
index ca444cd..8323557 100644
--- a/trytond/backend/postgresql/database.py
+++ b/trytond/backend/postgresql/database.py
@@ -1,7 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from trytond.backend.database import DatabaseInterface, CursorInterface
-from trytond.config import CONFIG
+from trytond.config import config, parse_uri
from psycopg2.pool import ThreadedConnectionPool
from psycopg2.extensions import cursor as PsycopgCursor
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
@@ -54,14 +54,15 @@ class Database(DatabaseInterface):
return self
logger = logging.getLogger('database')
logger.info('connect to "%s"' % self.database_name)
- host = CONFIG['db_host'] and "host=%s" % CONFIG['db_host'] or ''
- port = CONFIG['db_port'] and "port=%s" % CONFIG['db_port'] or ''
+ uri = parse_uri(config.get('database', 'uri'))
+ assert uri.scheme == 'postgresql'
+ host = uri.hostname and "host=%s" % uri.hostname or ''
+ port = uri.port and "port=%s" % uri.port or ''
name = "dbname=%s" % self.database_name
- user = CONFIG['db_user'] and "user=%s" % CONFIG['db_user'] or ''
- password = (CONFIG['db_password']
- and "password=%s" % CONFIG['db_password'] or '')
- minconn = int(CONFIG['db_minconn']) or 1
- maxconn = int(CONFIG['db_maxconn']) or 64
+ 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)
dsn = '%s %s %s %s %s' % (host, port, name, user, password)
self._connpool = ThreadedConnectionPool(minconn, maxconn, dsn)
return self
@@ -106,18 +107,25 @@ class Database(DatabaseInterface):
@staticmethod
def dump(database_name):
- from trytond.tools import exec_pg_command_pipe
+ from trytond.tools import exec_command_pipe
cmd = ['pg_dump', '--format=c', '--no-owner']
- if CONFIG['db_user']:
- cmd.append('--username=' + CONFIG['db_user'])
- if CONFIG['db_host']:
- cmd.append('--host=' + CONFIG['db_host'])
- if CONFIG['db_port']:
- cmd.append('--port=' + CONFIG['db_port'])
+ env = {}
+ uri = parse_uri(config.get('database', 'uri'))
+ if uri.username:
+ cmd.append('--username=' + uri.username)
+ if uri.hostname:
+ cmd.append('--host=' + uri.hostname)
+ if uri.port:
+ cmd.append('--port=' + uri.port)
+ if uri.password:
+ # if db_password is set in configuration we should pass
+ # an environment variable PGPASSWORD to our subprocess
+ # see libpg documentation
+ env['PGPASSWORD'] = uri.password
cmd.append(database_name)
- pipe = exec_pg_command_pipe(*tuple(cmd))
+ pipe = exec_command_pipe(*tuple(cmd), env=env)
pipe.stdin.close()
data = pipe.stdout.read()
res = pipe.wait()
@@ -127,7 +135,7 @@ class Database(DatabaseInterface):
@staticmethod
def restore(database_name, data):
- from trytond.tools import exec_pg_command_pipe
+ from trytond.tools import exec_command_pipe
database = Database().connect()
cursor = database.cursor(autocommit=True)
@@ -136,12 +144,16 @@ class Database(DatabaseInterface):
cursor.close()
cmd = ['pg_restore', '--no-owner']
- if CONFIG['db_user']:
- cmd.append('--username=' + CONFIG['db_user'])
- if CONFIG['db_host']:
- cmd.append('--host=' + CONFIG['db_host'])
- if CONFIG['db_port']:
- cmd.append('--port=' + CONFIG['db_port'])
+ env = {}
+ uri = parse_uri(config.get('database', 'uri'))
+ if uri.username:
+ cmd.append('--username=' + uri.username)
+ if uri.hostname:
+ cmd.append('--host=' + uri.hostname)
+ if uri.port:
+ cmd.append('--port=' + uri.port)
+ if uri.password:
+ env['PGPASSWORD'] = uri.password
cmd.append('--dbname=' + database_name)
args2 = tuple(cmd)
@@ -153,7 +165,7 @@ class Database(DatabaseInterface):
args2.append(' ' + tmpfile)
args2 = tuple(args2)
- pipe = exec_pg_command_pipe(*args2)
+ pipe = exec_command_pipe(*args2, env=env)
if not os.name == "nt":
pipe.stdin.write(data)
pipe.stdin.close()
@@ -175,23 +187,14 @@ class Database(DatabaseInterface):
@staticmethod
def list(cursor):
now = time.time()
- timeout = int(CONFIG['session_timeout'])
+ timeout = config.getint('session', 'timeout')
res = Database._list_cache
if res and abs(Database._list_cache_timestamp - now) < timeout:
return res
- db_user = CONFIG['db_user']
+ uri = parse_uri(config.get('database', 'uri'))
+ db_user = uri.username
if not db_user and os.name == 'posix':
db_user = pwd.getpwuid(os.getuid())[0]
- if not db_user:
- cursor.execute("SELECT usename "
- "FROM pg_user "
- "WHERE usesysid = ("
- "SELECT datdba "
- "FROM pg_database "
- "WHERE datname = %s)",
- (CONFIG["db_name"],))
- res = cursor.fetchone()
- db_user = res and res[0]
if db_user:
cursor.execute("SELECT datname "
"FROM pg_database "
diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py
index 9f132a0..7ee1e2b 100644
--- a/trytond/backend/sqlite/database.py
+++ b/trytond/backend/sqlite/database.py
@@ -1,7 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from trytond.backend.database import DatabaseInterface, CursorInterface
-from trytond.config import CONFIG
+from trytond.config import config
import os
from decimal import Decimal
import datetime
@@ -22,7 +22,8 @@ except ImportError:
from sqlite3 import IntegrityError as DatabaseIntegrityError
from sqlite3 import OperationalError as DatabaseOperationalError
from sql import Flavor, Table
-from sql.functions import Function, Extract, Position, Now, Substring, Overlay
+from sql.functions import (Function, Extract, Position, Now, Substring,
+ Overlay, CharLength)
__all__ = ['Database', 'DatabaseIntegrityError', 'DatabaseOperationalError',
'Cursor']
@@ -133,6 +134,11 @@ class SQLiteOverlay(Function):
return string[:from_ - 1] + placing_string + string[from_ - 1 + for_:]
+class SQLiteCharLength(Function):
+ __slots__ = ()
+ _function = 'LENGTH'
+
+
def sign(value):
return math.copysign(1, value)
@@ -142,6 +148,7 @@ MAPPING = {
Position: SQLitePosition,
Substring: SQLiteSubstring,
Overlay: SQLiteOverlay,
+ CharLength: SQLiteCharLength,
}
@@ -167,7 +174,7 @@ class Database(DatabaseInterface):
path = ':memory:'
else:
db_filename = self.database_name + '.sqlite'
- path = os.path.join(CONFIG['data_path'], db_filename)
+ path = os.path.join(config.get('database', 'path'), db_filename)
if not os.path.isfile(path):
raise IOError('Database "%s" doesn\'t exist!' % db_filename)
if self._conn is not None:
@@ -209,7 +216,7 @@ class Database(DatabaseInterface):
else:
if os.sep in database_name:
return
- path = os.path.join(CONFIG['data_path'],
+ path = os.path.join(config.get('database', 'path'),
database_name + '.sqlite')
with sqlite.connect(path) as conn:
cursor = conn.cursor()
@@ -222,7 +229,7 @@ class Database(DatabaseInterface):
return
if os.sep in database_name:
return
- os.remove(os.path.join(CONFIG['data_path'],
+ os.remove(os.path.join(config.get('database', 'path'),
database_name + '.sqlite'))
@staticmethod
@@ -231,7 +238,7 @@ class Database(DatabaseInterface):
raise Exception('Unable to dump memory database!')
if os.sep in database_name:
raise Exception('Wrong database name!')
- path = os.path.join(CONFIG['data_path'],
+ path = os.path.join(config.get('database', 'path'),
database_name + '.sqlite')
with open(path, 'rb') as file_p:
data = file_p.read()
@@ -243,7 +250,7 @@ class Database(DatabaseInterface):
raise Exception('Unable to restore memory database!')
if os.sep in database_name:
raise Exception('Wrong database name!')
- path = os.path.join(CONFIG['data_path'],
+ path = os.path.join(config.get('database', 'path'),
database_name + '.sqlite')
if os.path.isfile(path):
raise Exception('Database already exists!')
@@ -255,7 +262,7 @@ class Database(DatabaseInterface):
res = []
listdir = [':memory:']
try:
- listdir += os.listdir(CONFIG['data_path'])
+ listdir += os.listdir(config.get('database', 'path'))
except OSError:
pass
for db_file in listdir:
diff --git a/trytond/cache.py b/trytond/cache.py
index c6d7adb..6ccd039 100644
--- a/trytond/cache.py
+++ b/trytond/cache.py
@@ -1,12 +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 datetime
from threading import Lock
from collections import OrderedDict
+from sql import Table
+from sql.functions import Now
+
from trytond.transaction import Transaction
-from trytond.config import CONFIG
-from trytond import backend
__all__ = ['Cache', 'LRUDict']
@@ -74,18 +74,13 @@ class Cache(object):
@staticmethod
def clean(dbname):
- if not CONFIG['multi_server']:
- return
- database = backend.get('Database')(dbname).connect()
- cursor = database.cursor()
- try:
- cursor.execute('SELECT "timestamp", "name" FROM ir_cache')
+ with Transaction().new_cursor():
+ cursor = Transaction().cursor
+ table = Table('ir_cache')
+ cursor.execute(*table.select(table.timestamp, table.name))
timestamps = {}
for timestamp, name in cursor.fetchall():
timestamps[name] = timestamp
- finally:
- cursor.commit()
- cursor.close()
for inst in Cache._cache_instance:
if inst._name in timestamps:
with inst._lock:
@@ -96,36 +91,30 @@ class Cache(object):
@staticmethod
def reset(dbname, name):
- if not CONFIG['multi_server']:
- return
with Cache._resets_lock:
Cache._resets.setdefault(dbname, set())
Cache._resets[dbname].add(name)
@staticmethod
def resets(dbname):
- if not CONFIG['multi_server']:
- return
- database = backend.get('Database')(dbname).connect()
- cursor = database.cursor()
- try:
+ with Transaction().new_cursor():
+ cursor = Transaction().cursor
+ table = Table('ir_cache')
with Cache._resets_lock:
Cache._resets.setdefault(dbname, set())
for name in Cache._resets[dbname]:
- cursor.execute('SELECT name FROM ir_cache WHERE name = %s',
- (name,))
+ cursor.execute(*table.select(table.name,
+ where=table.name == name))
if cursor.fetchone():
# It would be better to insert only
- cursor.execute('UPDATE ir_cache SET "timestamp" = %s '
- 'WHERE name = %s', (datetime.datetime.now(), name))
+ cursor.execute(*table.update([table.timestamp],
+ [Now()], where=table.name == name))
else:
- cursor.execute('INSERT INTO ir_cache '
- '("timestamp", "name") '
- 'VALUES (%s, %s)', (datetime.datetime.now(), name))
+ cursor.execute(*table.insert(
+ [table.timestamp, table.name],
+ [[Now(), name]]))
Cache._resets[dbname].clear()
- finally:
cursor.commit()
- cursor.close()
class LRUDict(OrderedDict):
diff --git a/trytond/config.py b/trytond/config.py
index ba67332..99e3057 100644
--- a/trytond/config.py
+++ b/trytond/config.py
@@ -1,18 +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.
-import sys
-try:
- import cdecimal
- # Use cdecimal globally
- if 'decimal' not in sys.modules:
- sys.modules['decimal'] = cdecimal
-except ImportError:
- import decimal
- sys.modules['cdecimal'] = decimal
import os
import ConfigParser
-import getpass
-import socket
+import urlparse
+
+__all__ = ['config', 'get_hostname', 'get_port', 'split_netloc',
+ 'parse_listen', 'parse_uri']
def get_hostname(netloc):
@@ -24,119 +17,80 @@ def get_hostname(netloc):
return netloc
-def get_port(netloc, protocol):
+def get_port(netloc):
netloc = netloc.split(']')[-1]
- if ':' in netloc:
- return int(netloc.split(':')[1])
- else:
- return {
- 'jsonrpc': 8000,
- 'xmlrpc': 8069,
- 'webdav': 8080,
- }.get(protocol)
-
-
-class ConfigManager(object):
- def __init__(self, fname=None):
- self.options = {
- 'jsonrpc': [('localhost', 8000)],
- 'ssl_jsonrpc': False,
- 'hostname_jsonrpc': None,
- 'xmlrpc': [],
- 'ssl_xmlrpc': False,
- 'jsondata_path': '/var/www/localhost/tryton',
- 'webdav': [],
- 'ssl_webdav': False,
- 'hostname_webdav': None,
- 'db_type': 'postgresql',
- 'db_host': False,
- 'db_port': False,
- 'db_name': False,
- 'db_user': False,
- 'db_password': False,
- 'db_minconn': 1,
- 'db_maxconn': 64,
- 'pg_path': None,
- 'admin_passwd': 'admin',
- 'verbose': False,
- 'debug_mode': False,
- 'pidfile': None,
- 'logfile': None,
- 'privatekey': '/etc/ssl/trytond/server.key',
- 'certificate': '/etc/ssl/trytond/server.pem',
- 'smtp_server': 'localhost',
- 'smtp_port': 25,
- 'smtp_ssl': False,
- 'smtp_tls': False,
- 'smtp_user': False,
- 'smtp_password': False,
- 'smtp_default_from_email': '%s@%s' % (
- getpass.getuser(), socket.getfqdn()),
- 'data_path': '/var/lib/trytond',
- 'multi_server': False,
- 'session_timeout': 600,
- 'auto_reload': True,
- 'prevent_dblist': False,
- 'init': {},
- 'update': {},
- 'cron': True,
- 'unoconv': 'pipe,name=trytond;urp;StarOffice.ComponentContext',
- 'retry': 5,
- 'language': 'en_US',
- }
- self.configfile = None
-
- def update_cmdline(self, cmdline_options):
- self.options.update(cmdline_options)
-
- # Verify that we want to log or not,
- # if not the output will go to stdout
- if self.options['logfile'] in ('None', 'False'):
- self.options['logfile'] = False
- # the same for the pidfile
- if self.options['pidfile'] in ('None', 'False'):
- self.options['pidfile'] = False
- if self.options['data_path'] in ('None', 'False'):
- self.options['data_path'] = False
-
- def update_etc(self, configfile=None):
- if configfile is None:
- configfile = os.environ.get('TRYTOND_CONFIG')
- if not configfile:
- prefixdir = os.path.abspath(os.path.normpath(os.path.join(
- os.path.dirname(sys.prefix), '..')))
- configfile = os.path.join(prefixdir, 'etc', 'trytond.conf')
- if not os.path.isfile(configfile):
- configdir = os.path.abspath(os.path.normpath(os.path.join(
- os.path.dirname(__file__), '..')))
- configfile = os.path.join(configdir, 'etc', 'trytond.conf')
- if not os.path.isfile(configfile):
- configfile = None
-
- self.configfile = configfile
- if not self.configfile:
+ return int(netloc.split(':')[1])
+
+
+def split_netloc(netloc):
+ return get_hostname(netloc).replace('*', ''), get_port(netloc)
+
+
+def parse_listen(value):
+ for netloc in value.split(','):
+ yield split_netloc(netloc)
+
+
+def parse_uri(uri):
+ return urlparse.urlparse(uri)
+
+
+class TrytonConfigParser(ConfigParser.RawConfigParser):
+
+ def __init__(self):
+ ConfigParser.RawConfigParser.__init__(self)
+ self.add_section('jsonrpc')
+ self.set('jsonrpc', 'listen', 'localhost:8000')
+ self.set('jsonrpc', 'data', '/var/www/localhost/tryton')
+ self.add_section('xmlrpc')
+ self.add_section('webdav')
+ self.add_section('database')
+ self.set('database', 'uri',
+ os.environ.get('TRYTOND_DATABASE_URI', 'sqlite://'))
+ self.set('database', 'path', '/var/lib/trytond')
+ self.set('database', 'list', 'True')
+ self.set('database', 'retry', 5)
+ self.set('database', 'language', 'en_US')
+ self.add_section('ssl')
+ self.add_section('email')
+ self.set('email', 'uri', 'smtp://localhost:25')
+ self.add_section('session')
+ self.set('session', 'timeout', 600)
+ self.add_section('report')
+ self.set('report', 'unoconv',
+ 'pipe,name=trytond;urp;StarOffice.ComponentContext')
+
+ def update_etc(self, configfile=os.environ.get('TRYTOND_CONFIG')):
+ if not configfile:
return
+ self.read(configfile)
+
+ def get(self, section, option, default=None):
+ try:
+ return ConfigParser.RawConfigParser.get(self, section, option)
+ except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+ return default
+
+ def getint(self, section, option, default=None):
+ try:
+ return ConfigParser.RawConfigParser.getint(self, section, option)
+ except (ConfigParser.NoOptionError, ConfigParser.NoSectionError,
+ TypeError):
+ return default
+
+ def getfloat(self, section, option, default=None):
+ try:
+ return ConfigParser.RawConfigParser.getfloat(self, section, option)
+ except (ConfigParser.NoOptionError, ConfigParser.NoSectionError,
+ TypeError):
+ return default
+
+ def getboolean(self, section, option, default=None):
+ try:
+ return ConfigParser.RawConfigParser.getboolean(
+ self, section, option)
+ except (ConfigParser.NoOptionError, ConfigParser.NoSectionError,
+ AttributeError):
+ return default
- parser = ConfigParser.ConfigParser()
- with open(self.configfile) as fp:
- parser.readfp(fp)
- for (name, value) in parser.items('options'):
- if value == 'True' or value == 'true':
- value = True
- if value == 'False' or value == 'false':
- value = False
- if name in ('xmlrpc', 'jsonrpc', 'webdav') and value:
- value = [(get_hostname(netloc).replace('*', ''),
- get_port(netloc, name)) for netloc in value.split(',')]
- self.options[name] = value
-
- def get(self, key, default=None):
- return self.options.get(key, default)
-
- def __setitem__(self, key, value):
- self.options[key] = value
-
- def __getitem__(self, key):
- return self.options[key]
-
-CONFIG = ConfigManager()
+config = TrytonConfigParser()
diff --git a/trytond/convert.py b/trytond/convert.py
index 378da55..8d5515c 100644
--- a/trytond/convert.py
+++ b/trytond/convert.py
@@ -2,8 +2,6 @@
#this repository contains the full copyright notices and license terms.
import time
from xml import sax
-from decimal import Decimal
-import datetime
import logging
import traceback
import sys
@@ -13,7 +11,7 @@ from itertools import izip
from collections import defaultdict
from .version import VERSION
-from .tools import safe_eval
+from .tools import safe_eval, grouped_slice
from .transaction import Transaction
CDATA_START = re.compile('^\s*\<\!\[cdata\[', re.IGNORECASE)
@@ -354,7 +352,6 @@ class Fs2bdAccessor:
self.browserecord[module][model_name][model.id] = model
def fetch_new_module(self, module):
- cursor = Transaction().cursor
self.fs2db[module] = {}
module_data_ids = self.ModelData.search([
('module', '=', module),
@@ -377,12 +374,11 @@ class Fs2bdAccessor:
continue
Model = self.pool.get(model_name)
self.browserecord[module][model_name] = {}
- for i in range(0, len(record_ids[model_name]), cursor.IN_MAX):
- sub_record_ids = record_ids[model_name][i:i + cursor.IN_MAX]
+ for sub_record_ids in grouped_slice(record_ids[model_name]):
with Transaction().set_context(active_test=False):
records = Model.search([
- ('id', 'in', sub_record_ids),
- ])
+ ('id', 'in', list(sub_record_ids)),
+ ], order=[('id', 'ASC')])
with Transaction().set_context(language='en_US'):
models = Model.browse(map(int, records))
for model in models:
@@ -493,7 +489,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
module, model = key
self.write_records(module, model, *actions)
self.grouped_write.clear()
- if self.grouped_model_data:
+ if name == 'data' and self.grouped_model_data:
self.ModelData.write(*self.grouped_model_data)
del self.grouped_model_data[:]
@@ -604,10 +600,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
if not old_values:
old_values = {}
else:
- old_values = safe_eval(old_values, {
- 'Decimal': Decimal,
- 'datetime': datetime,
- })
+ old_values = self.ModelData.load_values(old_values)
for key in old_values:
if isinstance(old_values[key], str):
@@ -691,6 +684,9 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
else:
self.write_records(module, model, record, to_update,
old_values, fs_id, mdata_id)
+ self.grouped_model_data.extend(([self.ModelData(mdata_id)], {
+ 'fs_values': self.ModelData.dump_values(values),
+ }))
else:
if self.grouped:
self.grouped_creations[model][fs_id] = values
@@ -713,7 +709,8 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
'model': model,
'module': self.module,
'db_id': record.id,
- 'values': str(values),
+ 'values': self.ModelData.dump_values(values),
+ 'fs_values': self.ModelData.dump_values(values),
'noupdate': self.noupdate,
})
@@ -725,7 +722,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
'db_id': record.id,
'model': model,
'id': mdata.id,
- 'values': str(values),
+ 'values': self.ModelData.dump_values(values),
})
self.fs2db.reset_browsercord(self.module, model,
[r.id for r in records])
@@ -774,8 +771,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler):
'model': model,
'module': module,
'db_id': record.id,
- 'values': str(values),
- 'date_update': datetime.datetime.now(),
+ 'values': self.ModelData.dump_values(values),
}))
# reset_browsercord to keep cache memory low
diff --git a/trytond/ir/action.py b/trytond/ir/action.py
index 873a131..606796a 100644
--- a/trytond/ir/action.py
+++ b/trytond/ir/action.py
@@ -170,11 +170,7 @@ class ActionKeyword(ModelSQL, ModelView):
def models_get():
pool = Pool()
Model = pool.get('ir.model')
- models = Model.search([])
- res = []
- for model in models:
- res.append([model.model, model.name])
- return res
+ return [(m.model, m.name) for m in Model.search([])]
@classmethod
def delete(cls, keywords):
@@ -241,6 +237,7 @@ class ActionKeyword(ModelSQL, ModelView):
class ActionMixin(ModelSQL):
_order_name = 'action'
+ _action_name = 'name'
@classmethod
def __setup__(cls):
@@ -298,6 +295,8 @@ class ActionMixin(ModelSQL):
@classmethod
def create(cls, vlist):
pool = Pool()
+ ModelView._fields_view_get_cache.clear()
+ ModelView._view_toolbar_get_cache.clear()
Action = pool.get('ir.action')
ir_action = cls.__table__()
new_records = []
@@ -336,11 +335,15 @@ class ActionMixin(ModelSQL):
pool = Pool()
ActionKeyword = pool.get('ir.action.keyword')
super(ActionMixin, cls).write(records, values, *args)
+ ModelView._fields_view_get_cache.clear()
+ ModelView._view_toolbar_get_cache.clear()
ActionKeyword._get_keyword_cache.clear()
@classmethod
def delete(cls, records):
pool = Pool()
+ ModelView._fields_view_get_cache.clear()
+ ModelView._view_toolbar_get_cache.clear()
Action = pool.get('ir.action')
actions = [x.action for x in records]
super(ActionMixin, cls).delete(records)
@@ -360,10 +363,23 @@ class ActionMixin(ModelSQL):
default=default))
return new_records
+ @classmethod
+ def get_groups(cls, name, action_id=None):
+ # TODO add cache
+ domain = [
+ (cls._action_name, '=', name),
+ ]
+ if action_id:
+ domain.append(('id', '=', action_id))
+ actions = cls.search(domain)
+ groups = {g.id for a in actions for g in a.groups}
+ return groups
+
class ActionReport(ActionMixin, ModelSQL, ModelView):
"Action report"
__name__ = 'ir.action.report'
+ _action_name = 'report_name'
model = fields.Char('Model')
report_name = fields.Char('Internal Name', required=True)
report = fields.Char('Path')
@@ -491,10 +507,9 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
where=outputformat.format == 'pdf'))
ids = [x[0] for x in cursor.fetchall()]
- with Transaction().set_user(0):
- cls.write(cls.browse(ids), {'extension': 'pdf'})
- ids = cls.search([('id', 'not in', ids)])
- cls.write(cls.browse(ids), {'extension': 'odt'})
+ cls.write(cls.browse(ids), {'extension': 'pdf'})
+ ids = cls.search([('id', 'not in', ids)])
+ cls.write(cls.browse(ids), {'extension': 'odt'})
table.drop_column("output_format")
TableHandler.dropTable(cursor, 'ir.action.report.outputformat',
@@ -902,12 +917,17 @@ class ActionActWindowView(ModelSQL, ModelView):
ondelete='CASCADE')
act_window = fields.Many2One('ir.action.act_window', 'Action',
ondelete='CASCADE')
+ active = fields.Boolean('Active', select=True)
@classmethod
def __setup__(cls):
super(ActionActWindowView, cls).__setup__()
cls._order.insert(0, ('sequence', 'ASC'))
+ @staticmethod
+ def default_active():
+ return True
+
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
@@ -960,6 +980,7 @@ class ActionActWindowDomain(ModelSQL, ModelView):
class ActionWizard(ActionMixin, ModelSQL, ModelView):
"Action wizard"
__name__ = 'ir.action.wizard'
+ _action_name = 'wiz_name'
wiz_name = fields.Char('Wizard name', required=True)
action = fields.Many2One('ir.action', 'Action', required=True,
ondelete='CASCADE')
diff --git a/trytond/ir/attachment.py b/trytond/ir/attachment.py
index d261509..0195c70 100644
--- a/trytond/ir/attachment.py
+++ b/trytond/ir/attachment.py
@@ -5,7 +5,7 @@ import hashlib
from sql.operators import Concat
from ..model import ModelView, ModelSQL, fields
-from ..config import CONFIG
+from ..config import config
from .. import backend
from ..transaction import Transaction
from ..pyson import Eval
@@ -117,7 +117,7 @@ class Attachment(ModelSQL, ModelView):
filename = self.digest
if self.collision:
filename = filename + '-' + str(self.collision)
- filename = os.path.join(CONFIG['data_path'], db_name,
+ filename = os.path.join(config.get('database', 'path'), db_name,
filename[0:2], filename[2:4], filename)
if name == 'data_size' or format_ == 'size':
try:
@@ -140,7 +140,7 @@ class Attachment(ModelSQL, ModelView):
cursor = Transaction().cursor
table = cls.__table__()
db_name = cursor.dbname
- directory = os.path.join(CONFIG['data_path'], db_name)
+ directory = os.path.join(config.get('database', 'path'), db_name)
if not os.path.isdir(directory):
os.makedirs(directory, 0770)
digest = hashlib.md5(value).hexdigest()
@@ -191,22 +191,19 @@ class Attachment(ModelSQL, ModelView):
return (self.write_date if self.write_date else self.create_date
).replace(microsecond=0)
- @classmethod
- def get_last_user(cls, attachments, name):
- with Transaction().set_user(0):
- return dict(
- (x.id, x.write_uid.rec_name
- if x.write_uid else x.create_uid.rec_name)
- for x in cls.browse(attachments))
+ def get_last_user(self, name):
+ return (self.write_uid.rec_name if self.write_uid
+ else self.create_uid.rec_name)
@classmethod
def check_access(cls, ids, mode='read'):
pool = Pool()
ModelAccess = pool.get('ir.model.access')
- if Transaction().user == 0:
+ if ((Transaction().user == 0)
+ or not Transaction().context.get('_check_access')):
return
model_names = set()
- with Transaction().set_user(0):
+ with Transaction().set_context(_check_access=False):
for attachment in cls.browse(ids):
if attachment.resource:
model_names.add(attachment.resource.__name__)
diff --git a/trytond/ir/configuration.py b/trytond/ir/configuration.py
index abe72db..4ee7307 100644
--- a/trytond/ir/configuration.py
+++ b/trytond/ir/configuration.py
@@ -2,7 +2,7 @@
#this repository contains the full copyright notices and license terms.
from ..model import ModelSQL, ModelSingleton, fields
from ..cache import Cache
-from ..config import CONFIG
+from ..config import config
__all__ = ['Configuration']
@@ -15,7 +15,7 @@ class Configuration(ModelSingleton, ModelSQL):
@staticmethod
def default_language():
- return CONFIG['language']
+ return config.get('database', 'language')
@classmethod
def get_language(cls):
@@ -25,6 +25,6 @@ class Configuration(ModelSingleton, ModelSQL):
config = cls(1)
language = config.language
if not language:
- language = CONFIG['language']
+ language = config.get('database', 'language')
cls._get_language_cache.set(None, language)
return language
diff --git a/trytond/ir/cron.py b/trytond/ir/cron.py
index 10f7e3f..438a0d2 100644
--- a/trytond/ir/cron.py
+++ b/trytond/ir/cron.py
@@ -14,7 +14,7 @@ from ..tools import get_smtp_server
from ..transaction import Transaction
from ..pool import Pool
from .. import backend
-from ..config import CONFIG
+from ..config import config
__all__ = [
'Cron',
@@ -133,7 +133,7 @@ class Cron(ModelSQL, ModelView):
(cron.name, cron.__url__, tb_s),
raise_exception=False)
- from_addr = CONFIG['smtp_default_from_email']
+ from_addr = config.get('email', 'from')
to_addr = cron.request_user.email
msg = MIMEText(body, _charset='utf-8')
diff --git a/trytond/ir/gen_time_locale.py b/trytond/ir/gen_time_locale.py
index b0f5eaa..9f78290 100644
--- a/trytond/ir/gen_time_locale.py
+++ b/trytond/ir/gen_time_locale.py
@@ -195,6 +195,7 @@ if __name__ == '__main__':
'de_DE',
'en_US',
'es_AR',
+ 'es_EC',
'es_ES',
'es_CO',
'fr_FR',
diff --git a/trytond/ir/lang.py b/trytond/ir/lang.py
index f5fd4f0..c0e175b 100644
--- a/trytond/ir/lang.py
+++ b/trytond/ir/lang.py
@@ -10,11 +10,14 @@ from ..tools import datetime_strftime
from ..transaction import Transaction
from ..pool import Pool
from .time_locale import TIME_LOCALE
+from ..backend.database import CursorInterface
warnings.filterwarnings('ignore', "", ImportWarning)
from locale import CHAR_MAX
warnings.resetwarnings()
+CursorInterface.cache_keys.add('translate_name')
+
__all__ = [
'Lang',
]
diff --git a/trytond/ir/lang.xml b/trytond/ir/lang.xml
index 7be3725..00b333d 100644
--- a/trytond/ir/lang.xml
+++ b/trytond/ir/lang.xml
@@ -48,6 +48,14 @@ this repository contains the full copyright notices and license terms. -->
<field name="decimal_point">,</field>
<field name="thousands_sep">.</field>
</record>
+ <record model="ir.lang" id="lang_ec">
+ <field name="code">es_EC</field>
+ <field name="name">Spanish (Ecuador)</field>
+ <field name="date">%d/%m/%Y</field>
+ <field name="grouping">[3, 3, 0]</field>
+ <field name="decimal_point">.</field>
+ <field name="thousands_sep">,</field>
+ </record>
<record model="ir.lang" id="lang_es">
<field name="code">es_ES</field>
<field name="name">Spanish (Spain)</field>
diff --git a/trytond/ir/locale/bg_BG.po b/trytond/ir/locale/bg_BG.po
index 56ebd1b..4209638 100644
--- a/trytond/ir/locale/bg_BG.po
+++ b/trytond/ir/locale/bg_BG.po
@@ -23,7 +23,7 @@ msgstr "Нямате права да изтривате този запис."
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
msgctxt "error:domain_validation_record:"
@@ -570,6 +570,11 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Действие"
+#, fuzzy
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Активен"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr "Създадено на"
@@ -1394,14 +1399,6 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr "Създадено от"
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Начална дата"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Дата на обновяване"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "ID на ресурс"
@@ -1410,6 +1407,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr "Идентификатор от файлова система"
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr ""
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr "ID"
@@ -1426,6 +1427,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "Без обновяване"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr ""
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Име"
@@ -2356,6 +2361,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Действие"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr ""
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Активен"
@@ -2870,6 +2879,11 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Бутони"
+#, fuzzy
+msgctxt "model:ir.action,name:act_model_data_form"
+msgid "Data"
+msgstr "Данни"
+
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
msgstr "Достъп до полета"
@@ -2982,6 +2996,16 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr ""
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr ""
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr ""
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Изглед на действие на активния на прозорец"
@@ -3055,6 +3079,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Немски"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr ""
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Англииски"
@@ -3299,6 +3327,11 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Бутони"
+#, fuzzy
+msgctxt "model:ir.ui.menu,name:menu_model_data_form"
+msgid "Data"
+msgstr "Данни"
+
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
msgstr "Достъп до полета"
@@ -3758,6 +3791,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Бутони"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr ""
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr ""
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Достъп до поле"
diff --git a/trytond/ir/locale/ca_ES.po b/trytond/ir/locale/ca_ES.po
index 1f2be61..9e9483f 100644
--- a/trytond/ir/locale/ca_ES.po
+++ b/trytond/ir/locale/ca_ES.po
@@ -25,16 +25,18 @@ msgstr "No podeu eliminar aquest registre."
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
-"El nombre de dígits \"%(digits)s\" del camp \"%(field)s\" de %\"(value)s\" "
-"és superior al límit."
+"El nombre de dígits \"%(digits)s\" del camp \"%(field)s\" de \"%(value)s\" "
+"és superior al seu límit."
msgctxt "error:domain_validation_record:"
msgid ""
"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
" to its domain."
-msgstr "El valor del camp \"%s\" de \"%s\" no és correcte segons aquest domini."
+msgstr ""
+"El valor del camp \"%(field)s\" de \"%(model)s\" no és correcte segons "
+"aquest domini."
msgctxt "error:foreign_model_exist:"
msgid ""
@@ -42,7 +44,7 @@ msgid ""
" \"%(model)s\"."
msgstr ""
"No es poden eliminar els registres perquè es fan servir al camp "
-"\"%(field)s\" de \"%(models)\"."
+"\"%(field)s\" de \"%(model)s\"."
msgctxt "error:foreign_model_missing:"
msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
@@ -73,7 +75,7 @@ msgstr ""
msgctxt "error:ir.action.report:"
msgid "The internal name must be unique by module!"
-msgstr "El nom intern ha de ser únic del mòdul."
+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!"
@@ -237,7 +239,7 @@ msgstr "L'arrodoniment de data-hora ha de ser més gran que 0."
msgctxt "error:ir.translation:"
msgid "Translation must be unique"
-msgstr "La traducció ha de ser únic."
+msgstr "La traducció ha de ser única."
msgctxt "error:ir.translation:"
msgid ""
@@ -289,11 +291,11 @@ msgid ""
" was configured as ancestor of itself."
msgstr ""
"Error de recursivitat: El registre \"%(rec_name)s\" amb el pare "
-"\"%(parent_rec_name)s\" es va configurar com pare de si mateix."
+"\"%(parent_rec_name)s\" s'ha configurat com a pare de si mateix."
msgctxt "error:reference_syntax_error:"
msgid "Syntax error for reference %r in %s"
-msgstr "Error de sintaxi en la referència %r a %s."
+msgstr "Error de sintaxi en la referència %r de %s."
msgctxt "error:relation_not_found:"
msgid "Relation not found: %r in %s"
@@ -301,11 +303,11 @@ msgstr "No s'ha trobat la relació: %r a %s."
msgctxt "error:required_field:"
msgid "The field \"%(field)s\" on \"%(model)s\" is required."
-msgstr "El camp \"%(field)s\" de \"%(model)s\" es obligatori."
+msgstr "El camp \"%(field)s\" de \"%(model)s\" és obligatori."
msgctxt "error:required_validation_record:"
msgid "The field \"%(field)s\" on \"%(model)s\" is required."
-msgstr "El camp \"%(field)s\" de \"%(model)s\" es obligatori."
+msgstr "El camp \"%(field)s\" de \"%(model)s\" és obligatori."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
@@ -359,7 +361,7 @@ msgstr "No podeu modificar aquest registre."
msgctxt "error:xml_id_syntax_error:"
msgid "Syntax error for XML id %r in %s"
-msgstr "Error de sintaxi XML id %r a %s."
+msgstr "Error de sintaxi en el XML id %r de %s."
msgctxt "error:xml_record_desc:"
msgid "This record is part of the base configuration."
@@ -581,6 +583,10 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Acció"
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Actiu"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr "Data creació"
@@ -671,7 +677,7 @@ msgstr "Usuari creació"
msgctxt "field:ir.action.report,direct_print:"
msgid "Direct Print"
-msgstr "Direcció impressió"
+msgstr "Impressió directa"
msgctxt "field:ir.action.report,email:"
msgid "Email"
@@ -1259,7 +1265,7 @@ msgstr "Informació"
msgctxt "field:ir.model,model:"
msgid "Model Name"
-msgstr "Nom model"
+msgstr "Nom del model"
msgctxt "field:ir.model,module:"
msgid "Module"
@@ -1377,14 +1383,6 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr "Usuari creació"
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Data d'inicialització"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Data actualització"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "ID recurs"
@@ -1393,6 +1391,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr "Identificador en el sistema de fitxers"
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr "Valors en sistema de fitxers"
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr "ID"
@@ -1409,6 +1411,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "No actualitzat"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr "Sense sincronitzar"
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Nom"
@@ -1891,7 +1897,7 @@ msgstr "Desfasament de data-hora"
msgctxt "field:ir.sequence,timestamp_rounding:"
msgid "Timestamp Rounding"
-msgstr "Arronodiment de data-hora"
+msgstr "Arrodoniment de data-hora"
msgctxt "field:ir.sequence,type:"
msgid "Type"
@@ -1967,7 +1973,7 @@ msgstr "Desfasament de data-hora"
msgctxt "field:ir.sequence.strict,timestamp_rounding:"
msgid "Timestamp Rounding"
-msgstr "Arronodiment de data-hora"
+msgstr "Arrodoniment de data-hora"
msgctxt "field:ir.sequence.strict,type:"
msgid "Type"
@@ -2099,7 +2105,7 @@ msgstr "Mòdul"
msgctxt "field:ir.translation,name:"
msgid "Field Name"
-msgstr "Nom"
+msgstr "Nom del camp"
msgctxt "field:ir.translation,overriding_module:"
msgid "Overriding Module"
@@ -2333,6 +2339,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Acció"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr "Accions de teclat"
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Actiu"
@@ -2451,7 +2461,7 @@ msgstr "Domini"
msgctxt "field:ir.ui.view,field_childs:"
msgid "Children Field"
-msgstr "Camp fill"
+msgstr "Camp fills"
msgctxt "field:ir.ui.view,id:"
msgid "ID"
@@ -2635,7 +2645,7 @@ msgstr "Criteri de cerca per defecte en les vistes de llista."
msgctxt "help:ir.action.act_window,window_name:"
msgid "Use the action name as window name"
-msgstr "Utilitza el nom de l'acció pel nom de la finestra."
+msgstr "Utilitzeu el nom de l'acció pel nom de la finestra."
msgctxt "help:ir.action.report,email:"
msgid ""
@@ -2744,8 +2754,8 @@ msgid ""
"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
"0 for no delay."
msgstr ""
-"Interval mínim en minuts entre les crides \"Acció de funció\" pel mateix registre.\n"
-"0 per no interval."
+"Marge mínim en minuts entre les crides \"Acció de funció\" pel mateix registre.\n"
+"0 per no deixar marge."
msgctxt "help:ir.ui.view_search,domain:"
msgid "The PYSON domain"
@@ -2815,6 +2825,10 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Botons"
+msgctxt "model:ir.action,name:act_model_data_form"
+msgid "Data"
+msgstr "Dades"
+
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
msgstr "Permisos dels camps"
@@ -2927,6 +2941,16 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr "Domini acció de finestra"
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr "Tots"
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr "Sense sincronitzar"
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Vista acció de finestra"
@@ -2999,6 +3023,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Alemany"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr "Espanyol (Ecuador)"
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Anglès"
@@ -3045,7 +3073,7 @@ msgstr "Botó model"
msgctxt "model:ir.model.data,name:"
msgid "Model data"
-msgstr "Data model"
+msgstr "Dades del model"
msgctxt "model:ir.model.field,name:"
msgid "Model field"
@@ -3243,6 +3271,10 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Botons"
+msgctxt "model:ir.ui.menu,name:menu_model_data_form"
+msgid "Data"
+msgstr "Dades"
+
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
msgstr "Permisos camps"
@@ -3699,6 +3731,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Botons"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr "Dades del model"
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr "Sincronitza"
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Permisos dels camps"
diff --git a/trytond/ir/locale/cs_CZ.po b/trytond/ir/locale/cs_CZ.po
index 78ce3a0..a149887 100644
--- a/trytond/ir/locale/cs_CZ.po
+++ b/trytond/ir/locale/cs_CZ.po
@@ -21,7 +21,7 @@ msgstr ""
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
msgctxt "error:domain_validation_record:"
@@ -545,6 +545,10 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr ""
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr ""
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr ""
@@ -1341,14 +1345,6 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr ""
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr ""
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr ""
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr ""
@@ -1357,6 +1353,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr ""
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr ""
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr ""
@@ -1373,6 +1373,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr ""
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr ""
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr ""
@@ -2297,6 +2301,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr ""
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr ""
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr ""
@@ -2765,6 +2773,10 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr ""
+msgctxt "model:ir.action,name:act_model_data_form"
+msgid "Data"
+msgstr ""
+
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
msgstr ""
@@ -2877,6 +2889,16 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr ""
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr ""
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr ""
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr ""
@@ -2949,6 +2971,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr ""
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr ""
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr ""
@@ -3193,6 +3219,10 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr ""
+msgctxt "model:ir.ui.menu,name:menu_model_data_form"
+msgid "Data"
+msgstr ""
+
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
msgstr ""
@@ -3649,6 +3679,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr ""
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr ""
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr ""
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr ""
diff --git a/trytond/ir/locale/de_DE.po b/trytond/ir/locale/de_DE.po
index 22986fe..7009184 100644
--- a/trytond/ir/locale/de_DE.po
+++ b/trytond/ir/locale/de_DE.po
@@ -25,7 +25,7 @@ msgstr "Keine Löschberechtigung für diesen Datensatz"
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
"Die Anzahl der Nachkommastellen \"%(digits)s\" in Feld \"%(field)s\" in "
"\"%(value)s\" überschreitet die erlaubte Größe."
@@ -591,6 +591,10 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Aktion"
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Aktiv"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr "Erstellungsdatum"
@@ -965,7 +969,7 @@ msgstr "Ressource"
msgctxt "field:ir.attachment,summary:"
msgid "Summary"
-msgstr "Zusammenfassung"
+msgstr "Beschreibung"
msgctxt "field:ir.attachment,type:"
msgid "Type"
@@ -1387,14 +1391,6 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr "Erstellt durch"
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Initialdatum"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Aktualisierungsdatum"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "ID Ressource"
@@ -1403,6 +1399,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr "Identifikator im Dateisystem"
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr "Werte im Dateisystem"
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr "ID"
@@ -1417,7 +1417,11 @@ msgstr "Modul"
msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
-msgstr "Ohne Update"
+msgstr "Kein Update"
+
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr "Nicht synchronisiert"
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
@@ -2343,6 +2347,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Aktion"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr "Aktionsschlüsselwörter"
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Aktiv"
@@ -2830,6 +2838,10 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Knöpfe"
+msgctxt "model:ir.action,name:act_model_data_form"
+msgid "Data"
+msgstr "Daten"
+
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
msgstr "Zugriffsberechtigungen Felder"
@@ -2896,11 +2908,11 @@ msgstr "Übersetzungen"
msgctxt "model:ir.action,name:act_translation_set"
msgid "Set Report Translations"
-msgstr "Übersetzungen für Berichte aktualisieren"
+msgstr "Übersetzungen aktualisieren"
msgctxt "model:ir.action,name:act_translation_update"
msgid "Synchronize Translations"
-msgstr "Übersetzungen aktualisieren"
+msgstr "Übersetzungen eingeben"
msgctxt "model:ir.action,name:act_trigger_form"
msgid "Triggers"
@@ -2942,6 +2954,16 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr "Wertebereich Aktion Aktuelles Fenster"
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr "Alle"
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr "Nicht synchronisiert"
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Aktion aktives Fenster Sicht"
@@ -3014,6 +3036,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Deutsch"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr "Spanisch (Ecuador)"
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Englisch"
@@ -3258,6 +3284,10 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Knöpfe"
+msgctxt "model:ir.ui.menu,name:menu_model_data_form"
+msgid "Data"
+msgstr "Daten"
+
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
msgstr "Zugriffsberechtigungen Felder"
@@ -3324,11 +3354,11 @@ msgstr "Übersetzungen"
msgctxt "model:ir.ui.menu,name:menu_translation_set"
msgid "Set Translations"
-msgstr "Übersetzungen für Berichte aktualisieren"
+msgstr "Übersetzungen aktualisieren"
msgctxt "model:ir.ui.menu,name:menu_translation_update"
msgid "Synchronize Translations"
-msgstr "Übersetzungen aktualisieren"
+msgstr "Übersetzungen eingeben"
msgctxt "model:ir.ui.menu,name:menu_trigger_form"
msgid "Triggers"
@@ -3714,6 +3744,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Knöpfe"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr "Modelldaten"
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr "Synchronisation"
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Zugriffsberechtigung Felder"
@@ -3939,7 +3977,7 @@ msgstr "Übersetzungen bereinigen"
msgctxt "view:ir.translation.clean.succeed:"
msgid "Clean Translations Succeed!"
-msgstr "Bereinigung der Übersetzungen erfolgreich durchgeführt!"
+msgstr "Bereinigung der Übersetzungen erfolgreich durchgeführt"
msgctxt "view:ir.translation.export.result:"
msgid "Export Translation"
@@ -3951,7 +3989,7 @@ msgstr "Übersetzung exportieren"
msgctxt "view:ir.translation.set.start:"
msgid "Set Translations"
-msgstr "Übersetzungen für Berichte aktualisieren"
+msgstr "Übersetzungen aktualisieren"
msgctxt "view:ir.translation.set.start:"
msgid "Synchronize Translations?"
@@ -3959,15 +3997,15 @@ msgstr "Übersetzungen aktualisieren?"
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Succeed!"
-msgstr "Aktualisierung erfolgreich."
+msgstr "Aktualisierung erfolgreich"
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Translations"
-msgstr "Übersetzungen für Berichte aktualisieren"
+msgstr "Übersetzungen aktualisieren"
msgctxt "view:ir.translation.update.start:"
msgid "Synchronize Translations"
-msgstr "Übersetzungen aktualisieren"
+msgstr "Übersetzungen eingeben"
msgctxt "view:ir.translation:"
msgid "Translations"
@@ -4115,7 +4153,7 @@ msgstr "Abbrechen"
msgctxt "wizard_button:ir.translation.update,start,update:"
msgid "Update"
-msgstr "Aktualisieren"
+msgstr "Eingeben"
msgctxt "wizard_button:ir.ui.view.show,start,end:"
msgid "Close"
diff --git a/trytond/ir/locale/es_AR.po b/trytond/ir/locale/es_AR.po
index d727c5e..d59aedf 100644
--- a/trytond/ir/locale/es_AR.po
+++ b/trytond/ir/locale/es_AR.po
@@ -25,7 +25,7 @@ msgstr "No está autorizado a borrar este registro."
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
"El número de decimales «%(digits)s» del campo «%(field)s» en «%(value)s» "
"excede su límite."
@@ -254,7 +254,7 @@ msgstr ""
msgctxt "error:ir.trigger:"
msgid "\"On Time\" and others are mutually exclusive!"
-msgstr "¡\"Al Tiempo\" y otros son mutuamente excluyentes!"
+msgstr "¡«Al Tiempo» y otros son mutuamente excluyentes!"
msgctxt "error:ir.trigger:"
msgid ""
@@ -398,7 +398,7 @@ msgstr "ID"
msgctxt "field:ir.action,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Acciones de teclado"
msgctxt "field:ir.action,name:"
msgid "Name"
@@ -474,7 +474,7 @@ msgstr "ID"
msgctxt "field:ir.action.act_window,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Acciones de teclado"
msgctxt "field:ir.action.act_window,limit:"
msgid "Limit"
@@ -588,6 +588,10 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Acción"
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Activo"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr "Fecha creación"
@@ -642,7 +646,7 @@ msgstr "ID"
msgctxt "field:ir.action.keyword,keyword:"
msgid "Keyword"
-msgstr "Palabra clave"
+msgstr "Acción de teclado"
msgctxt "field:ir.action.keyword,model:"
msgid "Model"
@@ -702,7 +706,7 @@ msgstr "ID"
msgctxt "field:ir.action.report,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Acciones de teclado"
msgctxt "field:ir.action.report,model:"
msgid "Model"
@@ -802,7 +806,7 @@ msgstr "ID"
msgctxt "field:ir.action.url,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Acciones de teclado"
msgctxt "field:ir.action.url,name:"
msgid "Name"
@@ -866,7 +870,7 @@ msgstr "ID"
msgctxt "field:ir.action.wizard,keywords:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Acciones de teclado"
msgctxt "field:ir.action.wizard,model:"
msgid "Model"
@@ -950,7 +954,7 @@ msgstr "Enlace"
msgctxt "field:ir.attachment,name:"
msgid "Name"
-msgstr "Nombre del adjunto"
+msgstr "Nombre"
msgctxt "field:ir.attachment,rec_name:"
msgid "Name"
@@ -1384,21 +1388,17 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr "Usuario creación"
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Fecha de inicio"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Fecha de actualización"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "ID del recurso"
msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
-msgstr "Identificador en el sistema de archivos"
+msgstr "Identificador en el Sistema de Archivos"
+
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr "Valores en el Sistema de Archivos"
msgctxt "field:ir.model.data,id:"
msgid "ID"
@@ -1416,6 +1416,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "No Actualizar"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr "Sin sincronizar"
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Nombre"
@@ -2340,6 +2344,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Acción"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr "Acciones de teclado"
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Activo"
@@ -2824,6 +2832,10 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Botones"
+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 "Campos de Acceso"
@@ -2862,7 +2874,7 @@ msgstr "Propiedades predeterminadas"
msgctxt "model:ir.action,name:act_rule_group_form"
msgid "Record Rules"
-msgstr "Grabar reglas"
+msgstr "Reglas de registros"
msgctxt "model:ir.action,name:act_sequence_form"
msgid "Sequences"
@@ -2936,13 +2948,23 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr "Dominio de acción de ventana"
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr "Todo"
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr "Sin sincronizar"
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Acción de vista de ventana"
msgctxt "model:ir.action.keyword,name:"
msgid "Action keyword"
-msgstr "Palabra clave de acción"
+msgstr "Acción de teclado"
msgctxt "model:ir.action.report,name:"
msgid "Action report"
@@ -3008,6 +3030,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Alemán"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr "Español (Ecuador)"
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Inglés"
@@ -3066,7 +3092,7 @@ msgstr "Modelo de Acceso a Campo"
msgctxt "model:ir.model.print_model_graph.start,name:"
msgid "Print Model Graph"
-msgstr "Imprimir Gráfico del Modelo"
+msgstr "Imprimir gráfico de modelos"
msgctxt "model:ir.module.module,name:"
msgid "Module"
@@ -3253,9 +3279,13 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Botones"
+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 "Campos de Acceso"
+msgstr "Acceso a los campos"
msgctxt "model:ir.ui.menu,name:menu_model_form"
msgid "Models"
@@ -3287,7 +3317,7 @@ msgstr "Propiedades predeterminadas"
msgctxt "model:ir.ui.menu,name:menu_rule_group_form"
msgid "Record Rules"
-msgstr "Grabar reglas"
+msgstr "Reglas de registros"
msgctxt "model:ir.ui.menu,name:menu_scheduler"
msgid "Scheduler"
@@ -3379,11 +3409,11 @@ msgstr "Ancho vista de árbol"
msgctxt "selection:ir.action.keyword,keyword:"
msgid "Action form"
-msgstr "Formulario de acción"
+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"
@@ -3619,11 +3649,11 @@ msgstr "Abrir una ventana"
msgctxt "view:ir.action.keyword:"
msgid "Keyword"
-msgstr "Palabra clave"
+msgstr "Acción de teclado"
msgctxt "view:ir.action.keyword:"
msgid "Keywords"
-msgstr "Palabras clave"
+msgstr "Acciones de teclado"
msgctxt "view:ir.action.report:"
msgid "General"
@@ -3709,6 +3739,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Botones"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr "Modelo de Datos"
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr "Sincronizar"
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Acceso al Campo"
@@ -3723,7 +3761,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 de modelos"
msgctxt "view:ir.model:"
msgid "Model Description"
@@ -3833,7 +3871,7 @@ msgstr ""
msgctxt "view:ir.rule.group:"
msgid "Record rules"
-msgstr "Grabar reglas"
+msgstr "Reglas de registros"
msgctxt "view:ir.rule.group:"
msgid "The rule is satisfied if at least one test is True"
diff --git a/trytond/ir/locale/es_CO.po b/trytond/ir/locale/es_CO.po
index 01c59ac..7b69425 100644
--- a/trytond/ir/locale/es_CO.po
+++ b/trytond/ir/locale/es_CO.po
@@ -23,7 +23,7 @@ msgstr "No está autorizado a borrar este registro."
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
"El numero de decimales \"%(digits)s\" del campo \"%(field)s\" en el valor "
"\"%(value)s\" excede el limite."
@@ -120,19 +120,19 @@ msgstr "decimal_point y «thousands_sep» deben ser distintos"
msgctxt "error:ir.model.access:"
msgid "You can not create this kind of document! (%s)"
-msgstr "No puede crear este tipo de documento (%s)"
+msgstr "No puede crear este tipo de documento! (%s)"
msgctxt "error:ir.model.access:"
msgid "You can not delete this document! (%s)"
-msgstr "No puede borrar este documento (%s)"
+msgstr "No puede borrar este documento! (%s)"
msgctxt "error:ir.model.access:"
msgid "You can not read this document! (%s)"
-msgstr "No puede leer este documento (%s)"
+msgstr "No puede leer este documento! (%s)"
msgctxt "error:ir.model.access:"
msgid "You can not write in this document! (%s)"
-msgstr "No puede escribir en este documento (%s)"
+msgstr "No puede escribir en este documento! (%s)"
msgctxt "error:ir.model.button:"
msgid "The button name in model must be unique!"
@@ -586,6 +586,10 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Acción"
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Activo"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr "Fecha de Creación"
@@ -1382,14 +1386,6 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr "Creado por Usuario"
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Fecha Inicial"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Fecha de Actualización"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "ID del recurso"
@@ -1398,6 +1394,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr "Identificador en Sistema de Archivos"
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr "Valores en el Sistema de Archivos"
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr "ID"
@@ -1414,6 +1414,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "No Actualizar"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr "Sin Sincronizar"
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Nombre"
@@ -2338,6 +2342,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Acción"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr "Teclas de Acción"
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Activo"
@@ -2772,7 +2780,7 @@ msgstr "Acciones"
msgctxt "model:ir.action,name:act_action_report_form"
msgid "Reports"
-msgstr "Informes"
+msgstr "Reportes"
msgctxt "model:ir.action,name:act_action_url_form"
msgid "URLs"
@@ -2822,6 +2830,10 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Botones"
+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 Campos"
@@ -2934,6 +2946,16 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr "Accion en dominio de ventana de acción"
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr "Todo"
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr "Sin Sincronizar"
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Acción en vista de ventana de acción"
@@ -3006,6 +3028,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Alemán"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr "Español (Ecuador)"
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Inglés"
@@ -3048,7 +3074,7 @@ msgstr "Permisos de modelo"
msgctxt "model:ir.model.button,name:"
msgid "Model Button"
-msgstr "Modelo de Botón"
+msgstr "Modelo Botón"
msgctxt "model:ir.model.data,name:"
msgid "Model data"
@@ -3145,11 +3171,11 @@ msgstr "Iniciar limpieza de traducciones"
msgctxt "model:ir.translation.export.result,name:"
msgid "Export translation"
-msgstr "Exportar traducción - archivo"
+msgstr "Exportar traducción"
msgctxt "model:ir.translation.export.start,name:"
msgid "Export translation"
-msgstr "Exportar traducción - archivo"
+msgstr "Exportar traducción"
msgctxt "model:ir.translation.set.start,name:"
msgid "Set Translation"
@@ -3193,7 +3219,7 @@ msgstr "Ventana de Acciones"
msgctxt "model:ir.ui.menu,name:menu_action_report_form"
msgid "Reports"
-msgstr "Informes"
+msgstr "Reportes"
msgctxt "model:ir.ui.menu,name:menu_action_url"
msgid "URLs"
@@ -3251,6 +3277,10 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Botones"
+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"
@@ -3629,11 +3659,11 @@ msgstr "General"
msgctxt "view:ir.action.report:"
msgid "Report"
-msgstr "Informe"
+msgstr "Reporte"
msgctxt "view:ir.action.report:"
msgid "Report xml"
-msgstr "Informe xml"
+msgstr "Reporte xml"
msgctxt "view:ir.action.url:"
msgid "General"
@@ -3697,7 +3727,7 @@ msgstr "Formato de Números"
msgctxt "view:ir.model.access:"
msgid "Access controls"
-msgstr "Acceso a controles"
+msgstr "Controles de acceso"
msgctxt "view:ir.model.button:"
msgid "Button"
@@ -3707,6 +3737,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Botones"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr "Modelo de Datos"
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr "Syncronizar"
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Permiso de Campo"
diff --git a/trytond/ir/locale/es_CO.po b/trytond/ir/locale/es_EC.po
similarity index 91%
copy from trytond/ir/locale/es_CO.po
copy to trytond/ir/locale/es_EC.po
index 01c59ac..de8595c 100644
--- a/trytond/ir/locale/es_CO.po
+++ b/trytond/ir/locale/es_EC.po
@@ -7,26 +7,28 @@ msgid ""
"You try to bypass an access rule!\n"
"(Document type: %s)"
msgstr ""
-"Está tratando de saltarse una regla de acceso!\n"
+"¡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 "Esta intentando evitar una regla de acceso."
+msgstr ""
+"Esta intentando evitar una regla de acceso.\n"
+"(Tipo de documento: %s)"
msgctxt "error:delete_xml_record:"
msgid "You are not allowed to delete this record."
-msgstr "No está autorizado a borrar este registro."
+msgstr "No está autorizado para eliminar este registro."
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
-"El numero de decimales \"%(digits)s\" del campo \"%(field)s\" en el valor "
-"\"%(value)s\" excede el limite."
+"El número de decimales \"%(digits)s\" del campo \"%(field)s\" en el "
+"valor\"%(value)s\" excede su limite."
msgctxt "error:domain_validation_record:"
msgid ""
@@ -41,40 +43,38 @@ msgid ""
"Could not delete the records because they are used on field \"%(field)s\" of"
" \"%(model)s\"."
msgstr ""
-"No se pudieron borrar los registros porque son usados en el campo "
+"No se pudieron eliminar los registros porque son utilizados en el campo "
"\"%(field)s\" de \"%(model)s\"."
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\" en el campo \"%(field)s\" en el modelo \"%(model)s\" "
-"no existe."
+msgstr "El valor \"%(value)s\" del campo \"%(field)s\" en \"%(model)s\" no existe."
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
-msgstr "Contexto inválido \"%(context)s\" en action \"%(action)s\"."
+msgstr "El contexto \"%(context)s\" en la acción \"%(action)s\" no es válido."
msgctxt "error:ir.action.act_window:"
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\"."
+"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 view \"%(view)s\" for action \"%(action)s\"."
-msgstr "Vista \"%(view)s\" inválida para la acción \"%(action)s\"."
+msgstr "La vista \"%(view)s\" para la acción \"%(action)s\" no es válida."
msgctxt "error:ir.action.keyword:"
msgid "Wrong wizard model in keyword action \"%s\"."
-msgstr "Errado modelo asistente para acción la \"%s\"."
+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 "El correo electronico definido en el informe \"%s\" es inválido."
+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 de los módulos debe ser único!"
+msgstr "¡El nombre interno debe ser único por módulo!"
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
@@ -92,23 +92,25 @@ msgid ""
"\n"
"%s\n"
msgstr ""
-"La siguiente acción falló al ejecutar apropiadamente: \"%s\"\n"
+"La siguiente acción falló cuando se ejecutaba: \"%s\"\n"
"%s\n"
-" Traceback:\n"
+" Traza del programa:\n"
"\n"
-"%s"
+"%s\n"
msgctxt "error:ir.lang:"
msgid "Default language can not be deleted."
-msgstr "Los lenguajes por defecto no pueden ser borrados."
+msgstr "El idioma por defecto no puede ser eliminado."
msgctxt "error:ir.lang:"
msgid "Invalid date format \"%(format)s\" on \"%(language)s\" language."
-msgstr "Formato de fecha inválido \"%(format)s\" para el lenguaje \"%(languages)s\"."
+msgstr ""
+"El formato de la fecha \"%(format)s\" para el idioma \"%(languages)s\" no es"
+" válido."
msgctxt "error:ir.lang:"
msgid "Invalid grouping \"%(grouping)s\" on \"%(language)s\" language."
-msgstr "Agrupamiento inválido \"%(grouping)s\" en lenguaje \"%(languages)s\"."
+msgstr "Agrupamiento no válido \"%(grouping)s\" en idioma \"%(languages)s\"."
msgctxt "error:ir.lang:"
msgid "The default language must be translatable."
@@ -116,7 +118,7 @@ msgstr "El idioma por defecto debe ser traducible."
msgctxt "error:ir.lang:"
msgid "decimal_point and thousands_sep must be different!"
-msgstr "decimal_point y «thousands_sep» deben ser distintos"
+msgstr "¡el punto decimal y el separador de miles deben ser distintos!"
msgctxt "error:ir.model.access:"
msgid "You can not create this kind of document! (%s)"
@@ -124,7 +126,7 @@ msgstr "No puede crear este tipo de documento (%s)"
msgctxt "error:ir.model.access:"
msgid "You can not delete this document! (%s)"
-msgstr "No puede borrar este documento (%s)"
+msgstr "No puede eliminar este documento (%s)"
msgctxt "error:ir.model.access:"
msgid "You can not read this document! (%s)"
@@ -136,7 +138,7 @@ msgstr "No puede escribir en este documento (%s)"
msgctxt "error:ir.model.button:"
msgid "The button name in model must be unique!"
-msgstr "El nombre del botón en el modelo debe ser único!"
+msgstr "¡El nombre del botón en el modelo debe ser único!"
msgctxt "error:ir.model.data:"
msgid "The triple (fs_id, module, model) must be unique!"
@@ -152,7 +154,7 @@ msgstr "¡No puede escribir en el campo! (%s.%s)"
msgctxt "error:ir.model.field:"
msgid "Model Field name \"%s\" is not a valid python identifier."
-msgstr "El nombre del Modelo Campo \"%s\" no es un identificador python válido."
+msgstr "El nombre del Campo Modelo \"%s\" no es un identificador python válido."
msgctxt "error:ir.model.field:"
msgid "The field name in model must be unique!"
@@ -194,21 +196,19 @@ msgstr "Global y Predeterminado son mutuamente excluyentes"
msgctxt "error:ir.rule:"
msgid "Invalid domain in rule \"%s\"."
-msgstr "Dominio inválido en regla \"%s\"."
+msgstr "Dominio no válido en la regla \"%s\"."
msgctxt "error:ir.sequence.strict:"
msgid "Invalid prefix \"%(prefix)s\" on sequence \"%(sequence)s\"."
-msgstr "Prefijo inválido \"%(prefix)s\" en secuencia \"%(sequence)s\"."
+msgstr "El prefijo \"%(prefix)s\" en la secuencia \"%(sequence)s\" no es válido."
msgctxt "error:ir.sequence.strict:"
msgid "Invalid suffix \"%(suffix)s\" on sequence \"%(sequence)s\"."
-msgstr "Sufijo inválido \"%(suffix)s\" en secuencia \"%(sequence)s\"."
+msgstr "El sufijo \"%(sufix)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 marca de tiempo no puede estar en el futuro para la secuencia "
-"\"%s\"."
+msgstr "La ultima fecha-hora no puede ser del futuro para la secuencia \"%s\"."
msgctxt "error:ir.sequence.strict:"
msgid "Missing sequence."
@@ -216,21 +216,20 @@ msgstr "Falta la secuencia."
msgctxt "error:ir.sequence.strict:"
msgid "Timestamp rounding should be greater than 0"
-msgstr "El redondeo de la marca de tiempo debe ser mayor que 0"
+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 "Prefijo inválido \"%(prefix)s\" en secuencia \"%(sequence)s\"."
+msgstr "El prefijo \"%(prefix)s\" en la secuencia \"%(sequence)s\" no es válido."
msgctxt "error:ir.sequence:"
msgid "Invalid suffix \"%(suffix)s\" on sequence \"%(sequence)s\"."
-msgstr "Sufijo inválido \"%(suffix)s\" en secuencia \"%(sequence)s\"."
+msgstr "El sufijo \"%(sufix)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 marca de tiempo no puede estar en el futuro para la secuencia "
-"\"%s\"."
+"La ultima fecha-hora no puede estar en el futuro para la secuencia \"%s\"."
msgctxt "error:ir.sequence:"
msgid "Missing sequence."
@@ -238,7 +237,7 @@ msgstr "Falta la secuencia."
msgctxt "error:ir.sequence:"
msgid "Timestamp rounding should be greater than 0"
-msgstr "El redondeo de la marca de tiempo debe ser mayor que 0"
+msgstr "El redondeo de la fecha-hora debe ser mayor que 0"
msgctxt "error:ir.translation:"
msgid "Translation must be unique"
@@ -249,19 +248,19 @@ 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 esta es una traducción "
+"No puede exportar la traduccion %(name)s porque es una traducción "
"sobreescrita por el módulo %(overrinding_module)s"
msgctxt "error:ir.trigger:"
msgid "\"On Time\" and others are mutually exclusive!"
-msgstr "\"Al Tiempo\" y otros son mutuamente excluyentes!"
+msgstr "¡\"A Tiempo\" y otros son mutuamente excluyentes!"
msgctxt "error:ir.trigger:"
msgid ""
"Condition \"%(condition)s\" is not a valid python expression on trigger "
"\"%(trigger)s\"."
msgstr ""
-"Condición \"%(condition)s\" no es una expressión python válida en el "
+"La condición \"%(condition)s\" no es una expressión python válida en el "
"disparador \"%s(trigger)s\"."
msgctxt "error:ir.ui.menu:"
@@ -270,21 +269,23 @@ msgstr "\"%s\" no es un nombre válido de menú porque no esta permitido contene
msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
-msgstr "XML inválido para la vista \"%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"
+"¡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 "Esta intentando leer registros que ya no existen."
+msgstr ""
+"Está intentando leer registros que ya no existen.\n"
+"(Tipo de documento: %s)"
msgctxt "error:recursion_error:"
msgid ""
@@ -292,7 +293,7 @@ msgid ""
" was configured as ancestor of itself."
msgstr ""
"Error de recursión: El registro \"%(rec_name)s\" con padre \"%(rec_name)s\" "
-"fue configurado como antecesor del mismo."
+"fue configurado como antecesor de si mismo."
msgctxt "error:reference_syntax_error:"
msgid "Syntax error for reference %r in %s"
@@ -304,33 +305,33 @@ 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 el modelo \"%(model)s\" es obligatorio."
+msgstr "El campo \"%(field)s\" en \"%(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 el modelo \"%(model)s\" es obligatorio."
+msgstr "El campo \"%(field)s\" en \"%(model)s\" es requerido."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
-msgstr "Falta la función en el campo \"%s\"."
+msgstr "Falta la función de búsqueda en el campo \"%s\"."
msgctxt "error:selection_validation_record:"
msgid ""
"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
"the selection."
msgstr ""
-"El valor \"%(value)s\" en el campo \"%(field)s\" en el modelo \"%(model)s\" "
-"no esta en la selección."
+"El valor \"%(value)s\" del campo \"%(field)s\" en el modelo \"%(model)s\" no"
+" esta en la selección."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
-msgstr "El valor no esta en la selección para el campo \"%s\"."
+msgstr "El valor no 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 el modelo \"%(model)s\" is"
-" demasiado largo."
+"El tamaño \"%(size)s\" del campo \"%(field)s\" en \"%(model)s\" is demasiado"
+" largo."
msgctxt "error:time_format_validation_record:"
msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
@@ -347,7 +348,7 @@ 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"
+"¡Está tratando de escribir en registros que ya no existen!\n"
"(Tipo de documento: %s)"
msgctxt "error:write_error:"
@@ -364,7 +365,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 id XML %r en %s"
+msgstr "Error de sintaxis para XML id %r en %s"
msgctxt "error:xml_record_desc:"
msgid "This record is part of the base configuration."
@@ -396,7 +397,7 @@ msgstr "ID"
msgctxt "field:ir.action,keywords:"
msgid "Keywords"
-msgstr "Teclas"
+msgstr "Acciones de teclado"
msgctxt "field:ir.action,name:"
msgid "Name"
@@ -472,7 +473,7 @@ msgstr "ID"
msgctxt "field:ir.action.act_window,keywords:"
msgid "Keywords"
-msgstr "Teclas"
+msgstr "Acciones de teclado"
msgctxt "field:ir.action.act_window,limit:"
msgid "Limit"
@@ -586,6 +587,10 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Acción"
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Activo"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr "Fecha de Creación"
@@ -700,7 +705,7 @@ msgstr "ID"
msgctxt "field:ir.action.report,keywords:"
msgid "Keywords"
-msgstr "Teclas"
+msgstr "Acciones de teclado"
msgctxt "field:ir.action.report,model:"
msgid "Model"
@@ -752,7 +757,7 @@ msgstr "Estilo"
msgctxt "field:ir.action.report,template_extension:"
msgid "Template Extension"
-msgstr "Plantilla de Extensión"
+msgstr "Extensión de Plantilla"
msgctxt "field:ir.action.report,type:"
msgid "Type"
@@ -1060,11 +1065,11 @@ msgstr "ID"
msgctxt "field:ir.cron,interval_number:"
msgid "Interval Number"
-msgstr "Número de Intervalo"
+msgstr "Número de Intervalos"
msgctxt "field:ir.cron,interval_type:"
msgid "Interval Unit"
-msgstr "Unidad Intervalo"
+msgstr "Unidad de Intervalo"
msgctxt "field:ir.cron,model:"
msgid "Model"
@@ -1092,7 +1097,7 @@ msgstr "Repetir Fallidos"
msgctxt "field:ir.cron,request_user:"
msgid "Request User"
-msgstr "Petición de Usuario"
+msgstr "Solicitud de Usuario"
msgctxt "field:ir.cron,user:"
msgid "Execution User"
@@ -1382,21 +1387,17 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr "Creado por Usuario"
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Fecha Inicial"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Fecha de Actualización"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "ID del recurso"
msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
-msgstr "Identificador en Sistema de Archivos"
+msgstr "Identificador en el Sistema de Archivos"
+
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr "Valores del Sistema de Archivos"
msgctxt "field:ir.model.data,id:"
msgid "ID"
@@ -1414,6 +1415,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "No Actualizar"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr "Fuera de Sincronización"
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Nombre"
@@ -1472,7 +1477,7 @@ msgstr "Nombre"
msgctxt "field:ir.model.field,relation:"
msgid "Model Relation"
-msgstr "Modelo Relación"
+msgstr "Relación del Modelo"
msgctxt "field:ir.model.field,ttype:"
msgid "Field Type"
@@ -1516,7 +1521,7 @@ msgstr "Permiso para Crear"
msgctxt "field:ir.model.field.access,perm_delete:"
msgid "Delete Access"
-msgstr "Permiso para Borrar"
+msgstr "Permiso para Eliminar"
msgctxt "field:ir.model.field.access,perm_read:"
msgid "Read Access"
@@ -1804,7 +1809,7 @@ msgstr "Permiso para Crear"
msgctxt "field:ir.rule.group,perm_delete:"
msgid "Delete Access"
-msgstr "Permiso para Borrar"
+msgstr "Permiso para Eliminar"
msgctxt "field:ir.rule.group,perm_read:"
msgid "Read Access"
@@ -1856,7 +1861,7 @@ msgstr "ID"
msgctxt "field:ir.sequence,last_timestamp:"
msgid "Last Timestamp"
-msgstr "Última Marca de Tiempo"
+msgstr "Última Fecha-hora"
msgctxt "field:ir.sequence,name:"
msgid "Sequence Name"
@@ -1868,11 +1873,11 @@ msgstr "Número de Incremento"
msgctxt "field:ir.sequence,number_next:"
msgid "Next Number"
-msgstr "Número Siguiente"
+msgstr "Siguiente Número"
msgctxt "field:ir.sequence,number_next_internal:"
msgid "Next Number"
-msgstr "Número Siguiente"
+msgstr "Siguiente Número"
msgctxt "field:ir.sequence,padding:"
msgid "Number padding"
@@ -1892,11 +1897,11 @@ msgstr "Sufijo"
msgctxt "field:ir.sequence,timestamp_offset:"
msgid "Timestamp Offset"
-msgstr "Compensación de Marca de Tiempo"
+msgstr "Desfase de Fecha-hora"
msgctxt "field:ir.sequence,timestamp_rounding:"
msgid "Timestamp Rounding"
-msgstr "Redondeo de Marca de Tiempo"
+msgstr "Redondeo de Fecha-hora"
msgctxt "field:ir.sequence,type:"
msgid "Type"
@@ -1932,7 +1937,7 @@ msgstr "ID"
msgctxt "field:ir.sequence.strict,last_timestamp:"
msgid "Last Timestamp"
-msgstr "Última Marca de Tiempo"
+msgstr "Última Fecha-hora"
msgctxt "field:ir.sequence.strict,name:"
msgid "Sequence Name"
@@ -1944,11 +1949,11 @@ msgstr "Número de Incremento"
msgctxt "field:ir.sequence.strict,number_next:"
msgid "Next Number"
-msgstr "Número Siguiente"
+msgstr "Siguiente Número"
msgctxt "field:ir.sequence.strict,number_next_internal:"
msgid "Next Number"
-msgstr "Número Siguiente"
+msgstr "Siguiente Número"
msgctxt "field:ir.sequence.strict,padding:"
msgid "Number padding"
@@ -1968,11 +1973,11 @@ msgstr "Sufijo"
msgctxt "field:ir.sequence.strict,timestamp_offset:"
msgid "Timestamp Offset"
-msgstr "Compensación de Marca de Tiempo"
+msgstr "Desfase de Fecha-hora"
msgctxt "field:ir.sequence.strict,timestamp_rounding:"
msgid "Timestamp Rounding"
-msgstr "Redondeo de Marca de Tiempo"
+msgstr "Redondeo de Fecha-hora"
msgctxt "field:ir.sequence.strict,type:"
msgid "Type"
@@ -2084,7 +2089,7 @@ msgstr "Creado por Usuario"
msgctxt "field:ir.translation,fuzzy:"
msgid "Fuzzy"
-msgstr "Vago"
+msgstr "Confuso"
msgctxt "field:ir.translation,id:"
msgid "ID"
@@ -2188,11 +2193,11 @@ msgstr "Idioma"
msgctxt "field:ir.trigger,action_function:"
msgid "Action Function"
-msgstr "Acción Función"
+msgstr "Función de la Acción"
msgctxt "field:ir.trigger,action_model:"
msgid "Action Model"
-msgstr "Acción en Modelo"
+msgstr "Acción de Modelo"
msgctxt "field:ir.trigger,active:"
msgid "Active"
@@ -2338,6 +2343,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Acción"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr "Teclas de Acción"
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Activo"
@@ -2436,7 +2445,7 @@ msgstr "Modificado por Usuario"
msgctxt "field:ir.ui.view,arch:"
msgid "View Architecture"
-msgstr "Ver arquitectura"
+msgstr "Ver Arquitectura"
msgctxt "field:ir.ui.view,create_date:"
msgid "Create Date"
@@ -2632,11 +2641,11 @@ 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 en lista"
+msgstr "Límite predeterminado para la vista de lista"
msgctxt "help:ir.action.act_window,search_value:"
msgid "Default search criteria for the list view"
-msgstr "Criterio de búsqueda por defecto para la vista en lista"
+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"
@@ -2648,7 +2657,7 @@ msgid ""
"Example: {'to': 'test at example.com', 'cc': 'user at example.com'}"
msgstr ""
"Diccionario de Python donde las claves definen \"to\" \"cc\" \"subject\"\n"
-"Ejemplo: {'to': 'test at example.com', 'cc': 'user at example.com'}"
+"Ejemplo: {'to': 'test at ejemplo.com', 'cc': 'usuario at ejemplo.com'}"
msgctxt "help:ir.action.report,extension:"
msgid ""
@@ -2664,7 +2673,7 @@ msgstr "Define el estilo a aplicar en el informe."
msgctxt "help:ir.action.wizard,window:"
msgid "Run wizard in a new window"
-msgstr "Ejecutar asistente en una nueva ventana"
+msgstr "Ejecutar el asistente en una nueva ventana"
msgctxt "help:ir.cron,number_calls:"
msgid ""
@@ -2684,7 +2693,7 @@ msgstr "El usuario que se utilizará para ejecutar esta acción"
msgctxt "help:ir.lang,code:"
msgid "RFC 4646 tag: http://tools.ietf.org/html/rfc4646"
-msgstr "RFC 4646 tag: http://tools.ietf.org/html/rfc4646"
+msgstr "Etiqueta RFC 4646: http://tools.ietf.org/html/rfc4646"
msgctxt "help:ir.model,module:"
msgid "Module in which this model is defined"
@@ -2707,8 +2716,8 @@ msgid ""
"Entering a Python Regular Expression will exclude matching models from the "
"graph."
msgstr ""
-"Ingresando una Expresión Regular de Python se excluirán modelos que "
-"concuerden con el gráfico."
+"Ingresando una Expresión Regular de Python se excluirán del gráfico modelos "
+"que concuerden."
msgctxt "help:ir.rule,domain:"
msgid "Domain is evaluated with \"user\" as the current user"
@@ -2716,7 +2725,7 @@ msgstr "El dominio es evaluado con el \"usuario\" como el usuario actual"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
-msgstr "Añadir la regla a todos los usuarios por defecto"
+msgstr "Añadir esta regla a todos los usuarios por defecto"
msgctxt "help:ir.rule.group,global_p:"
msgid ""
@@ -2728,7 +2737,7 @@ msgstr ""
msgctxt "help:ir.rule.group,rules:"
msgid "The rule is satisfied if at least one test is True"
-msgstr "La regla se satisface si al menos una condición es cierta"
+msgstr "La regla se satisface si al menos una condición es verdadera"
msgctxt "help:ir.trigger,condition:"
msgid ""
@@ -2788,7 +2797,7 @@ msgstr "Adjuntos"
msgctxt "model:ir.action,name:act_config_wizard_item_form"
msgid "Config Wizard Items"
-msgstr "Asistentes de Configuración"
+msgstr "Elementos del Asistente de Configuración"
msgctxt "model:ir.action,name:act_cron_form"
msgid "Scheduled Actions"
@@ -2822,9 +2831,14 @@ 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 Campos"
+msgstr "Permisos de los Campos"
msgctxt "model:ir.action,name:act_model_fields_form"
msgid "Fields"
@@ -2836,7 +2850,7 @@ msgstr "Modelos"
msgctxt "model:ir.action,name:act_module_config"
msgid "Configure Modules"
-msgstr "Configuración de Modulos"
+msgstr "Configuración de Módulos"
msgctxt "model:ir.action,name:act_module_config_wizard"
msgid "Module Configuration"
@@ -2916,7 +2930,7 @@ msgstr "Estado de Arbol"
msgctxt "model:ir.action,name:act_view_tree_width_form"
msgid "View Tree Width"
-msgstr "Ancho de Columna"
+msgstr "Ancho de la Vista de Arbol"
msgctxt "model:ir.action,name:print_model_graph"
msgid "Graph"
@@ -2934,13 +2948,24 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr "Accion en dominio de ventana de acción"
+#, fuzzy
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+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"
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Acción en vista de ventana de acción"
msgctxt "model:ir.action.keyword,name:"
msgid "Action keyword"
-msgstr "Teclas de acción "
+msgstr "Tecla de acción "
msgctxt "model:ir.action.report,name:"
msgid "Action report"
@@ -2952,7 +2977,7 @@ msgstr "URL de la acción"
msgctxt "model:ir.action.wizard,name:"
msgid "Action wizard"
-msgstr "Acción del Asistente"
+msgstr "Asistente de la acción"
msgctxt "model:ir.attachment,name:"
msgid "Attachment"
@@ -3006,6 +3031,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Alemán"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr "Español (Ecuador)"
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Inglés"
@@ -3048,19 +3077,19 @@ msgstr "Permisos de modelo"
msgctxt "model:ir.model.button,name:"
msgid "Model Button"
-msgstr "Modelo de Botón"
+msgstr "Botón de Modelo"
msgctxt "model:ir.model.data,name:"
msgid "Model data"
-msgstr "Modelo de datos"
+msgstr "Datos de modelo"
msgctxt "model:ir.model.field,name:"
msgid "Model field"
-msgstr "Modelo de campo"
+msgstr "Campo de modelo"
msgctxt "model:ir.model.field.access,name:"
msgid "Model Field Access"
-msgstr "Permiso de Modelo Campo"
+msgstr "Permiso Campo de Modelo"
msgctxt "model:ir.model.print_model_graph.start,name:"
msgid "Print Model Graph"
@@ -3072,20 +3101,19 @@ msgstr "Módulo"
msgctxt "model:ir.module.module.config_wizard.done,name:"
msgid "Module Config Wizard Done"
-msgstr "Configuración del Módulo Terminada"
+msgstr "Asitente de Configuración del Módulo - Realizado"
msgctxt "model:ir.module.module.config_wizard.first,name:"
msgid "Module Config Wizard First"
-msgstr "Primer Asistente de Configuración de 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"
-msgstr ""
-"Asistente de configuración a ejecutar después de la instalaciń del módulo"
+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 "Siguiente Asistente de Configuración de Módulo"
+msgstr "Asistente de Configuración del Módulo - Otro"
msgctxt "model:ir.module.module.dependency,name:"
msgid "Module dependency"
@@ -3093,11 +3121,11 @@ msgstr "Dependencias del módulo"
msgctxt "model:ir.module.module.install_upgrade.done,name:"
msgid "Module Install Upgrade Done"
-msgstr "Actualización del Módulo Terminada"
+msgstr "Instalación / Actualización del Módulo - Realizada"
msgctxt "model:ir.module.module.install_upgrade.start,name:"
msgid "Module Install Upgrade Start"
-msgstr "Inicio de Asistente de Instalación del Módulo"
+msgstr "Instalación / Actualización del Módulo - Iniciar"
msgctxt "model:ir.property,name:"
msgid "Property"
@@ -3109,7 +3137,7 @@ msgstr "Regla"
msgctxt "model:ir.rule.group,name:"
msgid "Rule group"
-msgstr "Regla de grupo"
+msgstr "Grupo de reglas"
msgctxt "model:ir.sequence,name:"
msgid "Sequence"
@@ -3213,7 +3241,7 @@ msgstr "Adjuntos"
msgctxt "model:ir.ui.menu,name:menu_config_wizard_item_form"
msgid "Config Wizard Items"
-msgstr "Asistentes de Configuración"
+msgstr "Elementos del Asistente de Configuración"
msgctxt "model:ir.ui.menu,name:menu_cron_form"
msgid "Scheduled Actions"
@@ -3251,6 +3279,11 @@ 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"
@@ -3317,7 +3350,7 @@ msgstr "Traducciones"
msgctxt "model:ir.ui.menu,name:menu_translation_set"
msgid "Set Translations"
-msgstr "Establecer Traducción"
+msgstr "Establecer Traducciones"
msgctxt "model:ir.ui.menu,name:menu_translation_update"
msgid "Synchronize Translations"
@@ -3345,7 +3378,7 @@ msgstr "Estado de Arbol"
msgctxt "model:ir.ui.menu,name:menu_view_tree_width"
msgid "View Tree Width"
-msgstr "Ancho de Columna"
+msgstr "Ancho de la Vista de Arbol"
msgctxt "model:ir.ui.menu,name:model_model_fields_form"
msgid "Fields"
@@ -3373,7 +3406,7 @@ msgstr "Estado de Vista de Arbol"
msgctxt "model:ir.ui.view_tree_width,name:"
msgid "View Tree Width"
-msgstr "Ancho de Columna"
+msgstr "Ancho de la Vista de Arbol"
msgctxt "selection:ir.action.keyword,keyword:"
msgid "Action form"
@@ -3433,7 +3466,7 @@ msgstr "De izquierda a derecha"
msgctxt "selection:ir.lang,direction:"
msgid "Right-to-left"
-msgstr "Derecha-a-Izquierda"
+msgstr "De derecha a Izquierda"
msgctxt "selection:ir.module.module,state:"
msgid "Installed"
@@ -3445,15 +3478,15 @@ msgstr "No instalado"
msgctxt "selection:ir.module.module,state:"
msgid "To be installed"
-msgstr "Pendiente de ser instalado"
+msgstr "Para ser instalado"
msgctxt "selection:ir.module.module,state:"
msgid "To be removed"
-msgstr "Pendiente de ser eliminado"
+msgstr "Para ser eliminado"
msgctxt "selection:ir.module.module,state:"
msgid "To be upgraded"
-msgstr "Pendiente de ser actualizado"
+msgstr "Para ser actualizado"
msgctxt "selection:ir.module.module.config_wizard.item,state:"
msgid "Done"
@@ -3473,15 +3506,15 @@ msgstr "No instalado"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "To be installed"
-msgstr "Pendiente de instalar"
+msgstr "Para ser instalado"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "To be removed"
-msgstr "Pendiente de ser eliminado"
+msgstr "Para ser eliminado"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "To be upgraded"
-msgstr "Pendiente de ser actualizado"
+msgstr "Para ser actualizado"
msgctxt "selection:ir.module.module.dependency,state:"
msgid "Unknown"
@@ -3489,11 +3522,11 @@ msgstr "Desconocido"
msgctxt "selection:ir.sequence,type:"
msgid "Decimal Timestamp"
-msgstr "Marca de Tiempo Decimal"
+msgstr "Fecha-hora Decimal"
msgctxt "selection:ir.sequence,type:"
msgid "Hexadecimal Timestamp"
-msgstr "Marca de Tiempo Hexadecimal"
+msgstr "Fecha-hora Hexadecimal"
msgctxt "selection:ir.sequence,type:"
msgid "Incremental"
@@ -3501,11 +3534,11 @@ msgstr "Incremental"
msgctxt "selection:ir.sequence.strict,type:"
msgid "Decimal Timestamp"
-msgstr "Marca de Tiempo Decimal"
+msgstr "Fecha-hora Decimal"
msgctxt "selection:ir.sequence.strict,type:"
msgid "Hexadecimal Timestamp"
-msgstr "Marca de Tiempo Hexadecimal"
+msgstr "Fecha-hora Hexadecimal"
msgctxt "selection:ir.sequence.strict,type:"
msgid "Incremental"
@@ -3617,11 +3650,11 @@ msgstr "Abrir una Ventana"
msgctxt "view:ir.action.keyword:"
msgid "Keyword"
-msgstr "Tecla"
+msgstr "Acción de teclado"
msgctxt "view:ir.action.keyword:"
msgid "Keywords"
-msgstr "Teclas"
+msgstr "Acciones de teclado"
msgctxt "view:ir.action.report:"
msgid "General"
@@ -3633,7 +3666,7 @@ msgstr "Informe"
msgctxt "view:ir.action.report:"
msgid "Report xml"
-msgstr "Informe xml"
+msgstr "Informe XML"
msgctxt "view:ir.action.url:"
msgid "General"
@@ -3697,7 +3730,7 @@ msgstr "Formato de Números"
msgctxt "view:ir.model.access:"
msgid "Access controls"
-msgstr "Acceso a controles"
+msgstr "Controles de acceso"
msgctxt "view:ir.model.button:"
msgid "Button"
@@ -3707,6 +3740,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Botones"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr "Datos de Modelo"
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr "Sincronizar"
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Permiso de Campo"
@@ -3733,7 +3774,7 @@ msgstr "Configuración del módulo"
msgctxt "view:ir.module.module.config_wizard.done:"
msgid "The configuration is done."
-msgstr "La configuracion ha terminado."
+msgstr "La configuracion se ha realizado."
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
@@ -3744,12 +3785,12 @@ msgid ""
"You will be able to configure your installation depending on the modules you"
" have installed."
msgstr ""
-"Usted será capaz de configurar la instalación en función de los módulos que "
-"ha instalado."
+"Usted será capaz de 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 "Asistentes de Configuración"
+msgstr "Elementos del Asistente de Configuración"
msgctxt "view:ir.module.module.config_wizard.other:"
msgid "Configuration Wizard Next Step!"
@@ -3765,15 +3806,15 @@ msgstr "Dependencia"
msgctxt "view:ir.module.module.install_upgrade.done:"
msgid "System Upgrade Done"
-msgstr "Actualización del Sistema Finalizada"
+msgstr "Actualización del Sistema 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 se han actualizado / instalado!"
msgctxt "view:ir.module.module.install_upgrade.start:"
msgid "Note that this operation may take a few minutes."
-msgstr "Esta operación puede demorar varios minutos."
+msgstr "Tenga en cuenta que esta operación puede demorar varios minutos."
msgctxt "view:ir.module.module.install_upgrade.start:"
msgid "System Upgrade"
@@ -3836,7 +3877,7 @@ 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 por lo menos una condición es verdadera"
+msgstr "La regla se satisface si al menos una condición es verdadera"
msgctxt "view:ir.rule:"
msgid "Test"
@@ -3860,7 +3901,7 @@ msgstr "Día:"
msgctxt "view:ir.sequence.strict:"
msgid "Legend (for prefix, suffix)"
-msgstr "Leyenda (variables para usar en prefijo y/o sufijo)"
+msgstr "Leyenda (para prefijo y/o sufijo)"
msgctxt "view:ir.sequence.strict:"
msgid "Month:"
@@ -3900,7 +3941,7 @@ msgstr "Incremental"
msgctxt "view:ir.sequence:"
msgid "Legend (Placeholders for prefix, suffix)"
-msgstr "Leyenda (variables para usar en prefijo y/o sufijo)"
+msgstr "Leyenda (variables para utilizar en prefijo y/o sufijo)"
msgctxt "view:ir.sequence:"
msgid "Month:"
@@ -3912,7 +3953,7 @@ msgstr "Secuencias"
msgctxt "view:ir.sequence:"
msgid "Timestamp"
-msgstr "Marca de tiempo"
+msgstr "Fecha-hora"
msgctxt "view:ir.sequence:"
msgid "Year:"
@@ -3924,7 +3965,7 @@ msgstr "Limpiar Traducciones"
msgctxt "view:ir.translation.clean.start:"
msgid "Clean Translations?"
-msgstr "Limpiar Traducciones?"
+msgstr "¿Limpiar las Traducciones?"
msgctxt "view:ir.translation.clean.succeed:"
msgid "Clean Translations"
@@ -3944,19 +3985,19 @@ msgstr "Exportar Traducción"
msgctxt "view:ir.translation.set.start:"
msgid "Set Translations"
-msgstr "Establecer Traducción"
+msgstr "Establecer Traducciones"
msgctxt "view:ir.translation.set.start:"
msgid "Synchronize Translations?"
-msgstr "Sincronizar Traducciones?"
+msgstr "¿Sincronizar las Traducciones?"
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Succeed!"
-msgstr "Realizado Exitosamente!"
+msgstr "¡Realizado Exitosamente!"
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Translations"
-msgstr "Establecer Traducción"
+msgstr "Establecer Traducciones"
msgctxt "view:ir.translation.update.start:"
msgid "Synchronize Translations"
@@ -4016,15 +4057,15 @@ msgstr "Estado de Vista de Arbol"
msgctxt "view:ir.ui.view_tree_state:"
msgid "Views Tree State"
-msgstr "Estado de Vista de Arbol"
+msgstr "Estado de la Vista de Arbol"
msgctxt "view:ir.ui.view_tree_width:"
msgid "View Tree Width"
-msgstr "Ancho de Columna"
+msgstr "Ancho de la Vista de Arbol"
msgctxt "view:ir.ui.view_tree_width:"
msgid "Views Tree Width"
-msgstr "Anchos de Columna"
+msgstr "Ancho de la Vista de Arbol"
msgctxt "wizard_button:ir.model.print_model_graph,start,end:"
msgid "Cancel"
@@ -4064,7 +4105,7 @@ msgstr "Cancelar"
msgctxt "wizard_button:ir.module.module.install_upgrade,start,upgrade:"
msgid "Start Upgrade"
-msgstr "Iniciar Mejora"
+msgstr "Iniciar Actualización"
msgctxt "wizard_button:ir.translation.clean,start,clean:"
msgid "Clean"
diff --git a/trytond/ir/locale/es_ES.po b/trytond/ir/locale/es_ES.po
index bdb021d..987d042 100644
--- a/trytond/ir/locale/es_ES.po
+++ b/trytond/ir/locale/es_ES.po
@@ -25,7 +25,7 @@ msgstr "No está autorizado para eliminar este registro."
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"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."
@@ -43,8 +43,8 @@ msgid ""
"Could not delete the records because they are used on field \"%(field)s\" of"
" \"%(model)s\"."
msgstr ""
-"No se pueden eliminar los registros porqué se utilizan en el campo "
-"\"%(field)s\" del \"%(model)s\". "
+"No se pueden eliminar los registros porque se utilizan en el campo "
+"\"%(field)s\" de \"%(model)s\". "
msgctxt "error:foreign_model_missing:"
msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
@@ -291,11 +291,11 @@ msgid ""
" was configured as ancestor of itself."
msgstr ""
"Error de recursividad: El registro \"%(rec_name)s\" con el padre "
-"\"%(parent_rec_name)s\" se configuró como padre de si mismo."
+"\"%(parent_rec_name)s\" se ha configurado como padre de si mismo."
msgctxt "error:reference_syntax_error:"
msgid "Syntax error for reference %r in %s"
-msgstr "Error de sintaxis para la 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"
@@ -363,7 +363,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 el 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."
@@ -585,6 +585,10 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Acción"
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Activo"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr "Fecha creación"
@@ -891,7 +895,7 @@ msgstr "Ventana"
msgctxt "field:ir.action.wizard,wiz_name:"
msgid "Wizard name"
-msgstr "Nombre asistente"
+msgstr "Nombre del asistente"
msgctxt "field:ir.action.wizard,write_date:"
msgid "Write Date"
@@ -1263,7 +1267,7 @@ msgstr "Información"
msgctxt "field:ir.model,model:"
msgid "Model Name"
-msgstr "Nombre modelo"
+msgstr "Nombre del modelo"
msgctxt "field:ir.model,module:"
msgid "Module"
@@ -1381,14 +1385,6 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr "Usuario creación"
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Fecha de inicio"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Fecha de actualización"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "ID del recurso"
@@ -1397,6 +1393,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr "Identificador en el sistema de archivos"
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr "Valores en sistema de ficheros"
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr "ID"
@@ -1413,6 +1413,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "No actualizar"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr "Sin sincronizar"
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Nombre"
@@ -2103,7 +2107,7 @@ msgstr "Módulo"
msgctxt "field:ir.translation,name:"
msgid "Field Name"
-msgstr "Nombre campo"
+msgstr "Nombre del campo"
msgctxt "field:ir.translation,overriding_module:"
msgid "Overriding Module"
@@ -2337,6 +2341,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Acción"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr "Acciones de teclado"
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Activo"
@@ -2455,7 +2463,7 @@ msgstr "Dominio"
msgctxt "field:ir.ui.view,field_childs:"
msgid "Children Field"
-msgstr "Campo de los hijos"
+msgstr "Campo hijos"
msgctxt "field:ir.ui.view,id:"
msgid "ID"
@@ -2543,7 +2551,7 @@ msgstr "Usuario modificación"
msgctxt "field:ir.ui.view_tree_state,child_name:"
msgid "Child Name"
-msgstr "Nombre hijo"
+msgstr "Nombre del hijo"
msgctxt "field:ir.ui.view_tree_state,create_date:"
msgid "Create Date"
@@ -2821,6 +2829,10 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Botones"
+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 "Permiso de los campos"
@@ -2933,6 +2945,16 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr "Dominio acción de ventana"
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr "Todos"
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr "Sin sincronizar"
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Acción vista de ventana"
@@ -3005,6 +3027,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Alemán"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr "Español (Ecuador)"
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Inglés"
@@ -3051,7 +3077,7 @@ msgstr "Botón modelo"
msgctxt "model:ir.model.data,name:"
msgid "Model data"
-msgstr "Datos modelo"
+msgstr "Datos del modelo"
msgctxt "model:ir.model.field,name:"
msgid "Model field"
@@ -3249,6 +3275,10 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Botones"
+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 campos"
@@ -3705,6 +3735,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Botones"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr "Datos del modelo"
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr "Sincroniza"
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Permiso del campo"
diff --git a/trytond/ir/locale/fr_FR.po b/trytond/ir/locale/fr_FR.po
index 8cc677d..44e6984 100644
--- a/trytond/ir/locale/fr_FR.po
+++ b/trytond/ir/locale/fr_FR.po
@@ -25,9 +25,9 @@ msgstr "Vous n'êtes pas autorisé à supprimer cet enregistrement"
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
-"Le nombre de décimales \"%(digits)s\" du champ \"%(field)s\" dépasse sa "
+"Le nombre de décimales « %(digits)s » du champ « %(field)s » dépasse sa "
"limite."
msgctxt "error:domain_validation_record:"
@@ -35,7 +35,7 @@ msgid ""
"The value of the field \"%(field)s\" on \"%(model)s\" is not valid according"
" to its domain."
msgstr ""
-"La valeur du champ \"%(field)s\" sur \"%(model)s\" n'est pas valide selon "
+"La valeur du champ « %(field)s » sur « %(model)s » n'est pas valide selon "
"son domaine."
msgctxt "error:foreign_model_exist:"
@@ -44,42 +44,44 @@ msgid ""
" \"%(model)s\"."
msgstr ""
"Impossible de supprimer les enregistrements car ils sont utilisés dans le "
-"champ \"%(field)s\" de \"%(model)s\"."
+"champ « %(field)s » de « %(model)s »."
msgctxt "error:foreign_model_missing:"
msgid "The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" doesn't exist."
-msgstr "La valeur \"%(value)s\" du champ \"%(field)s\" sur \"%(model)s\" n'existe pas."
+msgstr ""
+"La valeur « %(value)s » du champ « %(field)s » sur « %(model)s » n'existe "
+"pas."
msgctxt "error:ir.action.act_window:"
msgid "Invalid context \"%(context)s\" on action \"%(action)s\"."
-msgstr "Le contexte \"%(context)s\" de l'action \"%(action)s\" est invalide."
+msgstr "Le contexte « %(context)s » de l'action « %(action)s » est invalide."
msgctxt "error:ir.action.act_window:"
msgid "Invalid domain or search criteria \"%(domain)s\" on action \"%(action)s\"."
msgstr ""
-"Le critère de recherche ou le domaine \"%(domain)s\" de l'action "
-"\"%(action)s\" est invalide."
+"Le critère de recherche ou le domaine « %(domain)s » de l'action « "
+"%(action)s » est invalide."
msgctxt "error:ir.action.act_window:"
msgid "Invalid view \"%(view)s\" for action \"%(action)s\"."
-msgstr "La vue \"%(view)s\" de l'action \"%(action)s\" est invalide."
+msgstr "La vue « %(view)s » de l'action « %(action)s » est invalide."
msgctxt "error:ir.action.keyword:"
msgid "Wrong wizard model in keyword action \"%s\"."
-msgstr "Modèle d'assistant incorrect pour le mot-clé d'action \"%s\"."
+msgstr "Modèle d'assistant incorrect pour le mot-clé d'action « %s »."
msgctxt "error:ir.action.report:"
msgid "Invalid email definition on report \"%s\"."
-msgstr "Définition de mail incorrecte sur le rapport \"%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 !"
+msgstr "Le nom interne doit être unique par module !"
msgctxt "error:ir.attachment:"
msgid "The names of attachments must be unique by resource!"
msgstr ""
-"Le nom des pièces jointes doivent être unique pour une même ressource !"
+"Le nom des pièces jointes doivent être unique pour une même ressource !"
msgctxt "error:ir.cron:"
msgid "Scheduled action failed"
@@ -93,7 +95,7 @@ msgid ""
"\n"
"%s\n"
msgstr ""
-"L'action suivante n'a pas pu s'exécuter correctement: \"%s\"\n"
+"L'action suivante n'a pas pu s'exécuter correctement: « %s »\n"
"%s\n"
" Retraçage:\n"
"\n"
@@ -105,11 +107,13 @@ msgstr "Le langage par défaut ne peut pas être supprimé"
msgctxt "error:ir.lang:"
msgid "Invalid date format \"%(format)s\" on \"%(language)s\" language."
-msgstr "Le format de date \"%(format)s\" du langage \"%(language)s\" est invalide."
+msgstr ""
+"Le format de date « %(format)s » du langage « %(language)s » est invalide."
msgctxt "error:ir.lang:"
msgid "Invalid grouping \"%(grouping)s\" on \"%(language)s\" language."
-msgstr "Le groupement \"%(grouping)s\" de la langye \"%(language)s\" est invalide."
+msgstr ""
+"Le groupement « %(grouping)s » de la langye « %(language)s » est invalide."
msgctxt "error:ir.lang:"
msgid "The default language must be translatable."
@@ -119,23 +123,23 @@ msgctxt "error:ir.lang:"
msgid "decimal_point and thousands_sep must be different!"
msgstr ""
"Le séparateur de décimale et le séparateur des centaines doivent être "
-"différents !"
+"différents !"
msgctxt "error:ir.model.access:"
msgid "You can not create this kind of document! (%s)"
-msgstr "Vous ne pouvez pas créer ce type de document ! (%s)"
+msgstr "Vous ne pouvez pas créer ce type de document ! (%s)"
msgctxt "error:ir.model.access:"
msgid "You can not delete this document! (%s)"
-msgstr "Vous ne pouvez pas supprimer ce document ! (%s)"
+msgstr "Vous ne pouvez pas supprimer ce document ! (%s)"
msgctxt "error:ir.model.access:"
msgid "You can not read this document! (%s)"
-msgstr "Vous ne pouvez pas lire ce document ! (%s)"
+msgstr "Vous ne pouvez pas lire ce document ! (%s)"
msgctxt "error:ir.model.access:"
msgid "You can not write in this document! (%s)"
-msgstr "Vous ne pouvez pas écrire dans ce document ! (%s)"
+msgstr "Vous ne pouvez pas écrire dans ce document ! (%s)"
msgctxt "error:ir.model.button:"
msgid "The button name in model must be unique!"
@@ -143,45 +147,45 @@ msgstr "Le nom du bouton dans le modèle doit être unique"
msgctxt "error:ir.model.data:"
msgid "The triple (fs_id, module, model) must be unique!"
-msgstr "Le triplet (fs_id, module, model) doit être unique !"
+msgstr "Le triplet (fs_id, module, model) doit être unique !"
msgctxt "error:ir.model.field.access:"
msgid "You can not read the field! (%s.%s)"
-msgstr "Vous ne pouvez lire le champs (%s.%s) !"
+msgstr "Vous ne pouvez lire le champs ! (%s.%s)"
msgctxt "error:ir.model.field.access:"
msgid "You can not write on the field! (%s.%s)"
-msgstr "Vous ne pouvez écrire sur le champs (%s.%s) !"
+msgstr "Vous ne pouvez écrire sur le champs ! (%s.%s)"
msgctxt "error:ir.model.field:"
msgid "Model Field name \"%s\" is not a valid python identifier."
-msgstr "Le nom de champ modèle \"%s\" n'est pas un identifiant Python valide."
+msgstr "Le nom de champ modèle « %s » n'est pas un identifiant Python valide."
msgctxt "error:ir.model.field:"
msgid "The field name in model must be unique!"
-msgstr "Le nom du champ doit être unique sur le modèle !"
+msgstr "Le nom du champ doit être unique sur le modèle !"
msgctxt "error:ir.model:"
msgid "Module name \"%s\" is not a valid python identifier."
-msgstr "Le nom de module \"%s\" n'est pas un identifiant Python valide."
+msgstr "Le nom de module « %s » n'est pas un identifiant Python valide."
msgctxt "error:ir.model:"
msgid "The model must be unique!"
-msgstr "Le modèle doit être unique !"
+msgstr "Le modèle doit être unique !"
msgctxt "error:ir.module.module.dependency:"
msgid "Dependency must be unique by module!"
-msgstr "Une dépendance doit être unique par module !"
+msgstr "Une dépendance doit être unique par module !"
msgctxt "error:ir.module.module:"
msgid "Missing dependencies %s for module \"%s\""
-msgstr "Dépendences %s manquante pour le module \"%s\""
+msgstr "Dépendences %s manquante pour le module « %s »"
msgctxt "error:ir.module.module:"
msgid "The modules you are trying to uninstall depends on installed modules:"
msgstr ""
"Les modules que vous essayez de dés-installer dépendent de modules installés"
-" :"
+" :"
msgctxt "error:ir.module.module:"
msgid "The name of the module must be unique!"
@@ -194,25 +198,27 @@ msgstr ""
msgctxt "error:ir.rule.group:"
msgid "Global and Default are mutually exclusive!"
-msgstr "Global et Défaut sont exclusifs !"
+msgstr "Global et Défaut sont exclusifs !"
msgctxt "error:ir.rule:"
msgid "Invalid domain in rule \"%s\"."
-msgstr "Domaine invalide sur la règle \"%s\"."
+msgstr "Domaine invalide sur la règle « %s »."
msgctxt "error:ir.sequence.strict:"
msgid "Invalid prefix \"%(prefix)s\" on sequence \"%(sequence)s\"."
-msgstr "Le préfixe \"%(prefix)s\" de la séquence \"%(sequence)s\" est invalide"
+msgstr ""
+"Le préfixe « %(prefix)s » de la séquence « %(sequence)s » est invalide"
msgctxt "error:ir.sequence.strict:"
msgid "Invalid suffix \"%(suffix)s\" on sequence \"%(sequence)s\"."
-msgstr "Le suffixe \"%(suffix)s\" de la séquence \"%(sequence)s\" est invalide"
+msgstr ""
+"Le suffixe « %(suffix)s » de la séquence « %(sequence)s » est invalide"
msgctxt "error:ir.sequence.strict:"
msgid "Last Timestamp cannot be in the future on sequence \"%s\"."
msgstr ""
-"La dernière estampille ne peut pas être dans le futur pour la séquence "
-"\"%s\""
+"La dernière estampille ne peut pas être dans le futur pour la séquence « %s "
+"»."
msgctxt "error:ir.sequence.strict:"
msgid "Missing sequence."
@@ -224,17 +230,19 @@ msgstr "L'estampille doit être plus grande que 0"
msgctxt "error:ir.sequence:"
msgid "Invalid prefix \"%(prefix)s\" on sequence \"%(sequence)s\"."
-msgstr "Le préfixe \"%(prefix)s\" de la séquence \"%(sequence)s\" est invalide"
+msgstr ""
+"Le préfixe « %(prefix)s » de la séquence « %(sequence)s » est invalide."
msgctxt "error:ir.sequence:"
msgid "Invalid suffix \"%(suffix)s\" on sequence \"%(sequence)s\"."
-msgstr "Le suffixe \"%(suffix)s\" de la séquence \"%(sequence)s\" est invalide"
+msgstr ""
+"Le suffixe « %(suffix)s » de la séquence « %(sequence)s » est invalide."
msgctxt "error:ir.sequence:"
msgid "Last Timestamp cannot be in the future on sequence \"%s\"."
msgstr ""
-"La dernière estampille ne peut pas être dans le futur pour la séquence "
-"\"%s\""
+"La dernière estampille ne peut pas être dans le futur pour la séquence « %s "
+"»."
msgctxt "error:ir.sequence:"
msgid "Missing sequence."
@@ -258,30 +266,30 @@ msgstr ""
msgctxt "error:ir.trigger:"
msgid "\"On Time\" and others are mutually exclusive!"
-msgstr "\"À temps\" exclus les autres type de déclencheurs !"
+msgstr "« À temps » exclus les autres type de déclencheurs !"
msgctxt "error:ir.trigger:"
msgid ""
"Condition \"%(condition)s\" is not a valid python expression on trigger "
"\"%(trigger)s\"."
msgstr ""
-"la condition \"%(condition)s\" du déclencheur \"%(trigger)s\" n'est pas une "
+"la condition « %(condition)s » du déclencheur « %(trigger)s » n'est pas une "
"expression python valide."
msgctxt "error:ir.ui.menu:"
msgid "\"%s\" is not a valid menu name because it is not allowed to contain \" / \"."
-msgstr "\"%s\" n'est pas un nom de menu valide car il contient \"/\"."
+msgstr "« %s » n'est pas un nom de menu valide car il contient « / »."
msgctxt "error:ir.ui.view:"
msgid "Invalid XML for view \"%s\"."
-msgstr "XML invalide sur la vue \"%s\"."
+msgstr "XML invalide sur la vue « %s »."
msgctxt "error:read_error:"
msgid ""
"You try to read records that don't exist anymore!\n"
"(Document type: %s)"
msgstr ""
-"Vous essayez de lire un enregistrement qui n'existe plus !\n"
+"Vous essayez de lire un enregistrement qui n'existe plus !\n"
"(Type du document: %s)"
msgctxt "error:read_error:"
@@ -289,7 +297,7 @@ msgid ""
"You try to read records that don't exist anymore.\n"
"(Document type: %s)"
msgstr ""
-"Vous essayez de lire des enregistrements qui n'existent plus !\n"
+"Vous essayez de lire des enregistrements qui n'existent plus.\n"
"(Type du document: %s)"
msgctxt "error:recursion_error:"
@@ -297,8 +305,8 @@ msgid ""
"Recursion error: Record \"%(rec_name)s\" with parent \"%(parent_rec_name)s\""
" was configured as ancestor of itself."
msgstr ""
-"Erreur de récursion: L'enregistrement \"%(rec_name)s\" avec le parent "
-"\"%(parent_rec_name)s\" a été configuré comme ancêtre de lui même."
+"Erreur de récursion: L'enregistrement « %(rec_name)s » avec le parent « "
+"%(parent_rec_name)s » a été configuré comme ancêtre de lui même."
msgctxt "error:reference_syntax_error:"
msgid "Syntax error for reference %r in %s"
@@ -306,50 +314,54 @@ msgstr "Erreur de syntaxe pour la référence %r de %r"
msgctxt "error:relation_not_found:"
msgid "Relation not found: %r in %s"
-msgstr "Relation non trouvée : %r dans %r"
+msgstr "Relation non trouvée : %r dans %r"
msgctxt "error:required_field:"
msgid "The field \"%(field)s\" on \"%(model)s\" is required."
-msgstr "Le champ \"%(field)s\" sur \"%(model)s\" est requis."
+msgstr "Le champ « %(field)s » sur « %(model)s » est requis."
msgctxt "error:required_validation_record:"
msgid "The field \"%(field)s\" on \"%(model)s\" is required."
-msgstr "Le champ \"%(field)s\" sur \"%(model)s\" est requis."
+msgstr "Le champ « %(field)s » sur « %(model)s » est requis."
msgctxt "error:search_function_missing:"
msgid "Missing search function on field \"%s\"."
-msgstr "Fonction de recherche absente pour le champ \"%s\"."
+msgstr "Fonction de recherche absente pour le champ « %s »."
msgctxt "error:selection_validation_record:"
msgid ""
"The value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not in "
"the selection."
msgstr ""
-"La valeur \"%(value)s\" du champ \"%(field)s\" sur \"%(model)s\" n'est pas "
+"La valeur « %(value)s » du champ « %(field)s » sur « %(model)s » n'est pas "
"dans la sélection."
msgctxt "error:selection_value_notfound:"
msgid "Value not in the selection for field \"%s\"."
-msgstr "Valeur absente de la sélection pour le champ \"%s\"."
+msgstr "Valeur absente de la sélection pour le champ « %s »."
msgctxt "error:size_validation_record:"
msgid "The size \"%(size)s\" of the field \"%(field)s\" on \"%(model)s\" is too long."
-msgstr "La taille \"%(size)s\" du champ \"%(field)s\" sur \"%(model)s\" est trop longue."
+msgstr ""
+"La taille « %(size)s » du champ « %(field)s » sur « %(model)s » est trop "
+"longue."
msgctxt "error:time_format_validation_record:"
msgid "The time value \"%(value)s\" of field \"%(field)s\" on \"%(model)s\" is not valid."
-msgstr "L'heure \"%(value)s\" du champ \"%(field)s\" sur \"%(model)s\" n'est pas valide."
+msgstr ""
+"L'heure « %(value)s » du champ « %(field)s » sur « %(model)s » n'est pas "
+"valide."
msgctxt "error:too_many_relations_found:"
msgid "Too many relations found: %r in %s"
-msgstr "Trop de relations pour : %r dans %s"
+msgstr "Trop de relations pour : %r dans %s"
msgctxt "error:write_error:"
msgid ""
"You try to write on records that don't exist anymore!\n"
"(Document type: %s)"
msgstr ""
-"Vous essayez d'écrire sur un enregistrement qui n'existe plus !\n"
+"Vous essayez d'écrire sur un enregistrement qui n'existe plus !\n"
"(Type du document: %s)"
msgctxt "error:write_error:"
@@ -588,6 +600,10 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Action"
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Actif"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr "Date de création"
@@ -682,7 +698,7 @@ msgstr "Imprimer directement"
msgctxt "field:ir.action.report,email:"
msgid "Email"
-msgstr "E-mail"
+msgstr "Email"
msgctxt "field:ir.action.report,extension:"
msgid "Extension"
@@ -850,7 +866,7 @@ msgstr "Créé par"
msgctxt "field:ir.action.wizard,email:"
msgid "Email"
-msgstr "E-mail"
+msgstr "Email"
msgctxt "field:ir.action.wizard,groups:"
msgid "Groups"
@@ -1384,14 +1400,6 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr "Créé par"
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Date d'initialisation"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Date de mis à jour"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "ID de la ressource"
@@ -1400,6 +1408,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr "Identifiant sur le système de fichier"
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr "Valeurs sur le système de fichier"
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr "ID"
@@ -1416,6 +1428,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "Pas de mise à jour"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr "Désynchronisé"
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Nom"
@@ -2340,6 +2356,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Action"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr "Mots-clés d'action"
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Actif"
@@ -2649,7 +2669,7 @@ msgid ""
"Python dictonary where keys define \"to\" \"cc\" \"subject\"\n"
"Example: {'to': 'test at example.com', 'cc': 'user at example.com'}"
msgstr ""
-"Dictionnaire Python définissant \"to\", \"cc\" et \"subject\".\n"
+"Dictionnaire Python définissant « to », « cc » et « subject ».\n"
" Exemple: {'to': 'test at example.com', 'cc': 'user at example.com'}"
msgctxt "help:ir.action.report,extension:"
@@ -2714,7 +2734,7 @@ msgstr ""
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"
+msgstr "Le domaine est évalué avec « user » comme utilisateur courant"
msgctxt "help:ir.rule.group,default_p:"
msgid "Add this rule to all users by default"
@@ -2736,7 +2756,7 @@ msgid ""
"A Python statement evaluated with record represented by \"self\"\n"
"It triggers the action if true."
msgstr ""
-"Une expression Python ou l'enregistrement est représenté par \"self\".\n"
+"Une expression Python ou l'enregistrement est représenté par « self ».\n"
"Déclenche une action si vrai."
msgctxt "help:ir.trigger,limit_number:"
@@ -2744,7 +2764,7 @@ msgid ""
"Limit the number of call to \"Action Function\" by records.\n"
"0 for no limit."
msgstr ""
-"Limite le nombre d'appel aux \"Fonction action\" par enregistrement.\n"
+"Limite le nombre d'appel aux « Fonction action » par enregistrement.\n"
"0 signifie pas de limite."
msgctxt "help:ir.trigger,minimum_delay:"
@@ -2752,7 +2772,7 @@ msgid ""
"Set a minimum delay in minutes between call to \"Action Function\" for the same record.\n"
"0 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"
+"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."
msgctxt "help:ir.ui.view_search,domain:"
@@ -2823,6 +2843,10 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Boutons"
+msgctxt "model:ir.action,name:act_model_data_form"
+msgid "Data"
+msgstr "Données"
+
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
msgstr "Droits d'accès"
@@ -2935,6 +2959,16 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr "Action act window domain"
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr "Toutes"
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr "Désynchronisé"
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Action ouvrir fenêtre vue"
@@ -3007,6 +3041,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Allemand"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr "Espagnol (Équateur)"
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Anglais"
@@ -3251,6 +3289,10 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Boutons"
+msgctxt "model:ir.ui.menu,name:menu_model_data_form"
+msgid "Data"
+msgstr "Données"
+
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
msgstr "Droits d'accès au champs"
@@ -3707,6 +3749,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Boutons"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr "Données de modèle"
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr "Synchroniser"
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Droits d'accès au champs"
@@ -3737,7 +3787,7 @@ msgstr "La configuration est faite."
msgctxt "view:ir.module.module.config_wizard.first:"
msgid "Welcome to the module configuration wizard!"
-msgstr "Bienvenu dans l'assistant de configuration de module !"
+msgstr "Bienvenu dans l'assistant de configuration de module !"
msgctxt "view:ir.module.module.config_wizard.first:"
msgid ""
@@ -3753,7 +3803,7 @@ msgstr "Élements de l'assistant de configuration"
msgctxt "view:ir.module.module.config_wizard.other:"
msgid "Configuration Wizard Next Step!"
-msgstr "Prochaine étape du wizard de configuration !"
+msgstr "Prochaine étape du wizard de configuration !"
msgctxt "view:ir.module.module.dependency:"
msgid "Dependencies"
@@ -3769,7 +3819,7 @@ msgstr "Mise à jour du système terminée"
msgctxt "view:ir.module.module.install_upgrade.done:"
msgid "The modules have been upgraded / installed !"
-msgstr "Les modules ont été mis à jour / installés !"
+msgstr "Les modules ont été mis à jour / installés !"
msgctxt "view:ir.module.module.install_upgrade.start:"
msgid "Note that this operation may take a few minutes."
@@ -3864,7 +3914,7 @@ msgstr "Légende (préfixe et suffixe)"
msgctxt "view:ir.sequence.strict:"
msgid "Month:"
-msgstr "Mois :"
+msgstr "Mois :"
msgctxt "view:ir.sequence.strict:"
msgid "Sequences Strict"
@@ -3872,7 +3922,7 @@ msgstr "Séquence stricte"
msgctxt "view:ir.sequence.strict:"
msgid "Year:"
-msgstr "Année :"
+msgstr "Année :"
msgctxt "view:ir.sequence.type:"
msgid "Sequence Type"
@@ -3904,7 +3954,7 @@ msgstr "Légende (charactère pour le préfixe, le suffixe)"
msgctxt "view:ir.sequence:"
msgid "Month:"
-msgstr "Mois :"
+msgstr "Mois :"
msgctxt "view:ir.sequence:"
msgid "Sequences"
@@ -3916,7 +3966,7 @@ msgstr "Estampille"
msgctxt "view:ir.sequence:"
msgid "Year:"
-msgstr "Année :"
+msgstr "Année :"
msgctxt "view:ir.translation.clean.start:"
msgid "Clean Translations"
@@ -3924,7 +3974,7 @@ msgstr "Nettoyer les traductions"
msgctxt "view:ir.translation.clean.start:"
msgid "Clean Translations?"
-msgstr "Nettoyer les traductions ?"
+msgstr "Nettoyer les traductions ?"
msgctxt "view:ir.translation.clean.succeed:"
msgid "Clean Translations"
@@ -3932,7 +3982,7 @@ msgstr "Nettoyer les traductions"
msgctxt "view:ir.translation.clean.succeed:"
msgid "Clean Translations Succeed!"
-msgstr "Nettoyage des traductions réussi !"
+msgstr "Nettoyage des traductions réussi !"
msgctxt "view:ir.translation.export.result:"
msgid "Export Translation"
@@ -3948,11 +3998,11 @@ msgstr "Définir les traductions"
msgctxt "view:ir.translation.set.start:"
msgid "Synchronize Translations?"
-msgstr "Synchroniser les traductions ?"
+msgstr "Synchroniser les traductions ?"
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Succeed!"
-msgstr "Mise à jour réussie !"
+msgstr "Mise à jour réussie !"
msgctxt "view:ir.translation.set.succeed:"
msgid "Set Translations"
diff --git a/trytond/ir/locale/nl_NL.po b/trytond/ir/locale/nl_NL.po
index daa70aa..a34a383 100644
--- a/trytond/ir/locale/nl_NL.po
+++ b/trytond/ir/locale/nl_NL.po
@@ -23,7 +23,7 @@ msgstr "Het is u niet toegestaan dit item te verwijderen."
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
msgctxt "error:domain_validation_record:"
@@ -566,6 +566,11 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Actie"
+#, fuzzy
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Actief"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr ""
@@ -1392,14 +1397,6 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr ""
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Datum aanmaken"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Datum bijgewerkt"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "Middel ID"
@@ -1408,6 +1405,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr "Kenmerk voor bestandssysteem"
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr ""
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr ""
@@ -1424,6 +1425,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "Niet bij te werken"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr ""
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Naam"
@@ -2371,6 +2376,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Actie"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr ""
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Actief"
@@ -2861,6 +2870,10 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr ""
+msgctxt "model:ir.action,name:act_model_data_form"
+msgid "Data"
+msgstr ""
+
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
msgstr ""
@@ -2973,6 +2986,16 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr ""
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr ""
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr ""
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Actie uitvoerend schermaanzicht"
@@ -3046,6 +3069,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Duits"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr ""
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Engels"
@@ -3296,6 +3323,10 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr ""
+msgctxt "model:ir.ui.menu,name:menu_model_data_form"
+msgid "Data"
+msgstr ""
+
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
msgstr ""
@@ -3758,6 +3789,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr ""
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr ""
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr ""
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr ""
diff --git a/trytond/ir/locale/ru_RU.po b/trytond/ir/locale/ru_RU.po
index 6af0951..687f877 100644
--- a/trytond/ir/locale/ru_RU.po
+++ b/trytond/ir/locale/ru_RU.po
@@ -25,7 +25,7 @@ msgstr "Вы не можете удалить эту запись."
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
msgctxt "error:domain_validation_record:"
@@ -562,6 +562,11 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Действие"
+#, fuzzy
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Действующий"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
msgstr "Дата создания"
@@ -1358,14 +1363,6 @@ msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
msgstr "Создано пользователем"
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Дата уставки"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Дата обновления"
-
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
msgstr "Ресурс ID"
@@ -1374,6 +1371,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr "Идентификатор в файловой системе"
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr ""
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr "ID"
@@ -1390,6 +1391,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "Нет обновлений"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr ""
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Наименование"
@@ -2315,6 +2320,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Действие"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr ""
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Действующий"
@@ -2808,6 +2817,11 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Кнопки"
+#, fuzzy
+msgctxt "model:ir.action,name:act_model_data_form"
+msgid "Data"
+msgstr "Данные"
+
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
msgstr "Доступ к полям"
@@ -2920,6 +2934,17 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr "Действие окна домен"
+#, fuzzy
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr "Все"
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr ""
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Действие окна вида"
@@ -2992,6 +3017,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Немецкий"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr ""
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Английский"
@@ -3236,6 +3265,11 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Кнопки"
+#, fuzzy
+msgctxt "model:ir.ui.menu,name:menu_model_data_form"
+msgid "Data"
+msgstr "Данные"
+
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
msgstr "Доступ к полям"
@@ -3693,6 +3727,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Кнопки"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr ""
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr ""
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Доступ к полю"
diff --git a/trytond/ir/locale/sl_SI.po b/trytond/ir/locale/sl_SI.po
index 59694de..8b73500 100644
--- a/trytond/ir/locale/sl_SI.po
+++ b/trytond/ir/locale/sl_SI.po
@@ -25,7 +25,7 @@ msgstr "Nimate dovoljenja za brisanje tega zapisa"
msgctxt "error:digits_validation_record:"
msgid ""
"The number of digits \"%(digits)s\" of field \"%(field)s\" on \"%(value)s\" "
-"exceeds it's limit."
+"exceeds its limit."
msgstr ""
"Število decimalk \"%(digits)s\" polja \"%(field)s\" pri \"%(model)s\" je "
"preseženo."
@@ -120,7 +120,7 @@ msgstr "decimal_point in thousands_sep morata biti različna."
msgctxt "error:ir.model.access:"
msgid "You can not create this kind of document! (%s)"
-msgstr "Te vrste dokumenta ni možno ustvariti. (%s)"
+msgstr "Te vrste dokumenta ni možno izdelati. (%s)"
msgctxt "error:ir.model.access:"
msgid "You can not delete this document! (%s)"
@@ -369,11 +369,11 @@ msgstr "Aktivno"
msgctxt "field:ir.action,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.action,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.action,groups:"
msgid "Groups"
@@ -437,11 +437,11 @@ msgstr "Vrednost konteksta"
msgctxt "field:ir.action.act_window,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.action.act_window,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.action.act_window,domain:"
msgid "Domain Value"
@@ -541,11 +541,11 @@ msgstr "Aktivno"
msgctxt "field:ir.action.act_window.domain,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.action.act_window.domain,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.action.act_window.domain,domain:"
msgid "Domain"
@@ -579,13 +579,17 @@ msgctxt "field:ir.action.act_window.view,act_window:"
msgid "Action"
msgstr "Ukrep"
+msgctxt "field:ir.action.act_window.view,active:"
+msgid "Active"
+msgstr "Aktivno"
+
msgctxt "field:ir.action.act_window.view,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.action.act_window.view,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.action.act_window.view,id:"
msgid "ID"
@@ -617,11 +621,11 @@ msgstr "Ukrep"
msgctxt "field:ir.action.keyword,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.action.keyword,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.action.keyword,groups:"
msgid "Groups"
@@ -661,11 +665,11 @@ msgstr "Aktivno"
msgctxt "field:ir.action.report,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.action.report,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.action.report,direct_print:"
msgid "Direct Print"
@@ -773,11 +777,11 @@ msgstr "Aktivno"
msgctxt "field:ir.action.url,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.action.url,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.action.url,groups:"
msgid "Groups"
@@ -833,11 +837,11 @@ msgstr "Aktivno"
msgctxt "field:ir.action.wizard,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.action.wizard,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.action.wizard,email:"
msgid "Email"
@@ -901,11 +905,11 @@ msgstr "Prekrivanje"
msgctxt "field:ir.attachment,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.attachment,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.attachment,data:"
msgid "Data"
@@ -969,11 +973,11 @@ msgstr "Zapisal"
msgctxt "field:ir.cache,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.cache,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.cache,id:"
msgid "ID"
@@ -1001,11 +1005,11 @@ msgstr "Zapisal"
msgctxt "field:ir.configuration,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.configuration,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.configuration,id:"
msgid "ID"
@@ -1037,11 +1041,11 @@ msgstr "Argumenti"
msgctxt "field:ir.cron,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.cron,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.cron,function:"
msgid "Function"
@@ -1105,11 +1109,11 @@ msgstr "ID"
msgctxt "field:ir.export,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.export,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.export,export_fields:"
msgid "Fields"
@@ -1141,11 +1145,11 @@ msgstr "Zapisal"
msgctxt "field:ir.export.line,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.export.line,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.export.line,export:"
msgid "Export"
@@ -1181,11 +1185,11 @@ msgstr "Šifra"
msgctxt "field:ir.lang,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.lang,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.lang,date:"
msgid "Date"
@@ -1233,11 +1237,11 @@ msgstr "Zapisal"
msgctxt "field:ir.model,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.model,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.model,fields:"
msgid "Fields"
@@ -1281,11 +1285,11 @@ msgstr "Zapisal"
msgctxt "field:ir.model.access,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.model.access,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.model.access,description:"
msgid "Description"
@@ -1305,7 +1309,7 @@ msgstr "Model"
msgctxt "field:ir.model.access,perm_create:"
msgid "Create Access"
-msgstr "Ustvarjanje"
+msgstr "Izdelava"
msgctxt "field:ir.model.access,perm_delete:"
msgid "Delete Access"
@@ -1333,11 +1337,11 @@ msgstr "Zapisal"
msgctxt "field:ir.model.button,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.model.button,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.model.button,groups:"
msgid "Groups"
@@ -1369,19 +1373,11 @@ msgstr "Zapisal"
msgctxt "field:ir.model.data,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.model.data,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
-
-msgctxt "field:ir.model.data,date_init:"
-msgid "Init Date"
-msgstr "Začetni datum"
-
-msgctxt "field:ir.model.data,date_update:"
-msgid "Update Date"
-msgstr "Datum posodobitve"
+msgstr "Izdelal"
msgctxt "field:ir.model.data,db_id:"
msgid "Resource ID"
@@ -1391,6 +1387,10 @@ msgctxt "field:ir.model.data,fs_id:"
msgid "Identifier on File System"
msgstr "Identifikator v datotečnem sistemu"
+msgctxt "field:ir.model.data,fs_values:"
+msgid "Values on File System"
+msgstr "Vrednosti v datotečnem sistemu"
+
msgctxt "field:ir.model.data,id:"
msgid "ID"
msgstr "ID"
@@ -1407,6 +1407,10 @@ msgctxt "field:ir.model.data,noupdate:"
msgid "No Update"
msgstr "Brez posodabljanja"
+msgctxt "field:ir.model.data,out_of_sync:"
+msgid "Out of Sync"
+msgstr "Izven sinhronizacije"
+
msgctxt "field:ir.model.data,rec_name:"
msgid "Name"
msgstr "Ime"
@@ -1425,11 +1429,11 @@ msgstr "Zapisal"
msgctxt "field:ir.model.field,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.model.field,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.model.field,field_description:"
msgid "Field Description"
@@ -1481,11 +1485,11 @@ msgstr "Zapisal"
msgctxt "field:ir.model.field.access,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.model.field.access,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.model.field.access,description:"
msgid "Description"
@@ -1505,7 +1509,7 @@ msgstr "ID"
msgctxt "field:ir.model.field.access,perm_create:"
msgid "Create Access"
-msgstr "Ustvarjanje"
+msgstr "Izdelava"
msgctxt "field:ir.model.field.access,perm_delete:"
msgid "Delete Access"
@@ -1549,11 +1553,11 @@ msgstr "Podmoduli"
msgctxt "field:ir.module.module,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.module.module,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.module.module,dependencies:"
msgid "Dependencies"
@@ -1605,11 +1609,11 @@ msgstr "Ukrep"
msgctxt "field:ir.module.module.config_wizard.item,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.module.module.config_wizard.item,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.module.module.config_wizard.item,id:"
msgid "ID"
@@ -1645,11 +1649,11 @@ msgstr "Odstotek"
msgctxt "field:ir.module.module.dependency,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.module.module.dependency,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.module.module.dependency,id:"
msgid "ID"
@@ -1693,11 +1697,11 @@ msgstr "Moduli za posodobitev"
msgctxt "field:ir.property,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.property,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.property,field:"
msgid "Field"
@@ -1729,11 +1733,11 @@ msgstr "Zapisal"
msgctxt "field:ir.rule,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.rule,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.rule,domain:"
msgid "Domain"
@@ -1761,11 +1765,11 @@ msgstr "Zapisal"
msgctxt "field:ir.rule.group,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.rule.group,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.rule.group,default_p:"
msgid "Default"
@@ -1793,7 +1797,7 @@ msgstr "Naziv"
msgctxt "field:ir.rule.group,perm_create:"
msgid "Create Access"
-msgstr "Ustvarjanje"
+msgstr "Izdelava"
msgctxt "field:ir.rule.group,perm_delete:"
msgid "Delete Access"
@@ -1837,11 +1841,11 @@ msgstr "Šifra"
msgctxt "field:ir.sequence,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.sequence,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.sequence,id:"
msgid "ID"
@@ -1913,11 +1917,11 @@ msgstr "Šifra"
msgctxt "field:ir.sequence.strict,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.sequence.strict,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.sequence.strict,id:"
msgid "ID"
@@ -1985,11 +1989,11 @@ msgstr "Šifra"
msgctxt "field:ir.sequence.type,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.sequence.type,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.sequence.type,id:"
msgid "ID"
@@ -2013,11 +2017,11 @@ msgstr "Zapisal"
msgctxt "field:ir.session,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.session,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.session,id:"
msgid "ID"
@@ -2041,11 +2045,11 @@ msgstr "Zapisal"
msgctxt "field:ir.session.wizard,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.session.wizard,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.session.wizard,data:"
msgid "Data"
@@ -2069,11 +2073,11 @@ msgstr "Zapisal"
msgctxt "field:ir.translation,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.translation,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.translation,fuzzy:"
msgid "Fuzzy"
@@ -2197,11 +2201,11 @@ msgstr "Pogoj"
msgctxt "field:ir.trigger,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.trigger,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.trigger,id:"
msgid "ID"
@@ -2225,7 +2229,7 @@ msgstr "Naziv"
msgctxt "field:ir.trigger,on_create:"
msgid "On Create"
-msgstr "Ob ustvarjanju"
+msgstr "Ob izdelavi"
msgctxt "field:ir.trigger,on_delete:"
msgid "On Delete"
@@ -2253,11 +2257,11 @@ msgstr "Zapisal"
msgctxt "field:ir.trigger.log,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.trigger.log,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.trigger.log,id:"
msgid "ID"
@@ -2285,11 +2289,11 @@ msgstr "Zapisal"
msgctxt "field:ir.ui.icon,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.ui.icon,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.ui.icon,icon:"
msgid "Icon"
@@ -2331,6 +2335,10 @@ msgctxt "field:ir.ui.menu,action:"
msgid "Action"
msgstr "Ukrep"
+msgctxt "field:ir.ui.menu,action_keywords:"
+msgid "Action Keywords"
+msgstr "Ključne besede ukrepov"
+
msgctxt "field:ir.ui.menu,active:"
msgid "Active"
msgstr "Aktivno"
@@ -2345,11 +2353,11 @@ msgstr "Polno ime"
msgctxt "field:ir.ui.menu,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.ui.menu,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.ui.menu,favorite:"
msgid "Favorite"
@@ -2393,11 +2401,11 @@ msgstr "Zapisal"
msgctxt "field:ir.ui.menu.favorite,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.ui.menu.favorite,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.ui.menu.favorite,id:"
msgid "ID"
@@ -2433,11 +2441,11 @@ msgstr "Zgradba"
msgctxt "field:ir.ui.view,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.ui.view,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.ui.view,data:"
msgid "Data"
@@ -2497,11 +2505,11 @@ msgstr "ID"
msgctxt "field:ir.ui.view_search,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.ui.view_search,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.ui.view_search,domain:"
msgid "Domain"
@@ -2541,11 +2549,11 @@ msgstr "Ime poddrevesa"
msgctxt "field:ir.ui.view_tree_state,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.ui.view_tree_state,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.ui.view_tree_state,domain:"
msgid "Domain"
@@ -2585,11 +2593,11 @@ msgstr "Zapisal"
msgctxt "field:ir.ui.view_tree_width,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.ui.view_tree_width,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.ui.view_tree_width,field:"
msgid "Field"
@@ -2813,6 +2821,10 @@ msgctxt "model:ir.action,name:act_model_button_form"
msgid "Buttons"
msgstr "Gumbi"
+msgctxt "model:ir.action,name:act_model_data_form"
+msgid "Data"
+msgstr "Podatki"
+
msgctxt "model:ir.action,name:act_model_field_access_form"
msgid "Fields Access"
msgstr "Dostop do polj"
@@ -2925,6 +2937,16 @@ msgctxt "model:ir.action.act_window.domain,name:"
msgid "Action act window domain"
msgstr "Domena okna za ukrepe"
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_all"
+msgid "All"
+msgstr "Vse"
+
+msgctxt ""
+"model:ir.action.act_window.domain,name:act_model_data_form_domain_out_of_sync"
+msgid "Out of Sync"
+msgstr "Nesinhronizirano"
+
msgctxt "model:ir.action.act_window.view,name:"
msgid "Action act window view"
msgstr "Pogled ukrepa za okna"
@@ -2997,6 +3019,10 @@ msgctxt "model:ir.lang,name:lang_de"
msgid "German"
msgstr "Nemščina"
+msgctxt "model:ir.lang,name:lang_ec"
+msgid "Spanish (Ecuador)"
+msgstr "Španščina (Ekvador)"
+
msgctxt "model:ir.lang,name:lang_en"
msgid "English"
msgstr "Angleščina"
@@ -3241,6 +3267,10 @@ msgctxt "model:ir.ui.menu,name:menu_model_button_form"
msgid "Buttons"
msgstr "Gumbi"
+msgctxt "model:ir.ui.menu,name:menu_model_data_form"
+msgid "Data"
+msgstr "Podatki"
+
msgctxt "model:ir.ui.menu,name:menu_model_field_access_form"
msgid "Fields Access"
msgstr "Dostop do polj"
@@ -3697,6 +3727,14 @@ msgctxt "view:ir.model.button:"
msgid "Buttons"
msgstr "Gumbi"
+msgctxt "view:ir.model.data:"
+msgid "Model Data"
+msgstr "Podatki modela"
+
+msgctxt "view:ir.model.data:"
+msgid "Sync"
+msgstr "Sinhronizacija"
+
msgctxt "view:ir.model.field.access:"
msgid "Field Access"
msgstr "Dostop do polj"
diff --git a/trytond/ir/model.py b/trytond/ir/model.py
index cb63303..53855a1 100644
--- a/trytond/ir/model.py
+++ b/trytond/ir/model.py
@@ -1,11 +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 datetime
import re
import heapq
from sql.aggregate import Max
from sql.conditionals import Case
from collections import defaultdict
+try:
+ import simplejson as json
+except ImportError:
+ import json
from ..model import ModelView, ModelSQL, fields
from ..report import Report
@@ -16,6 +19,7 @@ from ..pool import Pool
from ..pyson import Bool, Eval
from ..rpc import RPC
from .. import backend
+from ..protocols.jsonrpc import JSONDecoder, JSONEncoder
try:
from ..tools.StringMatcher import StringMatcher
except ImportError:
@@ -122,13 +126,12 @@ class Model(ModelSQL, ModelView):
@classmethod
def list_models(cls):
'Return a list of all models names'
- with Transaction().set_user(0):
- models = cls.search([], order=[
- ('module', 'ASC'), # Optimization assumption
- ('model', 'ASC'),
- ('id', 'ASC'),
- ])
- return [m.model for m in models]
+ models = cls.search([], order=[
+ ('module', 'ASC'), # Optimization assumption
+ ('model', 'ASC'),
+ ('id', 'ASC'),
+ ])
+ return [m.model for m in models]
@classmethod
def list_history(cls):
@@ -522,7 +525,9 @@ class ModelAccess(ModelSQL, ModelView):
'Check access for model_name and mode'
assert mode in ['read', 'write', 'create', 'delete'], \
'Invalid access mode for security'
- if Transaction().user == 0:
+ if ((Transaction().user == 0)
+ or (raise_exception
+ and not Transaction().context.get('_check_access'))):
return True
access = cls.get_access([model_name])[model_name][mode]
@@ -600,7 +605,7 @@ class ModelFieldAccess(ModelSQL, ModelView):
perm_create = fields.Boolean('Create Access')
perm_delete = fields.Boolean('Delete Access')
description = fields.Text('Description')
- _get_access_cache = Cache('ir_model_field_access.check')
+ _get_access_cache = Cache('ir_model_field_access.check', context=False)
@classmethod
def __setup__(cls):
@@ -702,7 +707,9 @@ class ModelFieldAccess(ModelSQL, ModelView):
'''
assert mode in ('read', 'write', 'create', 'delete'), \
'Invalid access mode'
- if Transaction().user == 0:
+ if ((Transaction().user == 0)
+ or (raise_exception
+ and not Transaction().context.get('_check_access'))):
if access:
return dict((x, True) for x in fields)
return True
@@ -813,10 +820,11 @@ class ModelData(ModelSQL, ModelView):
db_id = fields.Integer('Resource ID',
help="The id of the record in the database.", select=True,
required=True)
- date_update = fields.DateTime('Update Date')
- date_init = fields.DateTime('Init Date')
values = fields.Text('Values')
+ fs_values = fields.Text('Values on File System')
noupdate = fields.Boolean('No Update')
+ out_of_sync = fields.Function(fields.Boolean('Out of Sync'),
+ 'get_out_of_sync', searcher='search_out_of_sync')
_get_id_cache = Cache('ir_model_data.get_id', context=False)
@classmethod
@@ -826,6 +834,11 @@ class ModelData(ModelSQL, ModelView):
('fs_id_module_model_uniq', 'UNIQUE("fs_id", "module", "model")',
'The triple (fs_id, module, model) must be unique!'),
]
+ cls._buttons.update({
+ 'sync': {
+ 'invisible': ~Eval('out_of_sync'),
+ },
+ })
@classmethod
def __register__(cls, module_name):
@@ -844,13 +857,23 @@ class ModelData(ModelSQL, ModelView):
table.drop_column('inherit', True)
@staticmethod
- def default_date_init():
- return datetime.datetime.now()
-
- @staticmethod
def default_noupdate():
return False
+ def get_out_of_sync(self, name):
+ return self.values != self.fs_values and self.fs_values is not None
+
+ @classmethod
+ def search_out_of_sync(cls, name, clause):
+ table = cls.__table__()
+ name, operator, value = clause
+ Operator = fields.SQL_OPERATORS[operator]
+ query = table.select(table.id,
+ where=Operator(
+ (table.fs_values != table.values) & (table.fs_values != None),
+ value))
+ return [('id', 'in', query)]
+
@classmethod
def write(cls, data, values, *args):
super(ModelData, cls).write(data, values, *args)
@@ -877,6 +900,45 @@ class ModelData(ModelSQL, ModelView):
cls._get_id_cache.set(key, id_)
return id_
+ @classmethod
+ def dump_values(cls, values):
+ return json.dumps(sorted(values.iteritems()), cls=JSONEncoder)
+
+ @classmethod
+ def load_values(cls, values):
+ try:
+ 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, {
+ 'Decimal': Decimal,
+ 'datetime': datetime,
+ })
+
+ @classmethod
+ @ModelView.button
+ def sync(cls, records):
+ pool = Pool()
+ to_write = []
+ for data in records:
+ Model = pool.get(data.model)
+ values = cls.load_values(data.values)
+ fs_values = cls.load_values(data.fs_values)
+ # values could be the same once loaded
+ # if they come from version < 3.2
+ if values != fs_values:
+ record = Model(data.db_id)
+ Model.write([record], fs_values)
+ values.update(fs_values)
+ to_write.extend([[data], {
+ 'values': cls.dump_values(values),
+ }])
+ if to_write:
+ cls.write(*to_write)
+
class PrintModelGraphStart(ModelView):
'Print Model Graph'
diff --git a/trytond/ir/model.xml b/trytond/ir/model.xml
index d2806ac..9c5641a 100644
--- a/trytond/ir/model.xml
+++ b/trytond/ir/model.xml
@@ -173,5 +173,49 @@ this repository contains the full copyright notices and license terms. -->
<menuitem parent="menu_model_access_form"
action="act_model_button_form" id="menu_model_button_form"/>
+ <record model="ir.ui.view" id="model_data_view_list">
+ <field name="model">ir.model.data</field>
+ <field name="type">tree</field>
+ <field name="name">model_data_list</field>
+ </record>
+ <record model="ir.ui.view" id="model_data_view_form">
+ <field name="model">ir.model.data</field>
+ <field name="type">form</field>
+ <field name="name">model_data_form</field>
+ </record>
+
+ <record model="ir.action.act_window" id="act_model_data_form">
+ <field name="name">Data</field>
+ <field name="res_model">ir.model.data</field>
+ </record>
+ <record model="ir.action.act_window.view"
+ id="act_model_data_form_view1">
+ <field name="sequence" eval="10"/>
+ <field name="view" ref="model_data_view_list"/>
+ <field name="act_window" ref="act_model_data_form"/>
+ </record>
+ <record model="ir.action.act_window.view"
+ id="act_model_data_form_view2">
+ <field name="sequence" eval="20"/>
+ <field name="view" ref="model_data_view_form"/>
+ <field name="act_window" ref="act_model_data_form"/>
+ </record>
+ <record model="ir.action.act_window.domain"
+ 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="act_window" ref="act_model_data_form"/>
+ </record>
+ <record model="ir.action.act_window.domain"
+ id="act_model_data_form_domain_all">
+ <field name="name">All</field>
+ <field name="sequence" eval="9999"/>
+ <field name="domain"></field>
+ <field name="act_window" ref="act_model_data_form"/>
+ </record>
+ <menuitem parent="menu_model_form" action="act_model_data_form"
+ id="menu_model_data_form"/>
+
</data>
</tryton>
diff --git a/trytond/ir/module/module.py b/trytond/ir/module/module.py
index 605a614..8153bde 100644
--- a/trytond/ir/module/module.py
+++ b/trytond/ir/module/module.py
@@ -483,6 +483,12 @@ class ModuleInstallUpgrade(Wizard):
])
config = StateAction('ir.act_module_config_wizard')
+ @classmethod
+ def check_access(cls):
+ # Use new cursor to prevent lock when installing modules
+ with Transaction().new_cursor():
+ super(ModuleInstallUpgrade, cls).check_access()
+
@staticmethod
def default_start(fields):
pool = Pool()
@@ -509,13 +515,14 @@ class ModuleInstallUpgrade(Wizard):
modules = Module.search([
('state', 'in', ['to upgrade', 'to remove', 'to install']),
])
+ update = [m.name for m in modules]
langs = Lang.search([
('translatable', '=', True),
])
lang = [x.code for x in langs]
transaction.cursor.commit()
- if modules:
- pool.init(update=True, lang=lang)
+ if update:
+ pool.init(update=update, lang=lang)
return 'done'
diff --git a/trytond/ir/property.py b/trytond/ir/property.py
index 9567c45..f5c79ff 100644
--- a/trytond/ir/property.py
+++ b/trytond/ir/property.py
@@ -135,8 +135,7 @@ class Property(ModelSQL, ModelView):
('field', '=', model_field.id),
('res', 'in', [model + ',' + str(res_id) for res_id in ids]),
], order=[])
- with Transaction().set_user(0, set_context=True):
- cls.delete(properties)
+ cls.delete(properties)
defaults = cls.search([
('field', '=', model_field.id),
@@ -160,5 +159,4 @@ class Property(ModelSQL, ModelView):
if (val != default_val):
for res_id in ids:
vals = cls._set_values(model, res_id, val, model_field.id)
- with Transaction().set_user(0, set_context=True):
- cls.create([vals])
+ cls.create([vals])
diff --git a/trytond/ir/rule.py b/trytond/ir/rule.py
index 479f162..8491514 100644
--- a/trytond/ir/rule.py
+++ b/trytond/ir/rule.py
@@ -1,8 +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 time
-import datetime
-
from ..model import ModelView, ModelSQL, fields
from ..tools import safe_eval
from ..transaction import Transaction
@@ -29,6 +26,7 @@ class RuleGroup(ModelSQL, ModelView):
help="The rule is satisfied if at least one test is True")
groups = fields.Many2Many('ir.rule.group-res.group',
'rule_group', 'group', 'Groups')
+ # TODO remove to only use groups
users = fields.Many2Many('ir.rule.group-res.user',
'rule_group', 'user', 'Users')
perm_read = fields.Boolean('Read Access')
@@ -99,7 +97,7 @@ class Rule(ModelSQL, ModelView):
required=True, ondelete="CASCADE")
domain = fields.Char('Domain', required=True,
help='Domain is evaluated with "user" as the current user')
- _domain_get_cache = Cache('ir_rule.domain_get')
+ _domain_get_cache = Cache('ir_rule.domain_get', context=False)
@classmethod
def __setup__(cls):
@@ -144,15 +142,16 @@ class Rule(ModelSQL, ModelView):
def _get_context():
User = Pool().get('res.user')
user_id = Transaction().user
- with Transaction().set_user(0, set_context=True):
+ with Transaction().set_context(_check_access=False):
user = User(user_id)
return {
'user': user,
- 'current_date': datetime.datetime.today(),
- 'time': time,
- 'context': Transaction().context,
}
+ @staticmethod
+ def _get_cache_key():
+ return (Transaction().user,)
+
@classmethod
def domain_get(cls, model_name, mode='read'):
assert mode in ['read', 'write', 'create', 'delete'], \
@@ -165,7 +164,7 @@ class Rule(ModelSQL, ModelView):
with Transaction().set_user(Transaction().context['user']):
return cls.domain_get(model_name, mode=mode)
- key = (model_name, mode)
+ key = (model_name, mode) + cls._get_cache_key()
domain = cls._domain_get_cache.get(key, False)
if domain is not False:
return domain
diff --git a/trytond/ir/sequence.py b/trytond/ir/sequence.py
index d1b3c73..93bae63 100644
--- a/trytond/ir/sequence.py
+++ b/trytond/ir/sequence.py
@@ -10,14 +10,13 @@ from ..tools import datetime_strftime
from ..pyson import Eval, And
from ..transaction import Transaction
from ..pool import Pool
-from ..config import CONFIG
from .. import backend
__all__ = [
'SequenceType', 'Sequence', 'SequenceStrict',
]
-sql_sequence = CONFIG.options['db_type'] == 'postgresql'
+sql_sequence = backend.name() == 'postgresql'
class SequenceType(ModelSQL, ModelView):
@@ -314,20 +313,18 @@ class Sequence(ModelSQL, ModelView):
#Pre-fetch number_next
number_next = sequence.number_next_internal
- with Transaction().set_user(0):
- cls.write([sequence], {
- 'number_next_internal': (number_next
- + sequence.number_increment),
- })
+ cls.write([sequence], {
+ 'number_next_internal': (number_next
+ + sequence.number_increment),
+ })
return '%%0%sd' % sequence.padding % number_next
elif sequence.type in ('decimal timestamp', 'hexadecimal timestamp'):
timestamp = sequence.last_timestamp
while timestamp == sequence.last_timestamp:
timestamp = cls._timestamp(sequence)
- with Transaction().set_user(0):
- cls.write([sequence], {
- 'last_timestamp': timestamp,
- })
+ cls.write([sequence], {
+ 'last_timestamp': timestamp,
+ })
if sequence.type == 'decimal timestamp':
return '%d' % timestamp
else:
@@ -345,18 +342,18 @@ class Sequence(ModelSQL, ModelView):
domain = [('id', '=', domain)]
# bypass rules on sequences
- with Transaction().set_context(user=False):
+ with Transaction().set_context(user=False, _check_access=False):
with Transaction().set_user(0):
try:
sequence, = cls.search(domain, limit=1)
except TypeError:
cls.raise_user_error('missing')
- date = Transaction().context.get('date')
- return '%s%s%s' % (
- cls._process(sequence.prefix, date=date),
- cls._get_sequence(sequence),
- cls._process(sequence.suffix, date=date),
- )
+ date = Transaction().context.get('date')
+ return '%s%s%s' % (
+ cls._process(sequence.prefix, date=date),
+ cls._get_sequence(sequence),
+ cls._process(sequence.suffix, date=date),
+ )
@classmethod
def get(cls, code):
diff --git a/trytond/ir/session.py b/trytond/ir/session.py
index e6512ae..d9beae9 100644
--- a/trytond/ir/session.py
+++ b/trytond/ir/session.py
@@ -8,7 +8,7 @@ import uuid
import datetime
from trytond.model import ModelSQL, fields
-from trytond.config import CONFIG
+from trytond.config import config
from .. import backend
from ..transaction import Transaction
@@ -45,7 +45,8 @@ class Session(ModelSQL):
def check(cls, user, key):
"Check user key and delete old one"
now = datetime.datetime.now()
- timeout = datetime.timedelta(seconds=int(CONFIG['session_timeout']))
+ timeout = datetime.timedelta(
+ seconds=config.getint('session', 'timeout'))
sessions = cls.search([
('create_uid', '=', user),
])
diff --git a/trytond/ir/time_locale.py b/trytond/ir/time_locale.py
index e977b76..1371e77 100644
--- a/trytond/ir/time_locale.py
+++ b/trytond/ir/time_locale.py
@@ -265,6 +265,47 @@ TIME_LOCALE = \
u'nov',
u'dic'],
'%p': [u'AM', u'PM']},
+ 'es_EC': {'%A': [u'lunes',
+ u'martes',
+ u'mi\xe9rcoles',
+ u'jueves',
+ u'viernes',
+ u's\xe1bado',
+ u'domingo'],
+ '%B': [None,
+ u'enero',
+ u'febrero',
+ u'marzo',
+ u'abril',
+ u'mayo',
+ u'junio',
+ u'julio',
+ u'agosto',
+ u'septiembre',
+ u'octubre',
+ u'noviembre',
+ u'diciembre'],
+ '%a': [u'lun',
+ u'mar',
+ u'mi\xe9',
+ u'jue',
+ u'vie',
+ u's\xe1b',
+ u'dom'],
+ '%b': [None,
+ u'ene',
+ u'feb',
+ u'mar',
+ u'abr',
+ u'may',
+ u'jun',
+ u'jul',
+ u'ago',
+ u'sep',
+ u'oct',
+ u'nov',
+ u'dic'],
+ '%p': [u'AM', u'PM']},
'es_ES': {'%A': [u'lunes',
u'martes',
u'mi\xe9rcoles',
diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py
index 17e7079..b0c8a54 100644
--- a/trytond/ir/translation.py
+++ b/trytond/ir/translation.py
@@ -21,7 +21,7 @@ from sql.aggregate import Max
from ..model import ModelView, ModelSQL, fields
from ..wizard import Wizard, StateView, StateTransition, StateAction, \
Button
-from ..tools import file_open, reduce_ids
+from ..tools import file_open, reduce_ids, grouped_slice
from .. import backend
from ..pyson import PYSONEncoder
from ..transaction import Transaction
@@ -124,13 +124,10 @@ class Translation(ModelSQL, ModelView):
table = TableHandler(cursor, cls, module_name)
table.not_null_action('src_md5', action='add')
- # Migration from 2.2
+ # Migration from 2.2 and 2.8
cursor.execute(*ir_translation.update([ir_translation.res_id],
- [None], where=ir_translation.res_id == 0))
-
- # Migration from 2.8
- cursor.execute(*ir_translation.update([ir_translation.res_id],
- [-1], where=ir_translation.res_id == None))
+ [-1], where=(ir_translation.res_id == None)
+ | (ir_translation.res_id == 0)))
table = TableHandler(Transaction().cursor, cls, module_name)
table.index_action(['lang', 'type', 'name'], 'add')
@@ -387,7 +384,7 @@ class Translation(ModelSQL, ModelView):
lang = unicode(lang)
if name.split(',')[0] in ('ir.model.field', 'ir.model'):
field_name = name.split(',')[1]
- with Transaction().set_user(0):
+ with Transaction().set_context(_check_access=False):
if name.split(',')[0] == 'ir.model.field':
if field_name == 'field_description':
ttype = u'field'
@@ -433,8 +430,7 @@ class Translation(ModelSQL, ModelView):
if Transaction().context.get('fuzzy_translation', False):
fuzzy_sql = None
in_max = cursor.IN_MAX / 7
- for i in range(0, len(to_fetch), in_max):
- sub_to_fetch = to_fetch[i:i + in_max]
+ for sub_to_fetch in grouped_slice(to_fetch, in_max):
red_sql = reduce_ids(table.res_id, sub_to_fetch)
where = And(((table.lang == lang),
(table.type == ttype),
@@ -518,14 +514,14 @@ class Translation(ModelSQL, ModelView):
'fuzzy': False,
})
else:
- with Transaction().set_user(0):
+ with Transaction().set_context(_check_access=False):
cls.write([translation], {
'src': getattr(record, field_name),
'value': value,
'fuzzy': False,
})
if to_create:
- with Transaction().set_user(0):
+ with Transaction().set_context(_check_access=False):
cls.create(to_create)
return
@@ -568,7 +564,7 @@ class Translation(ModelSQL, ModelView):
'fuzzy': False,
})
else:
- with Transaction().set_user(0):
+ with Transaction().set_context(_check_access=False):
cls.write([translation], {
'value': value,
'src': getattr(record, field_name),
@@ -581,22 +577,20 @@ class Translation(ModelSQL, ModelView):
'fuzzy': True,
})
if to_create:
- with Transaction().set_user(0):
+ with Transaction().set_context(_check_access=False):
cls.create(to_create)
@classmethod
def delete_ids(cls, model, ttype, ids):
"Delete translation for each id"
- cursor = Transaction().cursor
translations = []
- for i in range(0, len(ids), cursor.IN_MAX):
- sub_ids = ids[i:i + cursor.IN_MAX]
+ for sub_ids in grouped_slice(ids):
translations += cls.search([
('type', '=', ttype),
('name', 'like', model + ',%'),
- ('res_id', 'in', sub_ids),
+ ('res_id', 'in', list(sub_ids)),
])
- with Transaction().set_user(0):
+ with Transaction().set_context(_check_access=False):
cls.delete(translations)
@classmethod
@@ -643,9 +637,8 @@ class Translation(ModelSQL, ModelView):
cursor = Transaction().cursor
table = cls.__table__()
if len(args) > cursor.IN_MAX:
- for i in range(0, len(args), cursor.IN_MAX):
- sub_args = args[i:i + cursor.IN_MAX]
- res.update(cls.get_sources(sub_args))
+ for sub_args in grouped_slice(args):
+ res.update(cls.get_sources(list(sub_args)))
return res
for name, ttype, lang, source in args:
name = unicode(name)
@@ -672,11 +665,11 @@ class Translation(ModelSQL, ModelView):
clause.append(where)
if clause:
in_max = cursor.IN_MAX / 7
- for i in range(0, len(clause), in_max):
+ for sub_clause in grouped_slice(clause, in_max):
cursor.execute(*table.select(
table.lang, table.type, table.name, table.src,
table.value,
- where=Or(clause[i:i + in_max])))
+ where=Or(list(sub_clause))))
for lang, ttype, name, source, value in cursor.fetchall():
if (name, ttype, lang, source) not in args:
source = None
@@ -815,8 +808,7 @@ class Translation(ModelSQL, ModelView):
res_id = model_data.db_id
else:
res_id = -1
- with Transaction().set_user(0), \
- Transaction().set_context(module=res_id_module):
+ with Transaction().set_context(module=res_id_module):
domain = [
('name', '=', new_translation.name),
('res_id', '=', res_id),
@@ -889,8 +881,7 @@ class Translation(ModelSQL, ModelView):
or old_translation.fuzzy !=
translation.fuzzy):
to_write.append(old_translation)
- with Transaction().set_user(0), \
- Transaction().set_context(module=module):
+ with Transaction().set_context(module=module):
if to_write and not noupdate:
cls.write(to_write, {
'value': translation.value,
@@ -899,8 +890,7 @@ class Translation(ModelSQL, ModelView):
translations |= set(cls.browse(ids))
if to_create:
- with Transaction().set_user(0), \
- Transaction().set_context(module=module):
+ with Transaction().set_context(module=module):
translations |= set(cls.create(to_create))
if translations:
@@ -1493,8 +1483,7 @@ class TranslationUpdate(Wizard):
'module': row['module'],
})
if to_create:
- with Transaction().set_user(0):
- Translation.create(to_create)
+ Translation.create(to_create)
columns = [translation.name.as_('name'),
translation.res_id.as_('res_id'), translation.type.as_('type'),
translation.module.as_('module')]
@@ -1514,8 +1503,7 @@ class TranslationUpdate(Wizard):
'module': row['module'],
})
if to_create:
- with Transaction().set_user(0):
- Translation.create(to_create)
+ Translation.create(to_create)
columns = [translation.name.as_('name'),
translation.res_id.as_('res_id'), translation.type.as_('type'),
translation.src.as_('src'), translation.module.as_('module')]
diff --git a/trytond/ir/translation.xml b/trytond/ir/translation.xml
index d6de503..e93b30b 100644
--- a/trytond/ir/translation.xml
+++ b/trytond/ir/translation.xml
@@ -127,8 +127,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">digits_validation_record</field>
<field name="lang">en_US</field>
<field name="type">error</field>
- <field name="src">The number of digits "%(digits)s" of field "%(field)s" on "%(value)s" exceeds it's limit.</field>
- <field name="value">The number of digits "%(digits)s" of field "%(field)s" on "%(value)s" exceeds it's limit.</field>
+ <field name="src">The number of digits "%(digits)s" of field "%(field)s" on "%(value)s" exceeds its limit.</field>
+ <field name="value">The number of digits "%(digits)s" of field "%(field)s" on "%(value)s" exceeds its limit.</field>
<field name="module">ir</field>
<field name="fuzzy" eval="False"/>
</record>
diff --git a/trytond/ir/trigger.py b/trytond/ir/trigger.py
index 56dbddd..9b88d2f 100644
--- a/trytond/ir/trigger.py
+++ b/trytond/ir/trigger.py
@@ -7,7 +7,7 @@ from sql.aggregate import Count, Max
from ..model import ModelView, ModelSQL, fields
from ..pyson import Eval
-from ..tools import safe_eval
+from ..tools import safe_eval, grouped_slice
from .. import backend
from ..tools import reduce_ids
from ..transaction import Transaction
@@ -172,15 +172,14 @@ class Trigger(ModelSQL, ModelView):
Model = pool.get(trigger.model.model)
ActionModel = pool.get(trigger.action_model.model)
cursor = Transaction().cursor
- in_max = cursor.IN_MAX
trigger_log = TriggerLog.__table__()
ids = map(int, records)
# Filter on limit_number
if trigger.limit_number:
new_ids = []
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids):
+ sub_ids = list(sub_ids)
red_sql = reduce_ids(trigger_log.record_id, sub_ids)
cursor.execute(*trigger_log.select(
trigger_log.record_id, Count(Literal(1)),
@@ -198,8 +197,8 @@ class Trigger(ModelSQL, ModelView):
# Filter on minimum_delay
if trigger.minimum_delay:
new_ids = []
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids):
+ sub_ids = list(sub_ids)
red_sql = reduce_ids(trigger_log.record_id, sub_ids)
cursor.execute(*trigger_log.select(
trigger_log.record_id, Max(trigger_log.create_date),
diff --git a/trytond/ir/ui/icons/tryton-tree.svg b/trytond/ir/ui/icons/tryton-tree.svg
index 0f9b8a2..3e26f16 100644
--- a/trytond/ir/ui/icons/tryton-tree.svg
+++ b/trytond/ir/ui/icons/tryton-tree.svg
@@ -14,7 +14,7 @@
height="48px"
id="svg4198"
sodipodi:version="0.32"
- inkscape:version="0.48.0 r9654"
+ inkscape:version="0.48.4 r9939"
sodipodi:docname="tryton-tree.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.1">
@@ -207,15 +207,15 @@
borderopacity="1.0000000"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
- inkscape:zoom="16.5"
- inkscape:cx="24"
- inkscape:cy="24"
+ inkscape:zoom="6.51"
+ inkscape:cx="22.261972"
+ inkscape:cy="12.80998"
inkscape:current-layer="layer1"
- showgrid="false"
+ showgrid="true"
inkscape:grid-bbox="true"
inkscape:document-units="px"
- inkscape:window-width="1278"
- inkscape:window-height="1007"
+ inkscape:window-width="1364"
+ inkscape:window-height="751"
inkscape:window-x="0"
inkscape:window-y="15"
inkscape:showpageshadow="false"
@@ -244,11 +244,6 @@
</dc:subject>
<cc:license
rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
- <dc:contributor>
- <cc:Agent>
- <dc:title>Cédric Krier</dc:title>
- </cc:Agent>
- </dc:contributor>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/">
@@ -297,43 +292,43 @@
rx="0.56650788"
ry="0.56650823" />
<rect
- style="color:#000000;fill:#999999;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible"
+ style="opacity:1;color:#000000;fill:#999999;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
id="rect4248"
- width="26"
+ width="30"
height="2"
- x="-34"
+ x="-39"
y="10"
- transform="scale(-1,1)" />
+ transform="scale(-1.000000,1.000000)" />
<rect
y="16"
- x="-31"
+ x="-39"
height="2"
- width="23"
+ width="25.5"
id="rect4250"
style="color:#000000;fill:#999999;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible"
transform="scale(-1,1)" />
<rect
style="color:#000000;fill:#999999;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible"
id="rect4252"
- width="21"
+ width="19"
height="2"
- x="-29"
+ x="-39"
y="22"
transform="scale(-1,1)" />
<rect
y="28"
- x="-34"
+ x="-39"
height="2"
- width="26"
+ width="12.5"
id="rect4254"
style="color:#000000;fill:#999999;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible"
transform="scale(-1,1)" />
<rect
style="color:#000000;fill:#999999;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible"
id="rect4256"
- width="17"
+ width="6"
height="2"
- x="-25"
+ x="-38.5"
y="34"
transform="scale(-1,1)" />
<rect
diff --git a/trytond/ir/ui/menu.py b/trytond/ir/ui/menu.py
index 2d3b45e..94af4d8 100644
--- a/trytond/ir/ui/menu.py
+++ b/trytond/ir/ui/menu.py
@@ -1,5 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
+from itertools import groupby
+
from trytond.model import ModelView, ModelSQL, fields
from trytond.transaction import Transaction
from trytond.pool import Pool
@@ -89,6 +91,8 @@ class UIMenu(ModelSQL, ModelView):
('ir.action.wizard', 'ir.action.wizard'),
('ir.action.url', 'ir.action.url'),
]), 'get_action', setter='set_action')
+ action_keywords = fields.One2Many('ir.action.keyword', 'model',
+ 'Action Keywords')
active = fields.Boolean('Active')
favorite = fields.Function(fields.Boolean('Favorite'), 'get_favorite')
@@ -190,25 +194,27 @@ class UIMenu(ModelSQL, ModelView):
@classmethod
def get_action(cls, menus, name):
pool = Pool()
- ActionKeyword = pool.get('ir.action.keyword')
actions = dict((m.id, None) for m in menus)
with Transaction().set_context(active_test=False):
- action_keywords = ActionKeyword.search([
- ('keyword', '=', 'tree_open'),
- ('model', 'in', [str(m) for m in menus]),
- ])
- for action_keyword in action_keywords:
- model = action_keyword.model
- Action = pool.get(action_keyword.action.type)
+ menus = cls.browse(menus)
+ action_keywords = sum((list(m.action_keywords) for m in menus), [])
+ key = lambda k: k.action.type
+ action_keywords.sort(key=key)
+ for type, action_keywords in groupby(action_keywords, key=key):
+ action_keywords = list(action_keywords)
+ for action_keyword in action_keywords:
+ model = action_keyword.model
+ actions[model.id] = '%s,-1' % type
+
+ Action = pool.get(type)
+ action2keyword = {k.action.id: k for k in action_keywords}
with Transaction().set_context(active_test=False):
factions = Action.search([
- ('action', '=', action_keyword.action.id),
- ], limit=1)
- if factions:
- action, = factions
- else:
- action = '%s,0' % action_keyword.action.type
- actions[model.id] = str(action)
+ ('action', 'in', action2keyword.keys()),
+ ])
+ for action in factions:
+ model = action2keyword[action.id].model
+ actions[model.id] = str(action)
return actions
@classmethod
diff --git a/trytond/ir/ui/tree.rnc b/trytond/ir/ui/tree.rnc
index 3500bc8..925331e 100644
--- a/trytond/ir/ui/tree.rnc
+++ b/trytond/ir/ui/tree.rnc
@@ -12,6 +12,8 @@ attlist.tree &= attribute sequence { text }?
attlist.tree &= attribute colors { text }?
attlist.tree &=
[ a:defaultValue = "0" ] attribute keyword_open { "0" | "1" }?
+attlist.tree &=
+ [ a:defaultValue = "0" ] attribute tree_state { "0" | "1" }?
field = element field { attlist.field, (prefix | suffix)* }
attlist.field &= attribute name { text }
attlist.field &= attribute readonly { "0" | "1" }?
@@ -39,6 +41,7 @@ attlist.field &=
| "reference"
| "one2one"
| "binary"
+ | "image"
}?
attlist.field &=
[ a:defaultValue = "0" ] attribute tree_invisible { "0" | "1" }?
@@ -47,6 +50,7 @@ attlist.field &=
attlist.field &= attribute icon { text }?
attlist.field &= attribute sum { text }?
attlist.field &= attribute width { text }?
+attlist.field &= attribute height { text }?
attlist.field &=
[ a:defaultValue = "left_to_right" ] attribute orientation {
"left_to_right"
diff --git a/trytond/ir/ui/tree.rng b/trytond/ir/ui/tree.rng
index 85ecc64..0508c44 100644
--- a/trytond/ir/ui/tree.rng
+++ b/trytond/ir/ui/tree.rng
@@ -51,6 +51,16 @@
</attribute>
</optional>
</define>
+ <define name="attlist.tree" combine="interleave">
+ <optional>
+ <attribute name="tree_state" a:defaultValue="0">
+ <choice>
+ <value>0</value>
+ <value>1</value>
+ </choice>
+ </attribute>
+ </optional>
+ </define>
<define name="field">
<element name="field">
<ref name="attlist.field"/>
@@ -101,6 +111,7 @@
<value>reference</value>
<value>one2one</value>
<value>binary</value>
+ <value>image</value>
</choice>
</attribute>
</optional>
@@ -142,6 +153,11 @@
</define>
<define name="attlist.field" combine="interleave">
<optional>
+ <attribute name="height"/>
+ </optional>
+ </define>
+ <define name="attlist.field" combine="interleave">
+ <optional>
<attribute name="orientation" a:defaultValue="left_to_right">
<choice>
<value>left_to_right</value>
diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py
index 5e16398..2c3fb90 100644
--- a/trytond/ir/ui/view.py
+++ b/trytond/ir/ui/view.py
@@ -3,6 +3,11 @@
import os
import sys
import logging
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
from lxml import etree
from trytond.model import ModelView, ModelSQL, fields
from trytond import backend
@@ -305,8 +310,8 @@ class ViewTreeState(ModelSQL, ModelView):
def __setup__(cls):
super(ViewTreeState, cls).__setup__()
cls.__rpc__.update({
- 'set': RPC(readonly=False),
- 'get': RPC(),
+ 'set': RPC(readonly=False, check_access=False),
+ 'get': RPC(check_access=False),
})
@classmethod
@@ -335,40 +340,40 @@ class ViewTreeState(ModelSQL, ModelView):
@classmethod
def set(cls, model, domain, child_name, nodes, selected_nodes):
current_user = Transaction().user
- with Transaction().set_user(0):
- records = cls.search([
- ('user', '=', current_user),
- ('model', '=', model),
- ('domain', '=', domain),
- ('child_name', '=', child_name),
- ])
- cls.delete(records)
- cls.create([{
- 'user': current_user,
- 'model': model,
- 'domain': domain,
- 'child_name': child_name,
- 'nodes': nodes,
- 'selected_nodes': selected_nodes,
- }])
+ records = cls.search([
+ ('user', '=', current_user),
+ ('model', '=', model),
+ ('domain', '=', domain),
+ ('child_name', '=', child_name),
+ ])
+ cls.delete(records)
+ cls.create([{
+ 'user': current_user,
+ 'model': model,
+ 'domain': domain,
+ 'child_name': child_name,
+ 'nodes': nodes,
+ 'selected_nodes': selected_nodes,
+ }])
@classmethod
def get(cls, model, domain, child_name):
+ # Normalize the json domain
+ domain = json.dumps(json.loads(domain))
current_user = Transaction().user
- with Transaction().set_user(0):
- try:
- expanded_info, = cls.search([
- ('user', '=', current_user),
- ('model', '=', model),
- ('domain', '=', domain),
- ('child_name', '=', child_name),
- ],
- limit=1)
- except ValueError:
- return (cls.default_nodes(), cls.default_selected_nodes())
- state = cls(expanded_info)
- return (state.nodes or cls.default_nodes(),
- state.selected_nodes or cls.default_selected_nodes())
+ try:
+ expanded_info, = cls.search([
+ ('user', '=', current_user),
+ ('model', '=', model),
+ ('domain', '=', domain),
+ ('child_name', '=', child_name),
+ ],
+ limit=1)
+ except ValueError:
+ return (cls.default_nodes(), cls.default_selected_nodes())
+ state = cls(expanded_info)
+ return (state.nodes or cls.default_nodes(),
+ state.selected_nodes or cls.default_selected_nodes())
class ViewSearch(ModelSQL, ModelView):
diff --git a/trytond/ir/view/action_act_window_view_form.xml b/trytond/ir/view/action_act_window_view_form.xml
index d282a67..689eac0 100644
--- a/trytond/ir/view/action_act_window_view_form.xml
+++ b/trytond/ir/view/action_act_window_view_form.xml
@@ -1,11 +1,13 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
-<form string="View" col="2">
+<form string="View">
<label name="act_window"/>
- <field name="act_window"/>
+ <field name="act_window" colspan="3"/>
<label name="sequence"/>
<field name="sequence"/>
+ <label name="active"/>
+ <field name="active"/>
<label name="view"/>
<field name="view"/>
</form>
diff --git a/trytond/ir/view/action_act_window_view_list.xml b/trytond/ir/view/action_act_window_view_list.xml
index 5868cf7..4f8c136 100644
--- a/trytond/ir/view/action_act_window_view_list.xml
+++ b/trytond/ir/view/action_act_window_view_list.xml
@@ -5,4 +5,5 @@ this repository contains the full copyright notices and license terms. -->
<field name="act_window"/>
<field name="sequence"/>
<field name="view"/>
+ <field name="active" tree_invisible="1"/>
</tree>
diff --git a/trytond/ir/view/model_data_form.xml b/trytond/ir/view/model_data_form.xml
new file mode 100644
index 0000000..9ba13fa
--- /dev/null
+++ b/trytond/ir/view/model_data_form.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form string="Model Data">
+ <label name="module"/>
+ <field name="module"/>
+ <label name="model"/>
+ <field name="model"/>
+ <label name="fs_id"/>
+ <field name="fs_id"/>
+ <label name="db_id"/>
+ <field name="db_id"/>
+ <label name="noupdate"/>
+ <field name="noupdate"/>
+ <newline/>
+ <label name="out_of_sync"/>
+ <field name="out_of_sync"/>
+ <button string="Sync" name="sync" colspan="2"/>
+ <group id="values" colspan="2" yexpand="1" yfill="1" col="1">
+ <separator name="values"/>
+ <field name="values"/>
+ </group>
+ <group id="fs_values" colspan="2" yexpand="1" yfill="1" col="1">
+ <separator name="fs_values"/>
+ <field name="fs_values"/>
+ </group>
+</form>
diff --git a/trytond/ir/view/model_data_list.xml b/trytond/ir/view/model_data_list.xml
new file mode 100644
index 0000000..ef361cc
--- /dev/null
+++ b/trytond/ir/view/model_data_list.xml
@@ -0,0 +1,13 @@
+<?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="Model Data">
+ <field name="module"/>
+ <field name="model"/>
+ <field name="fs_id"/>
+ <field name="db_id"/>
+ <field name="noupdate"/>
+ <field name="out_of_sync"/>
+ <button string="Sync" name="sync"/>
+</tree>
+
diff --git a/trytond/ir/view/ui_menu_tree.xml b/trytond/ir/view/ui_menu_tree.xml
index 36562f9..995c3cd 100644
--- a/trytond/ir/view/ui_menu_tree.xml
+++ b/trytond/ir/view/ui_menu_tree.xml
@@ -1,7 +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="Menu" keyword_open="1">
+<tree string="Menu" keyword_open="1" tree_state="1">
<field name="name" icon="icon" expand="1"/>
<field name="favorite" tree_invisible="1"/>
</tree>
diff --git a/trytond/model/__init__.py b/trytond/model/__init__.py
index b63e22c..dae657d 100644
--- a/trytond/model/__init__.py
+++ b/trytond/model/__init__.py
@@ -7,6 +7,8 @@ from .modelsingleton import ModelSingleton
from .modelsql import ModelSQL
from .workflow import Workflow
from .dictschema import DictSchemaMixin
+from .match import MatchMixin
+from .union import UnionMixin
__all__ = ['Model', 'ModelView', 'ModelStorage', 'ModelSingleton', 'ModelSQL',
- 'Workflow', 'DictSchemaMixin']
+ 'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin']
diff --git a/trytond/model/dictschema.py b/trytond/model/dictschema.py
index d6f1b33..34363a8 100644
--- a/trytond/model/dictschema.py
+++ b/trytond/model/dictschema.py
@@ -1,5 +1,6 @@
# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
# repository contains the full copyright notices and license terms.
+from collections import OrderedDict
try:
import simplejson as json
except ImportError:
@@ -33,6 +34,10 @@ class DictSchemaMixin(object):
'invisible': Eval('type_') != 'selection',
}, translate=True, depends=['type_'],
help='A couple of key and label separated by ":" per line')
+ selection_sorted = fields.Boolean('Selection Sorted', states={
+ 'invisible': Eval('type_') != 'selection',
+ }, depends=['type_'],
+ help='If the selection must be sorted on label')
selection_json = fields.Function(fields.Char('Selection JSON',
states={
'invisible': Eval('type_') != 'selection',
@@ -50,6 +55,10 @@ class DictSchemaMixin(object):
def default_digits():
return 2
+ @staticmethod
+ def default_selection_sorted():
+ return True
+
def get_selection_json(self, name):
db_selection = self.selection or ''
selection = [[w.strip() for w in v.split(':', 1)]
@@ -71,9 +80,11 @@ class DictSchemaMixin(object):
if record.type_ == 'selection':
with Transaction().set_context(language=Config.get_language()):
english_key = cls(record.id)
- selection = dict(json.loads(english_key.selection_json))
+ selection = OrderedDict(json.loads(
+ english_key.selection_json))
selection.update(dict(json.loads(record.selection_json)))
new_key['selection'] = selection.items()
+ new_key['sorted'] = record.selection_sorted
elif record.type_ in ('float', 'numeric'):
new_key['digits'] = (16, record.digits)
keys.append(new_key)
diff --git a/trytond/model/fields/binary.py b/trytond/model/fields/binary.py
index 94b278e..42faa79 100644
--- a/trytond/model/fields/binary.py
+++ b/trytond/model/fields/binary.py
@@ -4,7 +4,7 @@ from sql import Query, Expression
from .field import Field, SQLType
from ...transaction import Transaction
-from ...config import CONFIG
+from ... import backend
class Binary(Field):
@@ -59,14 +59,14 @@ class Binary(Field):
def sql_format(value):
if isinstance(value, (Query, Expression)):
return value
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'postgresql' and value is not None:
import psycopg2
return psycopg2.Binary(value)
return value
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'postgresql':
return SQLType('BYTEA', 'BYTEA')
elif db_type == 'mysql':
diff --git a/trytond/model/fields/char.py b/trytond/model/fields/char.py
index 1168cac..6254077 100644
--- a/trytond/model/fields/char.py
+++ b/trytond/model/fields/char.py
@@ -4,7 +4,7 @@ import warnings
from sql import Query, Expression
-from ...config import CONFIG
+from ... import backend
from .field import Field, FieldTranslate, size_validate, SQLType
@@ -59,7 +59,7 @@ class Char(FieldTranslate):
return value
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if self.size and db_type != 'sqlite':
return SQLType('VARCHAR', 'VARCHAR(%s)' % self.size)
elif db_type == 'mysql':
diff --git a/trytond/model/fields/date.py b/trytond/model/fields/date.py
index f769e9d..428f6c4 100644
--- a/trytond/model/fields/date.py
+++ b/trytond/model/fields/date.py
@@ -3,7 +3,7 @@
import datetime
from sql import Query, Expression
-from ...config import CONFIG
+from ... import backend
from .field import Field, SQLType
@@ -71,7 +71,7 @@ class DateTime(Field):
return value.replace(microsecond=0)
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'sqlite':
return SQLType('TIMESTAMP', 'TIMESTAMP')
elif db_type == 'mysql':
@@ -106,7 +106,7 @@ class Timestamp(Field):
return value
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'sqlite':
return SQLType('TIMESTAMP', 'TIMESTAMP')
elif db_type == 'mysql':
diff --git a/trytond/model/fields/dict.py b/trytond/model/fields/dict.py
index a775916..ef63d52 100644
--- a/trytond/model/fields/dict.py
+++ b/trytond/model/fields/dict.py
@@ -7,7 +7,7 @@ except ImportError:
from sql import Query, Expression
from .field import Field, SQLType
-from ...protocols.jsonrpc import object_hook, JSONEncoder
+from ...protocols.jsonrpc import JSONDecoder, JSONEncoder
class Dict(Field):
@@ -28,7 +28,7 @@ class Dict(Field):
for value in values or []:
if value[name]:
dicts[value['id']] = json.loads(value[name],
- object_hook=object_hook)
+ object_hook=JSONDecoder())
return dicts
@staticmethod
diff --git a/trytond/model/fields/field.py b/trytond/model/fields/field.py
index 1404b80..3eb95f2 100644
--- a/trytond/model/fields/field.py
+++ b/trytond/model/fields/field.py
@@ -81,6 +81,9 @@ def depends(*fields, **kwargs):
@wraps(func)
def wrapper(self, *args, **kwargs):
for field in fields:
+ field = field.split('.')[0]
+ if field.startswith('_parent_'):
+ field = field[8:] # Strip '_parent_'
if not hasattr(self, field):
setattr(self, field, None)
return func(self, *args, **kwargs)
@@ -248,6 +251,9 @@ class Field(object):
table, _ = tables[None]
name, operator, value = domain
assert name == self.name
+ method = getattr(Model, 'domain_%s' % name, None)
+ if method:
+ return method(domain, tables)
Operator = SQL_OPERATORS[operator]
column = self.sql_column(table)
expression = Operator(column, self._domain_value(operator, value))
diff --git a/trytond/model/fields/float.py b/trytond/model/fields/float.py
index 1041f0a..42a8db5 100644
--- a/trytond/model/fields/float.py
+++ b/trytond/model/fields/float.py
@@ -2,7 +2,7 @@
#this repository contains the full copyright notices and license terms.
from sql import Query, Expression
-from ...config import CONFIG
+from ... import backend
from .field import Field, SQLType
from ...pyson import PYSON
@@ -59,7 +59,7 @@ class Float(Field):
return float(value)
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'postgresql':
return SQLType('FLOAT8', 'FLOAT8')
elif db_type == 'mysql':
diff --git a/trytond/model/fields/function.py b/trytond/model/fields/function.py
index d5489b7..8a72955 100644
--- a/trytond/model/fields/function.py
+++ b/trytond/model/fields/function.py
@@ -4,6 +4,7 @@
import inspect
import copy
from trytond.model.fields.field import Field
+from trytond.transaction import Transaction
class Function(Field):
@@ -37,14 +38,11 @@ class Function(Field):
def __copy__(self):
return Function(copy.copy(self._field), self.getter,
- setter=self.setter, searcher=self.searcher)
+ setter=self.setter, searcher=self.searcher, loading=self.loading)
def __deepcopy__(self, memo):
- return Function(copy.deepcopy(self._field, memo),
- copy.deepcopy(self.getter, memo),
- setter=copy.deepcopy(self.setter, memo),
- searcher=copy.deepcopy(self.searcher, memo),
- loading=copy.deepcopy(self.loading, memo))
+ return Function(copy.deepcopy(self._field, memo), self.getter,
+ setter=self.setter, searcher=self.searcher, loading=self.loading)
def __getattr__(self, name):
return getattr(self._field, name)
@@ -71,36 +69,38 @@ class Function(Field):
If the function has ``names`` in the function definition then
it will call it with a list of name.
'''
- method = getattr(Model, self.getter)
+ with Transaction().set_context(_check_access=False):
+ method = getattr(Model, self.getter)
- def call(name):
- records = Model.browse(ids)
- if not hasattr(method, 'im_self') or method.im_self:
- return method(records, name)
+ def call(name):
+ records = Model.browse(ids)
+ if not hasattr(method, 'im_self') or method.im_self:
+ return method(records, name)
+ else:
+ return dict((r.id, method(r, name)) for r in records)
+ if isinstance(name, list):
+ names = name
+ # Test is the function works with a list of names
+ if 'names' in inspect.getargspec(method)[0]:
+ return call(names)
+ return dict((name, call(name)) for name in names)
else:
- return dict((r.id, method(r, name)) for r in records)
- if isinstance(name, list):
- names = name
- # Test is the function works with a list of names
- if 'names' in inspect.getargspec(method)[0]:
- return call(names)
- return dict((name, call(name)) for name in names)
- else:
- # Test is the function works with a list of names
- if 'names' in inspect.getargspec(method)[0]:
- name = [name]
- return call(name)
+ # Test is the function works with a list of names
+ if 'names' in inspect.getargspec(method)[0]:
+ name = [name]
+ return call(name)
def set(self, Model, name, ids, value, *args):
'''
Call the setter.
'''
- if self.setter:
- # TODO change setter API to use sequence of records, value
- setter = getattr(Model, self.setter)
- args = iter((ids, value) + args)
- for ids, value in zip(args, args):
- setter(Model.browse(ids), name, value)
+ with Transaction().set_context(_check_access=False):
+ if self.setter:
+ # TODO change setter API to use sequence of records, value
+ setter = getattr(Model, self.setter)
+ args = iter((ids, value) + args)
+ for ids, value in zip(args, args):
+ setter(Model.browse(ids), name, value)
def __set__(self, inst, value):
self._field.__set__(inst, value)
diff --git a/trytond/model/fields/integer.py b/trytond/model/fields/integer.py
index 2ba0694..64f51bf 100644
--- a/trytond/model/fields/integer.py
+++ b/trytond/model/fields/integer.py
@@ -2,7 +2,7 @@
#this repository contains the full copyright notices and license terms.
from sql import Query, Expression
-from ...config import CONFIG
+from ... import backend
from .field import Field, SQLType
@@ -13,7 +13,7 @@ class Integer(Field):
_type = 'integer'
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'postgresql':
return SQLType('INT4', 'INT4')
elif db_type == 'mysql':
@@ -22,7 +22,7 @@ class Integer(Field):
return SQLType('INTEGER', 'INTEGER')
def sql_format(self, value):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if (db_type == 'sqlite'
and value is not None
and not isinstance(value, (Query, Expression))):
@@ -37,7 +37,7 @@ class BigInteger(Integer):
_type = 'biginteger'
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'postgresql':
return SQLType('INT8', 'INT8')
return super(BigInteger, self).sql_type()
diff --git a/trytond/model/fields/many2many.py b/trytond/model/fields/many2many.py
index f037eaa..46f4da5 100644
--- a/trytond/model/fields/many2many.py
+++ b/trytond/model/fields/many2many.py
@@ -5,8 +5,8 @@ from sql import Cast, Literal
from sql.functions import Substring, Position
from .field import Field, size_validate
-from ...transaction import Transaction
from ...pool import Pool
+from ...tools import grouped_slice
class Many2Many(Field):
@@ -58,6 +58,10 @@ class Many2Many(Field):
size = property(_get_size, _set_size)
+ @property
+ def add_remove(self):
+ return self.domain
+
def get(self, ids, model, name, values=None):
'''
Return target records ordered.
@@ -79,13 +83,12 @@ class Many2Many(Field):
origin_field = Relation._fields[self.origin]
relations = []
- for i in range(0, len(ids), Transaction().cursor.IN_MAX):
- sub_ids = ids[i:i + Transaction().cursor.IN_MAX]
+ for sub_ids in grouped_slice(ids):
if origin_field._type == 'reference':
references = ['%s,%s' % (model.__name__, x) for x in sub_ids]
clause = [(self.origin, 'in', references)]
else:
- clause = [(self.origin, 'in', sub_ids)]
+ clause = [(self.origin, 'in', list(sub_ids))]
clause += [(self.target + '.id', '!=', None)]
relations.append(Relation.search(clause, order=order))
relations = list(chain(*relations))
@@ -150,12 +153,10 @@ class Many2Many(Field):
if not target_ids:
return
existing_ids = set()
- in_max = Transaction().cursor.IN_MAX
- for i in range(0, len(target_ids), in_max):
- sub_ids = target_ids[i:i + in_max]
+ for sub_ids in grouped_slice(target_ids):
relations = Relation.search([
search_clause(ids),
- (self.target, 'in', sub_ids),
+ (self.target, 'in', list(sub_ids)),
])
for relation in relations:
existing_ids.add(getattr(relation, self.target).id)
@@ -172,12 +173,10 @@ class Many2Many(Field):
target_ids = map(int, target_ids)
if not target_ids:
return
- in_max = Transaction().cursor.IN_MAX
- for i in range(0, len(target_ids), in_max):
- sub_ids = target_ids[i:i + in_max]
+ for sub_ids in grouped_slice(target_ids):
relation_to_delete.extend(Relation.search([
search_clause(ids),
- (self.target, 'in', sub_ids),
+ (self.target, 'in', list(sub_ids)),
]))
def copy(ids, copy_ids, default=None):
diff --git a/trytond/model/fields/many2one.py b/trytond/model/fields/many2one.py
index ef109d0..e6aa9ba 100644
--- a/trytond/model/fields/many2one.py
+++ b/trytond/model/fields/many2one.py
@@ -6,7 +6,7 @@ from sql.operators import Or
from .field import Field, SQLType
from ...pool import Pool
-from ...config import CONFIG
+from ... import backend
from ...tools import reduce_ids
from ...transaction import Transaction
@@ -86,7 +86,7 @@ class Many2One(Field):
return int(value)
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'postgresql':
return SQLType('INT4', 'INT4')
elif db_type == 'mysql':
diff --git a/trytond/model/fields/numeric.py b/trytond/model/fields/numeric.py
index 2a8dda3..3580bb7 100644
--- a/trytond/model/fields/numeric.py
+++ b/trytond/model/fields/numeric.py
@@ -3,7 +3,7 @@
from decimal import Decimal
from sql import Query, Expression, Cast, Literal, Select, CombiningQuery
-from ...config import CONFIG
+from ... import backend
from .field import SQLType
from .float import Float
@@ -26,14 +26,14 @@ class Numeric(Float):
return value
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'mysql':
return SQLType('DECIMAL', 'DECIMAL(65, 30)')
return SQLType('NUMERIC', 'NUMERIC')
def sql_column(self, table):
column = super(Numeric, self).sql_column(table)
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'sqlite':
# Must be casted as Decimal is stored as bytes
column = Cast(column, self.sql_type().base)
@@ -41,7 +41,7 @@ class Numeric(Float):
def _domain_value(self, operator, value):
value = super(Numeric, self)._domain_value(operator, value)
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'sqlite':
if isinstance(value, (Select, CombiningQuery)):
return value
diff --git a/trytond/model/fields/one2many.py b/trytond/model/fields/one2many.py
index f9c4f2a..5387ab6 100644
--- a/trytond/model/fields/one2many.py
+++ b/trytond/model/fields/one2many.py
@@ -5,8 +5,8 @@ from sql import Cast, Literal
from sql.functions import Substring, Position
from .field import Field, size_validate
-from ...transaction import Transaction
from ...pool import Pool
+from ...tools import grouped_slice
def add_remove_validate(value):
@@ -87,13 +87,12 @@ class One2Many(Field):
res[i] = []
targets = []
- for i in range(0, len(ids), Transaction().cursor.IN_MAX):
- sub_ids = ids[i:i + Transaction().cursor.IN_MAX]
+ for sub_ids in grouped_slice(ids):
if field._type == 'reference':
references = ['%s,%s' % (model.__name__, x) for x in sub_ids]
clause = [(self.field, 'in', references)]
else:
- clause = [(self.field, 'in', sub_ids)]
+ clause = [(self.field, 'in', list(sub_ids))]
targets.append(Relation.search(clause, order=self.order))
targets = list(chain(*targets))
@@ -162,12 +161,10 @@ class One2Many(Field):
target_ids = map(int, target_ids)
if not target_ids:
return
- in_max = Transaction().cursor.IN_MAX
- for i in range(0, len(target_ids), in_max):
- sub_ids = target_ids[i:i + in_max]
+ for sub_ids in grouped_slice(target_ids):
targets = Target.search([
search_clause(ids),
- ('id', 'in', sub_ids),
+ ('id', 'in', list(sub_ids)),
])
to_write.extend((targets, {
self.field: None,
diff --git a/trytond/model/fields/property.py b/trytond/model/fields/property.py
index 13737dd..94a77a4 100644
--- a/trytond/model/fields/property.py
+++ b/trytond/model/fields/property.py
@@ -42,25 +42,27 @@ class Property(Function):
:param values:
:return: a dictionary with ids as key and values as value
'''
- pool = Pool()
- Property = pool.get('ir.property')
- return Property.get(name, model.__name__, ids)
+ with Transaction().set_context(_check_access=False):
+ pool = Pool()
+ Property = pool.get('ir.property')
+ return Property.get(name, model.__name__, ids)
def set(self, Model, name, ids, value, *args):
'''
Set the property.
'''
- pool = Pool()
- Property = pool.get('ir.property')
- args = iter((ids, value) + args)
- for ids, value in zip(args, args):
- if value is not None:
- prop_value = '%s,%s' % (getattr(self, 'model_name', ''),
- str(value))
- else:
- prop_value = None
- # TODO change set API to use sequence of records, value
- Property.set(name, Model.__name__, ids, prop_value)
+ with Transaction().set_context(_check_access=False):
+ pool = Pool()
+ Property = pool.get('ir.property')
+ args = iter((ids, value) + args)
+ for ids, value in zip(args, args):
+ if value is not None:
+ prop_value = '%s,%s' % (getattr(self, 'model_name', ''),
+ str(value))
+ else:
+ prop_value = None
+ # TODO change set API to use sequence of records, value
+ Property.set(name, Model.__name__, ids, prop_value)
def convert_domain(self, domain, tables, Model):
pool = Pool()
diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py
index 1e3b378..e4a4079 100644
--- a/trytond/model/fields/reference.py
+++ b/trytond/model/fields/reference.py
@@ -8,7 +8,7 @@ from .field import Field, SQLType
from .char import Char
from ...transaction import Transaction
from ...pool import Pool
-from ...config import CONFIG
+from ... import backend
class Reference(Field):
@@ -68,7 +68,7 @@ class Reference(Field):
# Check if reference ids still exist
with Transaction().set_context(active_test=False), \
- Transaction().set_user(0):
+ Transaction().set_context(_check_access=False):
for ref_model, (ref_ids, ids) in ref_to_check.iteritems():
try:
pool.get(ref_model)
@@ -89,11 +89,14 @@ class Reference(Field):
from ..model import Model
if not isinstance(value, (Model, NoneType)):
if isinstance(value, basestring):
- target, id_ = value.split(',')
+ target, value = value.split(',')
else:
- target, id_ = value
+ target, value = value
Target = Pool().get(target)
- value = Target(id_)
+ if isinstance(value, dict):
+ value = Target(**value)
+ else:
+ value = Target(value)
super(Reference, self).__set__(inst, value)
@staticmethod
@@ -106,7 +109,7 @@ class Reference(Field):
return Char.sql_format(value)
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'mysql':
return SQLType('CHAR', 'VARCHAR(255)')
return SQLType('VARCHAR', 'VARCHAR')
diff --git a/trytond/model/fields/selection.py b/trytond/model/fields/selection.py
index 230e153..1f75d27 100644
--- a/trytond/model/fields/selection.py
+++ b/trytond/model/fields/selection.py
@@ -4,7 +4,7 @@ import warnings
from sql.conditionals import Case
-from ...config import CONFIG
+from ... import backend
from .field import Field, SQLType
@@ -44,7 +44,7 @@ class Selection(Field):
__init__.__doc__ += Field.__init__.__doc__
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'mysql':
return SQLType('CHAR', 'VARCHAR(255)')
return SQLType('VARCHAR', 'VARCHAR')
@@ -63,3 +63,29 @@ class Selection(Field):
for key, value in selections:
whens.append((column == key, value))
return [Case(*whens, else_=column)]
+
+ def translated(self, name=None):
+ "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 TranslatedSelection(name)
+
+
+class TranslatedSelection(object):
+ 'A descriptor for translated value of Selection field'
+
+ def __init__(self, name):
+ self.name = name
+
+ def __get__(self, inst, cls):
+ if inst is None:
+ return self
+ selection = dict(cls.fields_get([self.name])[self.name]['selection'])
+ value = getattr(inst, self.name)
+ # None and '' are equivalent
+ if value is None or value == '':
+ if value not in selection:
+ value = {None: '', '': None}[value]
+ return selection[value]
diff --git a/trytond/model/fields/sha.py b/trytond/model/fields/sha.py
index 0209337..222aca8 100644
--- a/trytond/model/fields/sha.py
+++ b/trytond/model/fields/sha.py
@@ -3,7 +3,7 @@
import hashlib
from sql import Query, Expression
-from ...config import CONFIG
+from ... import backend
from .field import SQLType
from .char import Char
@@ -22,7 +22,7 @@ class Sha(Char):
return super(Sha, self).sql_format(value)
def sql_type(self):
- db_type = CONFIG['db_type']
+ db_type = backend.name()
if db_type == 'mysql':
return SQLType('CHAR', 'VARCHAR(40)')
return SQLType('VARCHAR', 'VARCHAR(40)')
diff --git a/trytond/model/match.py b/trytond/model/match.py
new file mode 100644
index 0000000..cb82f19
--- /dev/null
+++ b/trytond/model/match.py
@@ -0,0 +1,19 @@
+# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
+# repository contains the full copyright notices and license terms.
+
+
+class MatchMixin(object):
+
+ def match(self, pattern):
+ '''Match on pattern
+ pattern is a dictionary with model field as key
+ and matching value as value'''
+ for field, pattern_value in pattern.iteritems():
+ value = getattr(self, field)
+ if value is None:
+ continue
+ if self._fields[field]._type == 'many2one':
+ value = value.id
+ if value != pattern_value:
+ return False
+ return True
diff --git a/trytond/model/model.py b/trytond/model/model.py
index 8458697..13e8554 100644
--- a/trytond/model/model.py
+++ b/trytond/model/model.py
@@ -4,6 +4,7 @@
import copy
import collections
import warnings
+from functools import total_ordering
from trytond.model import fields
from trytond.error import WarningErrorMixin
@@ -16,6 +17,7 @@ from trytond.rpc import RPC
__all__ = ['Model']
+ at total_ordering
class Model(WarningErrorMixin, URLMixin, PoolBase):
"""
Define a model in Tryton.
@@ -30,6 +32,7 @@ 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),
}
@@ -47,7 +50,14 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
if not isinstance(getattr(cls, attr), fields.Field):
continue
field_name = attr
- field = copy.deepcopy(getattr(cls, field_name))
+ field = getattr(cls, field_name)
+ # Copy the original field definition to prevent side-effect with
+ # the mutable attributes
+ for parent_cls in cls.__mro__:
+ parent_field = getattr(parent_cls, field_name, None)
+ if isinstance(parent_field, fields.Field):
+ field = parent_field
+ field = copy.deepcopy(field)
setattr(cls, field_name, field)
for attribute in ('on_change', 'on_change_with', 'autocomplete',
@@ -60,8 +70,14 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
continue
else:
function_name = '%s_%s' % (attribute, field_name)
- function = getattr(cls, function_name, None)
- if function:
+ 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)
@@ -157,12 +173,10 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
Translation.register_error_messages(cls, module_name)
@classmethod
- def default_get(cls, fields_names, with_rec_name=True,
- with_on_change=True):
+ def default_get(cls, fields_names, with_rec_name=True):
'''
Return a dict with the default values for each field in fields_names.
If with_rec_name is True, rec_name will be added.
- If with_on_change is True, on_change will be added.
'''
pool = Pool()
Property = pool.get('ir.property')
@@ -186,8 +200,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
value[field_name + '.rec_name'] = Target(
value[field_name]).rec_name
- if with_on_change:
- value = cls._default_on_change(value)
if not with_rec_name:
for field in value.keys():
if field.endswith('.rec_name'):
@@ -195,29 +207,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
return value
@classmethod
- def _default_on_change(cls, value):
- """
- Call on_change function for the default value
- and return new default value
- """
- pool = Pool()
- res = value.copy()
- val = {}
- for field in value.keys():
- if field in cls._fields:
- if cls._fields[field].on_change:
- inst = cls()
- for fname in cls._fields[field].on_change:
- setattr(inst, fname, value.get(fname))
- val.update(getattr(inst, 'on_change_' + field)())
- if cls._fields[field]._type in ('one2many',):
- Target = pool.get(cls._fields[field].model_name)
- for val2 in res[field]:
- val2.update(Target._default_on_change(val2))
- res.update(val)
- return res
-
- @classmethod
def fields_get(cls, fields_names=None):
"""
Return the definition of each field on the model.
@@ -365,12 +354,15 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
cls._fields[field].field)
if res[field]['type'] == 'many2one':
target = cls._fields[field].get_target()
+ relation_fields = []
for target_name, target_field in target._fields.iteritems():
if (target_field._type == 'one2many'
and target_field.model_name == cls.__name__
and target_field.field == field):
- res[field]['relation_field'] = target_name
- break
+ relation_fields.append(target_name)
+ # Set relation_field only if there is no ambiguity
+ if len(relation_fields) == 1:
+ res[field]['relation_field'], = relation_fields
if res[field]['type'] in ('datetime', 'time'):
res[field]['format'] = copy.copy(cls._fields[field].format)
if res[field]['type'] == 'selection':
@@ -399,6 +391,14 @@ 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:
@@ -480,11 +480,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
return NotImplemented
return self.id < other.id
- # TODO: replace by total_ordering when 2.6 will be dropped
- __gt__ = lambda self, other: not (self < other or self == other)
- __le__ = lambda self, other: self < other or self == other
- __ge__ = lambda self, other: not self < other
-
def __ne__(self, other):
if not isinstance(other, Model):
return NotImplemented
diff --git a/trytond/model/modelsingleton.py b/trytond/model/modelsingleton.py
index f9ff034..e1347f6 100644
--- a/trytond/model/modelsingleton.py
+++ b/trytond/model/modelsingleton.py
@@ -83,13 +83,12 @@ class ModelSingleton(ModelStorage):
return res
@classmethod
- def default_get(cls, fields_names, with_rec_name=True,
- with_on_change=True):
+ def default_get(cls, fields_names, with_rec_name=True):
if '_timestamp' in fields_names:
fields_names = list(fields_names)
fields_names.remove('_timestamp')
default = super(ModelSingleton, cls).default_get(fields_names,
- with_rec_name=with_rec_name, with_on_change=with_on_change)
+ with_rec_name=with_rec_name)
singleton = cls.get_singleton()
if singleton:
if with_rec_name:
diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py
index 58b7dc7..4f3cf43 100644
--- a/trytond/model/modelsql.py
+++ b/trytond/model/modelsql.py
@@ -3,7 +3,7 @@
import re
import datetime
from functools import reduce
-from itertools import islice, izip, chain
+from itertools import islice, izip, chain, ifilter
from sql import Table, Column, Literal, Desc, Asc, Expression, Flavor
from sql.functions import Now, Extract
@@ -14,7 +14,7 @@ from sql.aggregate import Count, Max
from trytond.model import ModelStorage, ModelView
from trytond.model import fields
from trytond import backend
-from trytond.tools import reduce_ids
+from trytond.tools import reduce_ids, grouped_slice
from trytond.const import OPERATORS, RECORD_CACHE_SIZE
from trytond.transaction import Transaction
from trytond.pool import Pool
@@ -139,8 +139,7 @@ class ModelSQL(ModelStorage):
if isinstance(field, fields.Many2One) \
and field.model_name == cls.__name__ \
and field.left and field.right:
- with Transaction().set_user(0):
- cls._rebuild_tree(field_name, None, 0)
+ cls._rebuild_tree(field_name, None, 0)
for ident, constraint, _ in cls._sql_constraints:
table.add_constraint(ident, constraint)
@@ -189,10 +188,6 @@ class ModelSQL(ModelStorage):
@staticmethod
def table_query():
- '''
- Return None if the model is a real table in the database
- or return a tuple with the SQL query and the arguments.
- '''
return None
@classmethod
@@ -246,9 +241,7 @@ class ModelSQL(ModelStorage):
table = cls.__table_history__()
user = User.__table__()
revisions = []
- in_max = cursor.IN_MAX
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids):
where = reduce_ids(table.id, sub_ids)
cursor.execute(*table.join(user, 'LEFT',
Coalesce(table.write_uid, table.create_uid) == user.id)
@@ -272,7 +265,6 @@ class ModelSQL(ModelStorage):
def __insert_history(cls, ids, deleted=False):
transaction = Transaction()
cursor = transaction.cursor
- in_max = cursor.IN_MAX
if not cls._history:
return
user = transaction.user
@@ -293,8 +285,7 @@ class ModelSQL(ModelStorage):
continue
columns.append(Column(table, fname))
hcolumns.append(Column(history, fname))
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids):
if not deleted:
where = reduce_ids(table.id, sub_ids)
cursor.execute(*history.insert(hcolumns,
@@ -310,14 +301,13 @@ class ModelSQL(ModelStorage):
return
transaction = Transaction()
cursor = transaction.cursor
- in_max = cursor.IN_MAX
table = cls.__table__()
history = cls.__table_history__()
columns = []
hcolumns = []
- for fname, field in sorted(cls._fields.iteritems()):
- if hasattr(field, 'set'):
- continue
+ fnames = sorted(n for n, f in cls._fields.iteritems()
+ if not hasattr(f, 'set'))
+ for fname in fnames:
columns.append(Column(table, fname))
if fname == 'write_uid':
hcolumns.append(Literal(transaction.user))
@@ -326,16 +316,20 @@ class ModelSQL(ModelStorage):
else:
hcolumns.append(Column(history, fname))
+ def is_deleted(values):
+ return all(not v for n, v in zip(fnames, values)
+ if n not in ['id', 'write_uid', 'write_date'])
+
to_delete = []
to_update = []
for id_ in ids:
column_datetime = Coalesce(history.write_date, history.create_date)
hwhere = (column_datetime <= datetime) & (history.id == id_)
- horder = column_datetime.desc
+ horder = (column_datetime.desc, Column(history, '__id').desc)
cursor.execute(*history.select(*hcolumns,
where=hwhere, order_by=horder, limit=1))
values = cursor.fetchone()
- if not values:
+ if not values or is_deleted(values):
to_delete.append(id_)
else:
to_update.append(id_)
@@ -351,8 +345,7 @@ class ModelSQL(ModelStorage):
cursor.execute(*table.insert(columns, [values]))
if to_delete:
- for i in range(0, len(to_delete), in_max):
- sub_ids = to_delete[i:i + in_max]
+ for sub_ids in grouped_slice(to_delete):
where = reduce_ids(table.id, sub_ids)
cursor.execute(*table.delete(where=where))
cls.__insert_history(to_delete, True)
@@ -363,12 +356,10 @@ class ModelSQL(ModelStorage):
def __check_timestamp(cls, ids):
transaction = Transaction()
cursor = transaction.cursor
- in_max = cursor.IN_MAX
table = cls.__table__()
if not transaction.timestamp:
return
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids):
where = Or()
for id_ in sub_ids:
try:
@@ -394,7 +385,6 @@ class ModelSQL(ModelStorage):
cursor = transaction.cursor
pool = Pool()
Translation = pool.get('ir.translation')
- in_max = cursor.IN_MAX
super(ModelSQL, cls).create(vlist)
@@ -422,8 +412,7 @@ class ModelSQL(ModelStorage):
default.append(f)
if default:
- defaults = cls.default_get(default, with_rec_name=False,
- with_on_change=False)
+ defaults = cls.default_get(default, with_rec_name=False)
values.update(cls._clean_defaults(defaults))
insert_columns = [table.create_uid, table.create_date]
@@ -455,15 +444,15 @@ class ModelSQL(ModelStorage):
new_ids.append(id_new)
except DatabaseIntegrityError, exception:
with Transaction().new_cursor(), \
- Transaction().set_user(0):
+ Transaction().set_context(_check_access=False):
cls.__raise_integrity_error(exception, values)
raise
domain = pool.get('ir.rule').domain_get(cls.__name__,
mode='create')
if domain:
- for i in range(0, len(new_ids), in_max):
- sub_ids = new_ids[i:i + in_max]
+ for sub_ids in grouped_slice(new_ids):
+ sub_ids = list(sub_ids)
red_sql = reduce_ids(table.id, sub_ids)
cursor.execute(*table.select(table.id,
@@ -499,8 +488,8 @@ class ModelSQL(ModelStorage):
cls.__insert_history(new_ids)
records = cls.browse(new_ids)
- for i in range(0, len(records), RECORD_CACHE_SIZE):
- cls._validate(records[i:i + RECORD_CACHE_SIZE])
+ for sub_records in grouped_slice(records, RECORD_CACHE_SIZE):
+ cls._validate(sub_records)
field_names = cls._fields.keys()
cls._update_mptt(field_names, [new_ids] * len(field_names))
@@ -575,8 +564,8 @@ class ModelSQL(ModelStorage):
if 'id' not in fields_names:
columns.append(table.id.as_('id'))
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids, in_max):
+ sub_ids = list(sub_ids)
red_sql = reduce_ids(table.id, sub_ids)
where = red_sql
if history_clause:
@@ -745,7 +734,6 @@ class ModelSQL(ModelStorage):
pool = Pool()
Translation = pool.get('ir.translation')
Config = pool.get('ir.configuration')
- in_max = cursor.IN_MAX
assert not len(args) % 2
all_records = sum(((records, values) + args)[0:None:2], [])
@@ -787,8 +775,8 @@ class ModelSQL(ModelStorage):
update_values.append(field.sql_format(value))
domain = pool.get('ir.rule').domain_get(cls.__name__, mode='write')
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids):
+ sub_ids = list(sub_ids)
red_sql = reduce_ids(table.id, sub_ids)
where = red_sql
if domain:
@@ -811,7 +799,7 @@ class ModelSQL(ModelStorage):
where=red_sql))
except DatabaseIntegrityError, exception:
with Transaction().new_cursor(), \
- Transaction().set_user(0):
+ Transaction().set_context(_check_access=False):
cls.__raise_integrity_error(exception, values,
values.keys())
raise
@@ -826,18 +814,17 @@ class ModelSQL(ModelStorage):
if hasattr(field, 'set'):
fields_to_set.setdefault(fname, []).extend((ids, value))
- field_names = cls._fields.keys()
+ field_names = values.keys()
cls._update_mptt(field_names, [ids] * len(field_names), values)
- all_field_names |= set(values.keys())
+ all_field_names |= set(field_names)
for fname, fargs in fields_to_set.iteritems():
field = cls._fields[fname]
field.set(cls, fname, *fargs)
cls.__insert_history(all_ids)
- for i in range(0, len(all_records), RECORD_CACHE_SIZE):
- cls._validate(all_records[i:i + RECORD_CACHE_SIZE],
- field_names=all_field_names)
+ for sub_records in grouped_slice(all_records, RECORD_CACHE_SIZE):
+ cls._validate(sub_records, field_names=all_field_names)
cls.trigger_write(trigger_eligibles)
@classmethod
@@ -848,7 +835,6 @@ class ModelSQL(ModelStorage):
pool = Pool()
Translation = pool.get('ir.translation')
ids = map(int, records)
- in_max = cursor.IN_MAX
if not ids:
return
@@ -871,8 +857,7 @@ class ModelSQL(ModelStorage):
and field.model_name == cls.__name__
and field.left and field.right):
tree_ids[fname] = []
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids):
where = reduce_ids(Column(table, fname), sub_ids)
cursor.execute(*table.select(table.id, where=where))
tree_ids[fname] += [x[0] for x in cursor.fetchall()]
@@ -903,8 +888,8 @@ class ModelSQL(ModelStorage):
domain = pool.get('ir.rule').domain_get(cls.__name__, mode='delete')
if domain:
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids):
+ sub_ids = list(sub_ids)
red_sql = reduce_ids(table.id, sub_ids)
cursor.execute(*table.select(table.id,
where=red_sql & table.id.in_(domain)))
@@ -916,9 +901,9 @@ class ModelSQL(ModelStorage):
cls.trigger_delete(records)
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
- sub_records = records[i:i + in_max]
+ for sub_ids, sub_records in izip(
+ grouped_slice(ids), grouped_slice(records)):
+ sub_ids = list(sub_ids)
red_sql = reduce_ids(table.id, sub_ids)
transaction.delete_records.setdefault(cls.__name__,
@@ -953,7 +938,7 @@ class ModelSQL(ModelStorage):
Model.delete(models)
for Model, field_name in foreign_keys_tocheck:
- with Transaction().set_user(0):
+ with Transaction().set_context(_check_access=False):
if Model.search([
(field_name, 'in', sub_ids),
], order=[]):
@@ -961,7 +946,7 @@ class ModelSQL(ModelStorage):
cls.raise_user_error('foreign_model_exist',
error_args=error_args)
- super(ModelSQL, cls).delete(sub_records)
+ super(ModelSQL, cls).delete(list(sub_records))
try:
cursor.execute(*table.delete(where=red_sql))
@@ -983,7 +968,6 @@ class ModelSQL(ModelStorage):
Rule = pool.get('ir.rule')
transaction = Transaction()
cursor = transaction.cursor
- in_max = cursor.IN_MAX
# Get domain clauses
tables, expression = cls.search_domain(domain)
@@ -1034,6 +1018,7 @@ class ModelSQL(ModelStorage):
columns.append(Coalesce(
main_table.write_date,
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()
@@ -1053,11 +1038,50 @@ class ModelSQL(ModelStorage):
cursor.execute(*select)
rows = cursor.dictfetchmany(cursor.IN_MAX)
- cache = cursor.get_cache(transaction.context)
+ cache = cursor.get_cache()
if cls.__name__ not in cache:
cache[cls.__name__] = LRUDict(RECORD_CACHE_SIZE)
delete_records = transaction.delete_records.setdefault(cls.__name__,
set())
+
+ def filter_history(rows):
+ if not (cls._history and transaction.context.get('_datetime')):
+ return rows
+
+ def history_key(row):
+ return row['_datetime'], row['__id']
+
+ ids_history = {}
+ for row in rows:
+ key = history_key(row)
+ if row['id'] in ids_history:
+ if key < ids_history[row['id']]:
+ continue
+ ids_history[row['id']] = key
+
+ to_delete = set()
+ history = cls.__table_history__()
+ for sub_ids in grouped_slice([r['id'] for r in rows]):
+ 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
+ <= transaction.context['_datetime'])))
+ for deleted_id, delete_date in cursor.fetchall():
+ history_date, _ = ids_history[deleted_id]
+ if isinstance(history_date, basestring):
+ strptime = datetime.datetime.strptime
+ format_ = '%Y-%m-%d %H:%M:%S.%f'
+ history_date = strptime(history_date, format_)
+ if history_date <= delete_date:
+ to_delete.add(deleted_id)
+
+ return ifilter(lambda r: history_key(r) == ids_history[r['id']]
+ and r['id'] not in to_delete, rows)
+
+ rows = list(filter_history(rows))
keys = None
for data in islice(rows, 0, cache.size_limit):
if data['id'] in delete_records:
@@ -1065,7 +1089,7 @@ class ModelSQL(ModelStorage):
if keys is None:
keys = data.keys()
for k in keys[:]:
- if k in ('_timestamp', '_datetime'):
+ if k in ('_timestamp', '_datetime', '__id'):
keys.remove(k)
continue
field = cls._fields[k]
@@ -1086,34 +1110,7 @@ class ModelSQL(ModelStorage):
cursor.execute(*table.select(*columns,
where=expression, order_by=order_by,
limit=limit, offset=offset))
- rows = cursor.dictfetchall()
-
- if cls._history and transaction.context.get('_datetime'):
- ids = []
- ids_date = {}
- for data in rows:
- if data['id'] in ids_date:
- if data['_datetime'] <= ids_date[data['id']]:
- continue
- if data['id'] in ids:
- ids.remove(data['id'])
- ids.append(data['id'])
- ids_date[data['id']] = data['_datetime']
- to_delete = set()
- history = cls.__table_history__()
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
- 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
- <= transaction.context['_datetime'])))
- for deleted_id, delete_date in cursor.fetchall():
- if ids_date[deleted_id] < delete_date:
- to_delete.add(deleted_id)
- return cls.browse(filter(lambda x: x not in to_delete, ids))
+ rows = filter_history(cursor.dictfetchall())
return cls.browse([x['id'] for x in rows])
@@ -1184,8 +1181,7 @@ class ModelSQL(ModelStorage):
cls._update_tree(id_, field_name,
field.left, field.right)
else:
- with Transaction().set_user(0):
- cls._rebuild_tree(field_name, None, 0)
+ cls._rebuild_tree(field_name, None, 0)
@classmethod
def _rebuild_tree(cls, parent, parent_id, left):
@@ -1291,8 +1287,7 @@ class ModelSQL(ModelStorage):
sql_clause = '(id != ' + param + ' AND ' + sql_clause + ')'
in_max = cursor.IN_MAX / (len(columns) + 1)
- for i in range(0, len(ids), in_max):
- sub_ids = ids[i:i + in_max]
+ for sub_ids in grouped_slice(ids, in_max):
red_sql = reduce_ids(table.id, sub_ids)
cursor.execute('SELECT id,' + sql + ' '
@@ -1312,8 +1307,7 @@ class ModelSQL(ModelStorage):
match = _RE_CHECK.match(sql)
if match:
sql = match.group(1)
- for i in range(0, len(ids), cursor.IN_MAX):
- sub_ids = ids[i:i + cursor.IN_MAX]
+ for sub_ids in grouped_slice(ids):
red_sql = reduce_ids(table.id, sub_ids)
cursor.execute('SELECT id '
'FROM "' + cls._table + '" '
diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py
index 6698c98..232b038 100644
--- a/trytond/model/modelstorage.py
+++ b/trytond/model/modelstorage.py
@@ -14,16 +14,17 @@ from decimal import Decimal
from itertools import islice, ifilter, chain, izip
from functools import reduce
from operator import itemgetter
+from collections import defaultdict
from trytond.model import Model
from trytond.model import fields
-from trytond.tools import safe_eval, reduce_domain, memoize
+from trytond.tools import reduce_domain, memoize
from trytond.pyson import PYSONEncoder, PYSONDecoder, PYSON
from trytond.const import OPERATORS, RECORD_CACHE_SIZE, BROWSE_FIELD_TRESHOLD
from trytond.transaction import Transaction
from trytond.pool import Pool
-from trytond.cache import LRUDict
-from trytond.config import CONFIG
+from trytond.cache import LRUDict, freeze
+from trytond import backend
from trytond.rpc import RPC
from .modelview import ModelView
@@ -451,6 +452,9 @@ class ModelStorage(Model):
if not isinstance(value, ModelStorage):
break
field_name = fields_tree[i]
+ descriptor = None
+ if '.' in field_name:
+ field_name, descriptor = field_name.split('.')
eModel = pool.get(value.__name__)
field = eModel._fields[field_name]
if field.states and 'invisible' in field.states:
@@ -466,7 +470,10 @@ class ModelStorage(Model):
if invisible:
value = ''
break
- value = getattr(value, field_name)
+ if descriptor:
+ value = getattr(field, descriptor)().__get__(value, eModel)
+ else:
+ value = getattr(value, field_name)
if isinstance(value, (list, tuple)):
first = True
child_fields_names = [(x[:i + 1] == fields_tree[:i + 1] and
@@ -746,7 +753,7 @@ class ModelStorage(Model):
# Allow root user to update/delete
if Transaction().user == 0:
return True
- with Transaction().set_user(0):
+ with Transaction().set_context(_check_access=False):
models_data = ModelData.search([
('model', '=', cls.__name__),
('db_id', 'in', map(int, records)),
@@ -758,10 +765,7 @@ class ModelStorage(Model):
for model_data in models_data:
if not model_data.values:
continue
- xml_values = safe_eval(model_data.values, {
- 'Decimal': Decimal,
- 'datetime': datetime,
- })
+ xml_values = ModelData.load_values(model_data.values)
for key, val in values.iteritems():
if key in xml_values and val != xml_values[key]:
return False
@@ -839,7 +843,7 @@ class ModelStorage(Model):
def _validate(cls, records, field_names=None):
pool = Pool()
# Ensure that records are readable
- with Transaction().set_user(0, set_context=True):
+ with Transaction().set_context(_check_access=False):
records = cls.browse(records)
def call(name):
@@ -893,6 +897,7 @@ class ModelStorage(Model):
Relation = field.get_target()
else:
Relation = cls
+ domains = defaultdict(list)
if is_pyson(field.domain):
pyson_domain = PYSONEncoder().encode(field.domain)
for record in records:
@@ -902,12 +907,13 @@ class ModelStorage(Model):
env['time'] = time
env['context'] = Transaction().context
env['active_id'] = record.id
- domain = PYSONDecoder(env).decode(pyson_domain)
- validate_relation_domain(
- field, [record], Relation, domain)
+ domain = freeze(PYSONDecoder(env).decode(pyson_domain))
+ domains[domain].append(record)
else:
- validate_relation_domain(
- field, records, Relation, field.domain)
+ domains[freeze(field.domain)].extend(records)
+
+ for domain, sub_records in domains.iteritems():
+ validate_relation_domain(field, sub_records, Relation, domain)
def validate_relation_domain(field, records, Relation, domain):
if field._type in ('many2one', 'one2many', 'many2many', 'one2one'):
@@ -1003,7 +1009,7 @@ class ModelStorage(Model):
def raise_user_error(value):
error_args = cls._get_error_args(field_name)
error_args['digits'] = digits[1]
- error_args['value'] = value
+ error_args['value'] = repr(value)
cls.raise_user_error('digits_validation_record',
error_args=error_args)
if value is None:
@@ -1012,7 +1018,7 @@ class ModelStorage(Model):
if (value.quantize(Decimal(str(10.0 ** -digits[1])))
!= value):
raise_user_error(value)
- elif CONFIG.options['db_type'] != 'mysql':
+ elif backend.name() != 'mysql':
if not (round(value, digits[1]) == float(value)):
raise_user_error(value)
# validate digits
@@ -1138,7 +1144,7 @@ class ModelStorage(Model):
else:
self._ids = [id]
- self._cursor_cache = self._cursor.get_cache(self._context)
+ self._cursor_cache = self._cursor.get_cache()
if _local_cache is not None:
self._local_cache = _local_cache
@@ -1223,6 +1229,14 @@ class ModelStorage(Model):
datetime_field = self._fields[field.datetime_field]
ffields[field.datetime_field] = datetime_field
+ # add depends of field with context
+ for field in ffields.values():
+ if field.context:
+ for context_field_name in field.depends:
+ context_field = self._fields.get(context_field_name)
+ if context_field not in ffields:
+ ffields[context_field_name] = context_field
+
def filter_(id_):
return (name not in self._cache.get(id_, {})
and name not in self._local_cache.get(id_, {}))
@@ -1258,14 +1272,18 @@ class ModelStorage(Model):
except KeyError:
return value
ctx = {}
+ if field.context:
+ pyson_context = PYSONEncoder().encode(field.context)
+ ctx.update(PYSONDecoder(data).decode(pyson_context))
datetime_ = None
if getattr(field, 'datetime_field', None):
datetime_ = data.get(field.datetime_field)
ctx = {'_datetime': datetime_}
with Transaction().set_context(**ctx):
- local_cache = model2cache.setdefault((Model, datetime_),
+ key = (Model, freeze(ctx))
+ local_cache = model2cache.setdefault(key,
LRUDict(RECORD_CACHE_SIZE))
- ids = model2ids.setdefault((Model, datetime_), [])
+ ids = model2ids.setdefault(key, [])
if field._type in ('many2one', 'one2one', 'reference'):
ids.append(value)
return Model(value, _ids=ids, _local_cache=local_cache)
@@ -1301,13 +1319,14 @@ class ModelStorage(Model):
if data['id'] == self.id and fname == name:
value = fvalue
if (field._type not in ('many2one', 'one2one', 'one2many',
- 'many2many', 'reference')
+ 'many2many', 'reference', 'binary')
and not isinstance(field, fields.Function)):
continue
if data['id'] not in self._local_cache:
self._local_cache[data['id']] = {}
self._local_cache[data['id']][fname] = fvalue
if (field._type not in ('many2one', 'reference')
+ or field.context
or getattr(field, 'datetime_field', None)
or isinstance(field, fields.Function)):
del data[fname]
diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py
index ae853e2..643bcba 100644
--- a/trytond/model/modelview.py
+++ b/trytond/model/modelview.py
@@ -253,7 +253,7 @@ class ModelView(Model):
- relate: a list of available relations
"""
Action = Pool().get('ir.action.keyword')
- key = (cls.__name__, repr(Transaction().context))
+ key = cls.__name__
result = cls._view_toolbar_get_cache.get(key)
if result:
return result
@@ -426,7 +426,9 @@ class ModelView(Model):
# convert attributes into pyson
encoder = PYSONEncoder()
for attr in ('states', 'domain', 'spell', 'colors'):
- if element.get(attr):
+ 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)))
@@ -476,17 +478,22 @@ class ModelView(Model):
@wraps(func)
def wrapper(cls, *args, **kwargs):
pool = Pool()
+ ModelAccess = pool.get('ir.model.access')
Button = pool.get('ir.model.button')
User = pool.get('res.user')
- if Transaction().user != 0:
+ if ((Transaction().user != 0)
+ and Transaction().context.get('_check_access')):
+ ModelAccess.check(cls.__name__, 'read')
+ ModelAccess.check(cls.__name__, 'write')
groups = set(User.get_groups())
button_groups = Button.get_groups(cls.__name__,
func.__name__)
if button_groups and not groups & button_groups:
raise UserError('Calling button %s on %s is not allowed!'
% (func.__name__, cls.__name__))
- return func(cls, *args, **kwargs)
+ with Transaction().set_context(_check_access=False):
+ return func(cls, *args, **kwargs)
return wrapper
@staticmethod
diff --git a/trytond/model/union.py b/trytond/model/union.py
new file mode 100644
index 0000000..b65f1ad
--- /dev/null
+++ b/trytond/model/union.py
@@ -0,0 +1,67 @@
+# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
+# repository contains the full copyright notices and license terms.
+from sql import Union, Column, Literal, Cast
+
+from trytond.model import fields
+from trytond.pool import Pool
+
+
+class UnionMixin:
+ 'Mixin to combine models'
+
+ @staticmethod
+ def union_models():
+ return []
+
+ @classmethod
+ def union_shard(cls, column, model):
+ models = cls.union_models()
+ length = len(models)
+ i = models.index(model)
+ return ((column * length) + i)
+
+ @classmethod
+ def union_unshard(cls, record_id):
+ pool = Pool()
+ models = cls.union_models()
+ length = len(models)
+ record_id, i = divmod(record_id, length)
+ Model = pool.get(models[i])
+ return Model(record_id)
+
+ @classmethod
+ def union_column(cls, name, field, table, Model):
+ column = Literal(None)
+ union_field = Model._fields.get(name)
+ if union_field:
+ column = Column(table, union_field.name)
+ if (isinstance(field, fields.Many2One)
+ and field.model_name == cls.__name__):
+ target_model = union_field.model_name
+ if target_model in cls.union_models():
+ column = cls.union_shard(column, target_model)
+ else:
+ column = Literal(None)
+ return column
+
+ @classmethod
+ def union_columns(cls, model):
+ pool = Pool()
+ Model = pool.get(model)
+ table = Model.__table__()
+ columns = [cls.union_shard(table.id, model).as_('id')]
+ for name in sorted(cls._fields.keys()):
+ field = cls._fields[name]
+ if name == 'id' or hasattr(field, 'set'):
+ continue
+ column = cls.union_column(name, field, table, Model)
+ columns.append(Cast(column, field.sql_type().base).as_(name))
+ return table, columns
+
+ @classmethod
+ def table_query(cls):
+ queries = []
+ for model in cls.union_models():
+ table, columns = cls.union_columns(model)
+ queries.append(table.select(*columns))
+ return Union(*queries)
diff --git a/trytond/modules/__init__.py b/trytond/modules/__init__.py
index e7ee406..7c233b5 100644
--- a/trytond/modules/__init__.py
+++ b/trytond/modules/__init__.py
@@ -14,7 +14,7 @@ from sql import Table
from sql.functions import Now
import trytond.tools as tools
-from trytond.config import CONFIG
+from trytond.config import config
from trytond.transaction import Transaction
from trytond.cache import Cache
import trytond.convert as convert
@@ -135,11 +135,11 @@ class Node(Singleton):
def get_module_info(name):
"Return the content of the tryton.cfg"
- config = ConfigParser.ConfigParser()
+ module_config = ConfigParser.ConfigParser()
with tools.file_open(os.path.join(name, 'tryton.cfg')) as fp:
- config.readfp(fp)
+ module_config.readfp(fp)
directory = os.path.dirname(fp.name)
- info = dict(config.items('tryton'))
+ info = dict(module_config.items('tryton'))
info['directory'] = directory
for key in ('depends', 'extras_depend', 'xml'):
if key in info:
@@ -152,11 +152,7 @@ def create_graph(module_list):
packages = []
for module in module_list:
- try:
- info = get_module_info(module)
- except IOError:
- if module != 'all':
- raise Exception('Module %s not found' % module)
+ info = get_module_info(module)
packages.append((module, info.get('depends', []),
info.get('extras_depend', []), info))
@@ -193,18 +189,17 @@ def create_graph(module_list):
return graph, packages, later
-def is_module_to_install(module):
- for kind in ('init', 'update'):
- if 'all' in CONFIG[kind] and module != 'tests':
- return True
- elif module in CONFIG[kind]:
- return True
+def is_module_to_install(module, update):
+ if module in update:
+ return True
return False
-def load_module_graph(graph, pool, lang=None):
+def load_module_graph(graph, pool, update=None, lang=None):
if lang is None:
- lang = [CONFIG['language']]
+ lang = [config.get('database', 'language')]
+ if update is None:
+ update = []
modules_todo = []
models_to_update_history = set()
logger = logging.getLogger('modules')
@@ -222,7 +217,7 @@ def load_module_graph(graph, pool, lang=None):
logger.info(module)
classes = pool.setup(module)
package_state = module2state.get(module, 'uninstalled')
- if (is_module_to_install(module)
+ if (is_module_to_install(module, update)
or package_state in ('to install', 'to upgrade')):
if package_state not in ('to install', 'to upgrade'):
if package_state == 'installed':
@@ -362,7 +357,7 @@ def register_classes():
MODULES.append(module)
-def load_modules(database_name, pool, update=False, lang=None):
+def load_modules(database_name, pool, update=None, lang=None):
res = True
def _load_modules():
@@ -372,29 +367,20 @@ def load_modules(database_name, pool, update=False, lang=None):
# Migration from 2.2: workflow module removed
cursor.execute(*ir_module.delete(
where=(ir_module.name == 'workflow')))
- if 'all' in CONFIG['init']:
- cursor.execute(*ir_module.select(ir_module.name,
- where=(ir_module.name != 'tests')))
- else:
- cursor.execute(*ir_module.select(ir_module.name,
- where=ir_module.state.in_(('installed', 'to install',
- 'to upgrade', 'to remove'))))
+ cursor.execute(*ir_module.select(ir_module.name,
+ where=ir_module.state.in_(('installed', 'to install',
+ 'to upgrade', 'to remove'))))
else:
cursor.execute(*ir_module.select(ir_module.name,
where=ir_module.state.in_(('installed', 'to upgrade',
'to remove'))))
module_list = [name for (name,) in cursor.fetchall()]
if update:
- for module in CONFIG['init'].keys():
- if CONFIG['init'][module]:
- module_list.append(module)
- for module in CONFIG['update'].keys():
- if CONFIG['update'][module]:
- module_list.append(module)
+ module_list += update
graph = create_graph(module_list)[0]
try:
- load_module_graph(graph, pool, lang)
+ load_module_graph(graph, pool, update, lang)
except Exception:
cursor.rollback()
raise
@@ -423,6 +409,7 @@ def load_modules(database_name, pool, update=False, lang=None):
Module = pool.get('ir.module.module')
Module.update_list()
cursor.commit()
+ Cache.resets(database_name)
if not Transaction().cursor:
with Transaction().start(database_name, 0):
@@ -433,5 +420,4 @@ def load_modules(database_name, pool, update=False, lang=None):
Transaction().reset_context():
_load_modules()
- Cache.resets(database_name)
return res
diff --git a/trytond/monitor.py b/trytond/monitor.py
index b067ca0..128a0bf 100644
--- a/trytond/monitor.py
+++ b/trytond/monitor.py
@@ -32,14 +32,17 @@ def _modified(path):
return False
-def monitor():
+def monitor(files):
'''
- Monitor module files for change
+ Monitor files and module files for change
:return: True if at least one file has changed
'''
global _MODULES
modified = False
+ for file_ in files:
+ if _modified(file_):
+ modified = True
directories = set()
for module in sys.modules.keys():
if not module.startswith('trytond.'):
diff --git a/trytond/pool.py b/trytond/pool.py
index f57f3dc..65c6f49 100644
--- a/trytond/pool.py
+++ b/trytond/pool.py
@@ -128,7 +128,7 @@ class Pool(object):
'''
return self._locks[self.database_name]
- def init(self, update=False, lang=None):
+ def init(self, update=None, lang=None):
'''
Init pool
Set update to proceed to update
diff --git a/trytond/protocols/dispatcher.py b/trytond/protocols/dispatcher.py
index 2d1915d..2ba1d10 100644
--- a/trytond/protocols/dispatcher.py
+++ b/trytond/protocols/dispatcher.py
@@ -1,18 +1,17 @@
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
-import traceback
import logging
import time
import sys
import pydoc
-from sql import Table, Flavor
+from sql import Table
from trytond.pool import Pool
from trytond import security
from trytond import backend
-from trytond.config import CONFIG
+from trytond.config import config
from trytond.version import VERSION
from trytond.transaction import Transaction
from trytond.cache import Cache
@@ -20,10 +19,11 @@ from trytond.exceptions import UserError, UserWarning, NotLogged, \
ConcurrencyException
from trytond.rpc import RPC
+logger = logging.getLogger(__name__)
ir_configuration = Table('ir_configuration')
ir_lang = Table('ir_lang')
-ir_module = Table('ir_module')
+ir_module = Table('ir_module_module')
res_user = Table('res_user')
@@ -40,16 +40,15 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
except Exception:
return False
res = security.login(database_name, user, session)
- Cache.clean(database_name)
- logger = logging.getLogger('dispatcher')
+ with Transaction().start(database_name, 0):
+ 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))
- Cache.resets(database_name)
return res or False
elif method == 'logout':
name = security.logout(database_name, user, session)
- logger = logging.getLogger('dispatcher')
logger.info(('logout \'%s\' from %s:%d '
'using %s on database \'%s\'')
% (name, host, port, protocol, database_name))
@@ -64,6 +63,7 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
('de_DE', 'Deutsch'),
('en_US', 'English'),
('es_AR', 'Español (Argentina)'),
+ ('es_EC', 'Español (Ecuador)'),
('es_ES', 'Español (España)'),
('es_CO', 'Español (Colombia)'),
('fr_FR', 'Français'),
@@ -81,7 +81,7 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
except Exception:
return False
elif method == 'list':
- if CONFIG['prevent_dblist']:
+ if not config.get('database', 'list'):
raise Exception('AccessDenied')
with Transaction().start(None, 0, close=True) as transaction:
return transaction.database.list(transaction.cursor)
@@ -121,7 +121,7 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
obj = pool.get(object_name, type=object_type)
return pydoc.getdoc(getattr(obj, method))
- for count in range(int(CONFIG['retry']), -1, -1):
+ for count in range(config.getint('database', 'retry'), -1, -1):
try:
user = security.check(database_name, user, session)
except DatabaseOperationalError:
@@ -130,7 +130,6 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
raise
break
- Cache.clean(database_name)
database_list = Pool.database_list()
pool = Pool(database_name)
if not database_name in database_list:
@@ -147,9 +146,13 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
raise UserError('Calling method %s on %s %s is not allowed!'
% (method, object_type, object_name))
- for count in range(int(CONFIG['retry']), -1, -1):
+ exception_message = ('Exception calling %s.%s.%s from %s@%s:%d/%s' %
+ (object_type, object_name, method, user, host, port, database_name))
+
+ for count in range(config.getint('database', 'retry'), -1, -1):
with Transaction().start(database_name, user,
readonly=rpc.readonly) as transaction:
+ Cache.clean(database_name)
try:
c_args, c_kwargs, transaction.context, transaction.timestamp \
= rpc.convert(obj, *args, **kwargs)
@@ -166,23 +169,20 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
for i in inst]
if not rpc.readonly:
transaction.cursor.commit()
- except DatabaseOperationalError, exception:
+ except DatabaseOperationalError:
transaction.cursor.rollback()
if count and not rpc.readonly:
continue
raise
- except Exception, exception:
- if CONFIG['verbose'] and not isinstance(exception, (
- NotLogged, ConcurrencyException, UserError,
- UserWarning)):
- tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
- logger = logging.getLogger('dispatcher')
- logger.error('Exception calling method %s on '
- '%s %s from %s@%s:%d/%s:\n'
- % (method, object_type, object_name, user, host, port,
- database_name) + tb_s)
+ except (NotLogged, ConcurrencyException, UserError, UserWarning):
+ logger.debug(exception_message, exc_info=sys.exc_info())
transaction.cursor.rollback()
raise
+ except Exception:
+ logger.error(exception_message, exc_info=sys.exc_info())
+ transaction.cursor.rollback()
+ raise
+ Cache.resets(database_name)
with Transaction().start(database_name, 0) as transaction:
pool = Pool(database_name)
Session = pool.get('ir.session')
@@ -193,7 +193,6 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
transaction.cursor.rollback()
else:
transaction.cursor.commit()
- Cache.resets(database_name)
return result
@@ -210,7 +209,6 @@ def create(database_name, password, lang, admin_password):
Database = backend.get('Database')
security.check_super(password)
res = False
- logger = logging.getLogger('database')
try:
with Transaction().start(None, 0, close=True, autocommit=True) \
@@ -225,7 +223,7 @@ def create(database_name, password, lang, admin_password):
transaction.cursor.commit()
pool = Pool(database_name)
- pool.init(update=True, lang=[lang])
+ pool.init(update=['res', 'ir'], lang=[lang])
with Transaction().start(database_name, 0) as transaction:
User = pool.get('res.user')
Lang = pool.get('ir.lang')
@@ -246,9 +244,8 @@ def create(database_name, password, lang, admin_password):
transaction.cursor.commit()
res = True
except Exception:
- logger.error('CREATE DB: %s failed' % (database_name,))
- tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
- logger.error('Exception in call: \n' + tb_s)
+ logger.error('CREATE DB: %s failed' % database_name,
+ exc_info=sys.exc_info())
raise
else:
logger.info('CREATE DB: %s' % (database_name,))
@@ -261,7 +258,6 @@ def drop(database_name, password):
Database(database_name).close()
# Sleep to let connections close
time.sleep(1)
- logger = logging.getLogger('database')
with Transaction().start(None, 0, close=True, autocommit=True) \
as transaction:
@@ -270,9 +266,8 @@ def drop(database_name, password):
Database.drop(cursor, database_name)
cursor.commit()
except Exception:
- logger.error('DROP DB: %s failed' % (database_name,))
- tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
- logger.error('Exception in call: \n' + tb_s)
+ logger.error('DROP DB: %s failed' % database_name,
+ exc_info=sys.exc_info())
raise
else:
logger.info('DROP DB: %s' % (database_name))
@@ -286,7 +281,6 @@ def dump(database_name, password):
Database(database_name).close()
# Sleep to let connections close
time.sleep(1)
- logger = logging.getLogger('database')
data = Database.dump(database_name)
logger.info('DUMP DB: %s' % (database_name))
@@ -295,7 +289,6 @@ def dump(database_name, password):
def restore(database_name, password, data, update=False):
Database = backend.get('Database')
- logger = logging.getLogger('database')
security.check_super(password)
try:
database = Database().connect()
@@ -307,14 +300,14 @@ def restore(database_name, password, data, update=False):
Database.restore(database_name, data)
logger.info('RESTORE DB: %s' % (database_name))
if update:
- cursor = Database(database_name).connect().cursor()
- cursor.execute(*ir_lang.select(ir_lang.code,
- where=ir_lang.translatable))
- lang = [x[0] for x in cursor.fetchall()]
- cursor.execute(*ir_module.update([ir_module.state], ['to upgrade'],
- where=(ir_module.state == 'installed')))
- cursor.commit()
- cursor.close()
+ with Transaction().start(database_name, 0) as transaction:
+ cursor = transaction.cursor
+ cursor.execute(*ir_lang.select(ir_lang.code,
+ where=ir_lang.translatable))
+ lang = [x[0] for x in cursor.fetchall()]
+ cursor.execute(*ir_module.select(ir_module.name,
+ where=(ir_module.state == 'installed')))
+ update = [x[0] for x in cursor.fetchall()]
Pool(database_name).init(update=update, lang=lang)
logger.info('Update/Init succeed!')
return True
diff --git a/trytond/protocols/jsonrpc.py b/trytond/protocols/jsonrpc.py
index 27ce605..088d7da 100644
--- a/trytond/protocols/jsonrpc.py
+++ b/trytond/protocols/jsonrpc.py
@@ -2,7 +2,7 @@
#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
+from trytond.config import config
from trytond.protocols.common import daemon, RegisterHandlerMixin
from trytond.exceptions import UserError, UserWarning, NotLogged, \
ConcurrencyException
@@ -33,63 +33,88 @@ except ImportError:
from StringIO import StringIO
-def object_hook(dct):
- if '__class__' in dct:
- if dct['__class__'] == 'datetime':
- return datetime.datetime(dct['year'], dct['month'], dct['day'],
- dct['hour'], dct['minute'], dct['second'], dct['microsecond'])
- elif dct['__class__'] == 'date':
- return datetime.date(dct['year'], dct['month'], dct['day'])
- elif dct['__class__'] == 'time':
- return datetime.time(dct['hour'], dct['minute'], dct['second'],
- dct['microsecond'])
- elif dct['__class__'] == 'buffer':
- return buffer(base64.decodestring(dct['base64']))
- elif dct['__class__'] == 'Decimal':
- return Decimal(dct['decimal'])
- return dct
+class JSONDecoder(object):
+
+ decoders = {}
+
+ @classmethod
+ def register(cls, klass, decoder):
+ assert klass not in cls.decoders
+ cls.decoders[klass] = decoder
+
+ def __call__(self, dct):
+ if dct.get('__class__') in self.decoders:
+ return self.decoders[dct['__class__']](dct)
+ return dct
+
+JSONDecoder.register('datetime',
+ lambda dct: datetime.datetime(dct['year'], dct['month'], dct['day'],
+ dct['hour'], dct['minute'], dct['second'], dct['microsecond']))
+JSONDecoder.register('date',
+ lambda dct: datetime.date(dct['year'], dct['month'], dct['day']))
+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('Decimal', lambda dct: Decimal(dct['decimal']))
class JSONEncoder(json.JSONEncoder):
+ serializers = {}
+
def __init__(self, *args, **kwargs):
super(JSONEncoder, self).__init__(*args, **kwargs)
# Force to use our custom decimal with simplejson
self.use_decimal = False
+ @classmethod
+ def register(cls, klass, encoder):
+ assert klass not in cls.serializers
+ cls.serializers[klass] = encoder
+
def default(self, obj):
- if isinstance(obj, datetime.date):
- if isinstance(obj, datetime.datetime):
- return {'__class__': 'datetime',
- 'year': obj.year,
- 'month': obj.month,
- 'day': obj.day,
- 'hour': obj.hour,
- 'minute': obj.minute,
- 'second': obj.second,
- 'microsecond': obj.microsecond,
- }
- return {'__class__': 'date',
- 'year': obj.year,
- 'month': obj.month,
- 'day': obj.day,
- }
- elif isinstance(obj, datetime.time):
- return {'__class__': 'time',
- 'hour': obj.hour,
- 'minute': obj.minute,
- 'second': obj.second,
- 'microsecond': obj.microsecond,
- }
- elif isinstance(obj, buffer):
- return {'__class__': 'buffer',
- 'base64': base64.encodestring(obj),
- }
- elif isinstance(obj, Decimal):
- return {'__class__': 'Decimal',
- 'decimal': str(obj),
- }
- return super(JSONEncoder, self).default(obj)
+ marshaller = self.serializers.get(type(obj),
+ super(JSONEncoder, self).default)
+ return marshaller(obj)
+
+JSONEncoder.register(datetime.datetime,
+ lambda o: {
+ '__class__': 'datetime',
+ 'year': o.year,
+ 'month': o.month,
+ 'day': o.day,
+ 'hour': o.hour,
+ 'minute': o.minute,
+ 'second': o.second,
+ 'microsecond': o.microsecond,
+ })
+JSONEncoder.register(datetime.date,
+ lambda o: {
+ '__class__': 'date',
+ 'year': o.year,
+ 'month': o.month,
+ 'day': o.day,
+ })
+JSONEncoder.register(datetime.time,
+ lambda o: {
+ '__class__': 'time',
+ 'hour': o.hour,
+ 'minute': o.minute,
+ 'second': o.second,
+ 'microsecond': o.microsecond,
+ })
+JSONEncoder.register(buffer,
+ lambda o: {
+ '__class__': 'buffer',
+ 'base64': base64.encodestring(o),
+ })
+JSONEncoder.register(Decimal,
+ lambda o: {
+ '__class__': 'Decimal',
+ 'decimal': str(o),
+ })
class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
@@ -111,7 +136,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
existing method through subclassing is the prefered means
of changing method dispatch behavior.
"""
- rawreq = json.loads(data, object_hook=object_hook)
+ rawreq = json.loads(data, object_hook=JSONDecoder())
req_id = rawreq.get('id', 0)
method = rawreq['method']
@@ -132,10 +157,6 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
for path in sys.path:
tb_s = tb_s.replace(path, '')
- if CONFIG['debug_mode']:
- import pdb
- traceb = sys.exc_info()[2]
- pdb.post_mortem(traceb)
# report exception back to server
response['error'] = (str(sys.exc_value), tb_s)
@@ -204,7 +225,7 @@ class SimpleJSONRPCRequestHandler(RegisterHandlerMixin,
path = posixpath.normpath(urllib.unquote(path))
words = path.split('/')
words = filter(None, words)
- path = CONFIG['jsondata_path']
+ path = config.get('jsonrpc', 'data')
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
@@ -222,7 +243,8 @@ class SimpleJSONRPCRequestHandler(RegisterHandlerMixin,
def send_tryton_url(self, path):
self.send_response(300)
- hostname = CONFIG['hostname'] or unicode(socket.getfqdn(), 'utf8')
+ hostname = (config.get('jsonrpc', 'hostname')
+ or unicode(socket.getfqdn(), 'utf8'))
hostname = '.'.join(encodings.idna.ToASCII(part) for part in
hostname.split('.'))
values = {
diff --git a/trytond/protocols/sslsocket.py b/trytond/protocols/sslsocket.py
index c3dcbdb..9c18a26 100644
--- a/trytond/protocols/sslsocket.py
+++ b/trytond/protocols/sslsocket.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.config import CONFIG
+from trytond.config import config
def SSLSocket(socket):
@@ -8,6 +8,6 @@ def SSLSocket(socket):
import ssl
return ssl.wrap_socket(socket,
server_side=True,
- certfile=CONFIG['certificate'],
- keyfile=CONFIG['privatekey'],
+ certfile=config.get('ssl', 'certificate'),
+ keyfile=config.get('ssl', 'privatekey'),
ssl_version=ssl.PROTOCOL_SSLv23)
diff --git a/trytond/protocols/webdav.py b/trytond/protocols/webdav.py
index 98b12ff..f820280 100644
--- a/trytond/protocols/webdav.py
+++ b/trytond/protocols/webdav.py
@@ -7,7 +7,6 @@ import urlparse
import time
import urllib
import sys
-import traceback
import logging
from threading import local
import xml.dom.minidom
@@ -21,7 +20,6 @@ from pywebdav.lib.davcmd import copyone, copytree, moveone, movetree, \
delone, deltree
from trytond.protocols.sslsocket import SSLSocket
from trytond.protocols.common import daemon
-from trytond.config import CONFIG
from trytond.security import login
from trytond.version import PACKAGE, VERSION, WEBSITE
from trytond.tools.misc import LocalDict
@@ -36,6 +34,8 @@ domimpl = xml.dom.minidom.getDOMImplementation()
DAV_VERSION_1['version'] += ',access-control'
DAV_VERSION_2['version'] += ',access-control'
+logger = logging.getLogger(__name__)
+
# Local int for multi-thread
class LocalInt(local):
@@ -131,12 +131,12 @@ class TrytonDAVInterface(iface.dav_interface):
self.verbose = False
def _log_exception(self, exception):
- if CONFIG['verbose'] and not isinstance(exception, (
- NotLogged, ConcurrencyException, UserError, UserWarning,
- DAV_Error, DAV_NotFound, DAV_Secret, DAV_Forbidden)):
- tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
- logger = logging.getLogger('webdav')
- logger.error('Exception:\n' + tb_s)
+ if isinstance(exception, (NotLogged, ConcurrencyException, UserError,
+ UserWarning, DAV_Error, DAV_NotFound, DAV_Secret,
+ DAV_Forbidden)):
+ logger.debug('Exception', exc_info=sys.exc_info())
+ else:
+ logger.error('Exception', exc_info=sys.exc_info())
@staticmethod
def get_dburi(uri):
@@ -581,7 +581,9 @@ class WebDAVAuthRequestHandler(WebDAVServer.DAVRequestHandler):
if not user:
return None
- Transaction().start(dbname, user)
+ Transaction().start(dbname, user, {
+ '_check_access': True,
+ })
Cache.clean(dbname)
return user
diff --git a/trytond/protocols/xmlrpc.py b/trytond/protocols/xmlrpc.py
index a5bbe2c..7b48c53 100644
--- a/trytond/protocols/xmlrpc.py
+++ b/trytond/protocols/xmlrpc.py
@@ -2,7 +2,6 @@
#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
from trytond.protocols.common import daemon, RegisterHandlerMixin
from trytond.exceptions import UserError, UserWarning, NotLogged, \
ConcurrencyException
@@ -10,16 +9,18 @@ from trytond import security
import SimpleXMLRPCServer
import SocketServer
import xmlrpclib
-import traceback
import socket
import sys
import base64
import datetime
from types import DictType
+import logging
# convert decimal to float before marshalling:
from decimal import Decimal
+logger = logging.getLogger(__name__)
+
def dump_decimal(self, value, write):
value = {'__class__': 'Decimal',
@@ -74,6 +75,28 @@ def dump_struct(self, value, write, escape=xmlrpclib.escape):
xmlrpclib.Marshaller.dispatch[DictType] = dump_struct
+class XMLRPCDecoder(object):
+
+ decoders = {}
+
+ @classmethod
+ def register(cls, klass, decoder):
+ assert klass not in cls.decoders
+ cls.decoders[klass] = decoder
+
+ def __call__(self, dct):
+ if dct.get('__class__') in self.decoders:
+ return self.decoders[dct['__class__']](dct)
+ return dct
+
+XMLRPCDecoder.register('date',
+ lambda dct: datetime.date(dct['year'], dct['month'], dct['day']))
+XMLRPCDecoder.register('time',
+ lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
+ dct['microsecond']))
+XMLRPCDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
+
+
def end_struct(self, data):
mark = self._marks.pop()
# map structs to Python dictionaries
@@ -81,14 +104,7 @@ def end_struct(self, data):
items = self._stack[mark:]
for i in range(0, len(items), 2):
dct[xmlrpclib._stringify(items[i])] = items[i + 1]
- if '__class__' in dct:
- if dct['__class__'] == 'date':
- dct = datetime.date(dct['year'], dct['month'], dct['day'])
- elif dct['__class__'] == 'time':
- dct = datetime.time(dct['hour'], dct['minute'], dct['second'],
- dct['microsecond'])
- elif dct['__class__'] == 'Decimal':
- dct = Decimal(dct['decimal'])
+ dct = XMLRPCDecoder()(dct)
self._stack[mark:] = [dct]
self._value = 0
@@ -103,6 +119,14 @@ def _end_dateTime(self, data):
xmlrpclib.Unmarshaller.dispatch["dateTime.iso8601"] = _end_dateTime
+def _end_base64(self, data):
+ value = xmlrpclib.Binary()
+ value.decode(data)
+ self.append(buffer(value.data))
+ self._value = 0
+xmlrpclib.Unmarshaller.dispatch['base64'] = _end_base64
+
+
class GenericXMLRPCRequestHandler:
def _dispatch(self, method, params):
@@ -110,6 +134,7 @@ class GenericXMLRPCRequestHandler:
database_name = self.path[1:]
user = self.tryton['user']
session = self.tryton['session']
+ exception_message = 'Exception calling %s%s' % (method, params)
try:
try:
method_list = method.split('.')
@@ -127,21 +152,15 @@ class GenericXMLRPCRequestHandler:
return dispatch(host, port, 'XML-RPC', database_name, user,
session, object_type, object_name, method, *params)
except (NotLogged, ConcurrencyException), exception:
- raise xmlrpclib.Fault(exception.code,
- '\n'.join(exception.args))
+ logger.debug(exception_message, exc_info=sys.exc_info())
+ raise xmlrpclib.Fault(exception.code, str(exception))
except (UserError, UserWarning), exception:
+ logger.debug(exception_message, exc_info=sys.exc_info())
error, description = exception.args
- raise xmlrpclib.Fault(exception.code,
- '\n'.join((error,) + description))
- except Exception:
- tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
- for path in sys.path:
- tb_s = tb_s.replace(path, '')
- if CONFIG['debug_mode']:
- import pdb
- traceb = sys.exc_info()[2]
- pdb.post_mortem(traceb)
- raise xmlrpclib.Fault(255, str(sys.exc_value) + '\n' + tb_s)
+ raise xmlrpclib.Fault(exception.code, str(exception))
+ except Exception, exception:
+ logger.error(exception_message, exc_info=sys.exc_info())
+ raise xmlrpclib.Fault(255, str(exception))
finally:
security.logout(database_name, user, session)
diff --git a/trytond/pyson.py b/trytond/pyson.py
index 6771f20..bab351c 100644
--- a/trytond/pyson.py
+++ b/trytond/pyson.py
@@ -28,25 +28,25 @@ class PYSON(object):
return Not(self)
def __and__(self, other):
+ if (isinstance(other, PYSON)
+ and other.types() != set([bool])):
+ other = Bool(other)
if (isinstance(self, And)
and not isinstance(self, Or)):
self._statements.append(other)
return self
- if (isinstance(other, PYSON)
- and other.types() != set([bool])):
- other = Bool(other)
if self.types() != set([bool]):
return And(Bool(self), other)
else:
return And(self, other)
def __or__(self, other):
- if isinstance(self, Or):
- self._statements.append(other)
- return self
if (isinstance(other, PYSON)
and other.types() != set([bool])):
other = Bool(other)
+ if isinstance(self, Or):
+ self._statements.append(other)
+ return self
if self.types() != set([bool]):
return Or(Bool(self), other)
else:
diff --git a/trytond/report/report.py b/trytond/report/report.py
index 42a6d5d..b0a3ef1 100644
--- a/trytond/report/report.py
+++ b/trytond/report/report.py
@@ -21,11 +21,12 @@ except ImportError:
Manifest, MANIFEST = None, None
from genshi.filters import Translator
import lxml.etree
-from trytond.config import CONFIG
+from trytond.config import config
from trytond.pool import Pool, PoolBase
from trytond.transaction import Transaction
from trytond.url import URLMixin
from trytond.rpc import RPC
+from trytond.exceptions import UserError
MIMETYPES = {
'odt': 'application/vnd.oasis.opendocument.text',
@@ -102,6 +103,20 @@ class Report(URLMixin, PoolBase):
}
@classmethod
+ def check_access(cls):
+ pool = Pool()
+ ActionReport = pool.get('ir.action.report')
+ User = pool.get('res.user')
+
+ if Transaction().user == 0:
+ return
+
+ groups = set(User.get_groups())
+ report_groups = ActionReport.get_groups(cls.__name__)
+ if report_groups and not groups & report_groups:
+ raise UserError('Calling report %s is not allowed!' % cls.__name__)
+
+ @classmethod
def execute(cls, ids, data):
'''
Execute the report on record ids.
@@ -120,6 +135,7 @@ class Report(URLMixin, PoolBase):
])
if not action_reports:
raise Exception('Error', 'Report (%s) not find!' % cls.__name__)
+ cls.check_access()
action_report = action_reports[0]
records = None
model = action_report.model or data.get('model')
@@ -295,7 +311,7 @@ class Report(URLMixin, PoolBase):
oext = FORMAT2EXT.get(output_format, output_format)
with os.fdopen(fd, 'wb+') as fp:
fp.write(data)
- cmd = ['unoconv', '--connection=%s' % CONFIG['unoconv'],
+ cmd = ['unoconv', '--connection=%s' % config.get('report', 'unoconv'),
'-f', oext, '--stdout', path]
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
diff --git a/trytond/res/group.py b/trytond/res/group.py
index 800b13d..e24f631 100644
--- a/trytond/res/group.py
+++ b/trytond/res/group.py
@@ -3,8 +3,8 @@
"Group"
from itertools import chain
from ..model import ModelView, ModelSQL, fields
-from ..transaction import Transaction
from ..pool import Pool, PoolMeta
+from ..tools import grouped_slice
__all__ = [
'Group', 'Group2',
@@ -19,8 +19,7 @@ class MenuMany2Many(fields.Many2Many):
values=values)
menu_ids = list(set(chain(*res.values())))
test_ids = []
- for i in range(0, len(menu_ids), Transaction().cursor.IN_MAX):
- sub_ids = menu_ids[i:i + Transaction().cursor.IN_MAX]
+ for sub_ids in grouped_slice(menu_ids):
test_ids.append(map(int, Menu.search([
('id', 'in', sub_ids),
])))
diff --git a/trytond/res/ir.xml b/trytond/res/ir.xml
index 73c8d96..041e090 100644
--- a/trytond/res/ir.xml
+++ b/trytond/res/ir.xml
@@ -265,14 +265,6 @@ 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="True"/>
- <field name="perm_delete" eval="True"/>
- </record>
<record model="ir.model.access" id="access_ir_cron">
<field name="model" search="[('model', '=', 'ir.cron')]"/>
<field name="perm_read" eval="True"/>
@@ -746,6 +738,16 @@ this repository contains the full copyright notices and license terms. -->
<field name="group" ref="group_admin"/>
</record>
+ <record model="ir.model.button" id="model_data_sync_button">
+ <field name="name">sync</field>
+ <field name="model" search="[('model', '=', 'ir.model.data')]"/>
+ </record>
+ <record model="ir.model.button-res.group"
+ id="model_data_sync_button_group_admin">
+ <field name="button" ref="model_data_sync_button"/>
+ <field name="group" ref="group_admin"/>
+ </record>
+
<record model="ir.ui.view" id="sequence_type_view_form">
<field name="model">ir.sequence.type</field>
<field name="inherit" ref="ir.sequence_type_view_form"/>
diff --git a/trytond/res/locale/ca_ES.po b/trytond/res/locale/ca_ES.po
index 71decc4..25cdfb9 100644
--- a/trytond/res/locale/ca_ES.po
+++ b/trytond/res/locale/ca_ES.po
@@ -350,7 +350,7 @@ msgstr "Direcció idioma"
msgctxt "field:res.user,login:"
msgid "Login"
-msgstr "Nom d'usuari"
+msgstr "Nom usuari"
msgctxt "field:res.user,menu:"
msgid "Menu Action"
@@ -486,7 +486,7 @@ msgstr "ID"
msgctxt "field:res.user.login.attempt,login:"
msgid "Login"
-msgstr "Nom d'usuari"
+msgstr "Nom usuari"
msgctxt "field:res.user.login.attempt,rec_name:"
msgid "Name"
diff --git a/trytond/res/locale/de_DE.po b/trytond/res/locale/de_DE.po
index 7b1e739..40fed63 100644
--- a/trytond/res/locale/de_DE.po
+++ b/trytond/res/locale/de_DE.po
@@ -739,4 +739,4 @@ msgstr "Hinzufügen"
msgctxt "wizard_button:res.user.config,user,end:"
msgid "End"
-msgstr "Ende"
+msgstr "Fertig"
diff --git a/trytond/res/locale/es_AR.po b/trytond/res/locale/es_AR.po
index f889836..71897ae 100644
--- a/trytond/res/locale/es_AR.po
+++ b/trytond/res/locale/es_AR.po
@@ -102,7 +102,7 @@ msgstr "Usuario creación"
msgctxt "field:ir.model.field-res.group,field:"
msgid "Model Field"
-msgstr "Model de Campo"
+msgstr "Campo del modelo"
msgctxt "field:ir.model.field-res.group,group:"
msgid "Group"
@@ -562,11 +562,11 @@ msgstr "Acción - Grupo"
msgctxt "model:ir.cron,name:cron_trigger_time"
msgid "Run On Time Triggers"
-msgstr "Ejecutar Disparadores Al Tiempo"
+msgstr "Ejecutar disparadores por tiempo"
msgctxt "model:ir.model.button-res.group,name:"
msgid "Model Button - Group"
-msgstr "Modelo de Botón - Grupo"
+msgstr "Modelo Botón - Grupo"
msgctxt "model:ir.model.field-res.group,name:"
msgid "Model Field Group Rel"
@@ -574,11 +574,11 @@ msgstr "Relación entre grupo y campo del modelo"
msgctxt "model:ir.rule.group-res.group,name:"
msgid "Rule Group - Group"
-msgstr "Regla de grupo - grupo"
+msgstr "Regla de grupo - Grupo"
msgctxt "model:ir.rule.group-res.user,name:"
msgid "Rule Group - User"
-msgstr "Regla de grupo - usuario"
+msgstr "Regla de grupo - Usuario"
msgctxt "model:ir.sequence.type-res.group,name:"
msgid "Sequence Type - Group"
@@ -618,7 +618,7 @@ msgstr "Administrador"
msgctxt "model:res.user,name:user_trigger"
msgid "Cron Trigger"
-msgstr "Disparador del Programador de tareas"
+msgstr "Programador de disparadores"
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 "Inicio de Configuración de Usuario"
+msgstr "Configuración inicial de Usuario"
msgctxt "model:res.user.login.attempt,name:"
msgid "Login Attempt"
diff --git a/trytond/res/locale/es_CO.po b/trytond/res/locale/es_CO.po
index b351bbe..4f741c0 100644
--- a/trytond/res/locale/es_CO.po
+++ b/trytond/res/locale/es_CO.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 "Contraseña incorrecta!"
msgctxt "error:res.user:"
msgid "You can not have two users with the same login!"
@@ -394,7 +394,7 @@ msgstr "Barra de Estado"
msgctxt "field:res.user,warnings:"
msgid "Warnings"
-msgstr "Avisos"
+msgstr "Advertencias"
msgctxt "field:res.user,write_date:"
msgid "Write Date"
@@ -478,7 +478,7 @@ 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"
@@ -618,11 +618,11 @@ 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"
-msgstr "res.user-ir.action"
+msgstr "Usuario - Acción"
msgctxt "model:res.user-res.group,name:"
msgid "User - Group"
@@ -638,7 +638,7 @@ msgstr "Intento de Login"
msgctxt "model:res.user.warning,name:"
msgid "User Warning"
-msgstr "Aviso a Usuario"
+msgstr "Advertencia a Usuario"
msgctxt "view:ir.module.module:"
msgid "Cancel Installation"
diff --git a/trytond/res/locale/es_CO.po b/trytond/res/locale/es_EC.po
similarity index 95%
copy from trytond/res/locale/es_CO.po
copy to trytond/res/locale/es_EC.po
index b351bbe..bdae57a 100644
--- a/trytond/res/locale/es_CO.po
+++ b/trytond/res/locale/es_EC.po
@@ -4,7 +4,7 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:res.group:"
msgid "The name of the group must be unique!"
-msgstr "El nombre del grupo debe ser único!"
+msgstr "¡El nombre del grupo debe ser único!"
msgctxt "error:res.user:"
msgid "Wrong password!"
@@ -12,7 +12,7 @@ msgstr "¡Contraseña incorrecta!"
msgctxt "error:res.user:"
msgid "You can not have two users with the same login!"
-msgstr "No puede tener dos usuarios con el mismo nombre de usuario!"
+msgstr "¡No puede tener dos usuarios con el mismo nombre de usuario!"
msgctxt "error:res.user:"
msgid ""
@@ -102,7 +102,7 @@ msgstr "Creado por Usuario"
msgctxt "field:ir.model.field-res.group,field:"
msgid "Model Field"
-msgstr "Modelo de Campo"
+msgstr "Campo del Modelo"
msgctxt "field:ir.model.field-res.group,group:"
msgid "Group"
@@ -146,7 +146,7 @@ msgstr "Nombre"
msgctxt "field:ir.rule.group-res.group,rule_group:"
msgid "Rule Group"
-msgstr "Regla de Grupo"
+msgstr "Grupo de Reglas"
msgctxt "field:ir.rule.group-res.group,write_date:"
msgid "Write Date"
@@ -174,7 +174,7 @@ msgstr "Nombre"
msgctxt "field:ir.rule.group-res.user,rule_group:"
msgid "Rule Group"
-msgstr "Regla de Grupo"
+msgstr "Grupo de Reglas"
msgctxt "field:ir.rule.group-res.user,user:"
msgid "User"
@@ -366,7 +366,7 @@ msgstr "Contraseña"
msgctxt "field:res.user,password_hash:"
msgid "Password Hash"
-msgstr "Contraseña Hash"
+msgstr "Hash de Contraseña"
msgctxt "field:res.user,pyson_menu:"
msgid "PySON Menu"
@@ -394,7 +394,7 @@ msgstr "Barra de Estado"
msgctxt "field:res.user,warnings:"
msgid "Warnings"
-msgstr "Avisos"
+msgstr "Advertencias"
msgctxt "field:res.user,write_date:"
msgid "Write Date"
@@ -538,11 +538,11 @@ 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 secuencias en este tipo"
+msgstr "Grupos autorizados para editar las secuencias de este tipo"
msgctxt "help:res.user,actions:"
msgid "Actions that will be run at login"
-msgstr "Acciones que se ejecutan al iniciar sesión"
+msgstr "Acciones que se ejecutarán al iniciar sesión"
msgctxt "model:ir.action,name:act_group_form"
msgid "Groups"
@@ -566,7 +566,7 @@ msgstr "Ejecutar Disparadores a Tiempo"
msgctxt "model:ir.model.button-res.group,name:"
msgid "Model Button - Group"
-msgstr "Modelo de Botón - Grupo"
+msgstr "Botón de Modelo - Grupo"
msgctxt "model:ir.model.field-res.group,name:"
msgid "Model Field Group Rel"
@@ -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 "Regla de Grupo - Grupo"
+msgstr "Grupo de Reglas - Grupo"
msgctxt "model:ir.rule.group-res.user,name:"
msgid "Rule Group - User"
-msgstr "Regla de Grupo - Usuario"
+msgstr "Grupo de Reglas - Usuario"
msgctxt "model:ir.sequence.type-res.group,name:"
msgid "Sequence Type - Group"
-msgstr "Secuencia de Tipo - Grupo"
+msgstr "Tipo de Secuencia - Grupo"
msgctxt "model:ir.ui.menu,name:menu_group_form"
msgid "Groups"
@@ -622,7 +622,7 @@ msgstr "Disparador del Programador de tareas"
msgctxt "model:res.user-ir.action,name:"
msgid "User - Action"
-msgstr "res.user-ir.action"
+msgstr "Usuario - Acción"
msgctxt "model:res.user-res.group,name:"
msgid "User - Group"
@@ -630,15 +630,15 @@ msgstr "Usuario - Grupo"
msgctxt "model:res.user.config.start,name:"
msgid "User Config Init"
-msgstr "Inicio de Configuración de Usuario"
+msgstr "Configuración Inicial de Usuario"
msgctxt "model:res.user.login.attempt,name:"
msgid "Login Attempt"
-msgstr "Intento de Login"
+msgstr "Intento de Inicio de Sesión"
msgctxt "model:res.user.warning,name:"
msgid "User Warning"
-msgstr "Aviso a Usuario"
+msgstr "Advertencia al Usuario"
msgctxt "view:ir.module.module:"
msgid "Cancel Installation"
@@ -654,15 +654,15 @@ msgstr "Cancelar Actualización"
msgctxt "view:ir.module.module:"
msgid "Mark for Installation"
-msgstr "Instalar"
+msgstr "Marcar para Instalar"
msgctxt "view:ir.module.module:"
msgid "Mark for Uninstallation (beta)"
-msgstr "Desinstalar (beta)"
+msgstr "Marcar para Desinstalar (beta)"
msgctxt "view:ir.module.module:"
msgid "Mark for Upgrade"
-msgstr "Actualizar"
+msgstr "Marcar para Actualizar"
msgctxt "view:res.group:"
msgid "Access Permissions"
@@ -686,7 +686,7 @@ msgstr "Agregar Usuarios"
msgctxt "view:res.user.config.start:"
msgid "Be careful that the login must be unique!"
-msgstr "Asegúrese de que los nombres de usuario sean únicos!"
+msgstr "¡Asegúrese de que el nombre de usuario sea único!"
msgctxt "view:res.user.config.start:"
msgid "You can now add some users into the system."
diff --git a/trytond/res/locale/es_ES.po b/trytond/res/locale/es_ES.po
index 7ed0da6..18997e6 100644
--- a/trytond/res/locale/es_ES.po
+++ b/trytond/res/locale/es_ES.po
@@ -350,7 +350,7 @@ msgstr "Dirección del idioma"
msgctxt "field:res.user,login:"
msgid "Login"
-msgstr "Nombre de usuario"
+msgstr "Nombre usuario"
msgctxt "field:res.user,menu:"
msgid "Menu Action"
@@ -486,7 +486,7 @@ msgstr "ID"
msgctxt "field:res.user.login.attempt,login:"
msgid "Login"
-msgstr "Nombre de usuario"
+msgstr "Nombre usuario"
msgctxt "field:res.user.login.attempt,rec_name:"
msgid "Name"
diff --git a/trytond/res/locale/fr_FR.po b/trytond/res/locale/fr_FR.po
index 758ba94..7ed7195 100644
--- a/trytond/res/locale/fr_FR.po
+++ b/trytond/res/locale/fr_FR.po
@@ -4,15 +4,15 @@ msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "error:res.group:"
msgid "The name of the group must be unique!"
-msgstr "Le nom du groupe doit être unique !"
+msgstr "Le nom du groupe doit être unique !"
msgctxt "error:res.user:"
msgid "Wrong password!"
-msgstr "Mauvais mot de passe !"
+msgstr "Mauvais mot de passe !"
msgctxt "error:res.user:"
msgid "You can not have two users with the same login!"
-msgstr "Vous ne pouvez pas créer deux utilisateur avec le même identifiant !"
+msgstr "Vous ne pouvez pas créer deux utilisateur avec le même identifiant !"
msgctxt "error:res.user:"
msgid ""
@@ -330,7 +330,7 @@ msgstr "Créé par"
msgctxt "field:res.user,email:"
msgid "Email"
-msgstr "E-mail"
+msgstr "Email"
msgctxt "field:res.user,groups:"
msgid "Groups"
@@ -562,7 +562,7 @@ msgstr "Action - Groupe"
msgctxt "model:ir.cron,name:cron_trigger_time"
msgid "Run On Time Triggers"
-msgstr "Lance les déclencheurs \"À temps\""
+msgstr "Lance les déclencheurs « À temps »"
msgctxt "model:ir.model.button-res.group,name:"
msgid "Model Button - Group"
diff --git a/trytond/res/locale/sl_SI.po b/trytond/res/locale/sl_SI.po
index e481897..8b96eb7 100644
--- a/trytond/res/locale/sl_SI.po
+++ b/trytond/res/locale/sl_SI.po
@@ -21,7 +21,7 @@ msgid ""
"created by the system (updates, module installation, ...)"
msgstr ""
"Skrbnika ni možno odstraniti, ker se interno uporablja za vire, \n"
-"ki jih ustvarja sistem (posodobitve, nameščanje modulov, ...)"
+"ki jih izdeluje sistem (posodobitve, nameščanje modulov, ...)"
msgctxt "field:ir.action-res.group,action:"
msgid "Action"
@@ -29,11 +29,11 @@ msgstr "Ukrep"
msgctxt "field:ir.action-res.group,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.action-res.group,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.action-res.group,group:"
msgid "Group"
@@ -65,11 +65,11 @@ msgstr "Gumb"
msgctxt "field:ir.model.button-res.group,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.model.button-res.group,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.model.button-res.group,group:"
msgid "Group"
@@ -93,11 +93,11 @@ msgstr "Zapisal"
msgctxt "field:ir.model.field-res.group,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.model.field-res.group,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.model.field-res.group,field:"
msgid "Model Field"
@@ -125,11 +125,11 @@ msgstr "Zapisal"
msgctxt "field:ir.rule.group-res.group,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.rule.group-res.group,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.rule.group-res.group,group:"
msgid "Group"
@@ -157,11 +157,11 @@ msgstr "Zapisal"
msgctxt "field:ir.rule.group-res.user,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.rule.group-res.user,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.rule.group-res.user,id:"
msgid "ID"
@@ -201,11 +201,11 @@ msgstr "Uporabniške skupine"
msgctxt "field:ir.sequence.type-res.group,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.sequence.type-res.group,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.sequence.type-res.group,group:"
msgid "User Groups"
@@ -233,11 +233,11 @@ msgstr "Zapisal"
msgctxt "field:ir.ui.menu-res.group,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:ir.ui.menu-res.group,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:ir.ui.menu-res.group,group:"
msgid "Group"
@@ -265,11 +265,11 @@ msgstr "Zapisal"
msgctxt "field:res.group,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:res.group,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:res.group,field_access:"
msgid "Access Field"
@@ -321,11 +321,11 @@ msgstr "Aktivno"
msgctxt "field:res.user,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:res.user,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:res.user,email:"
msgid "Email"
@@ -409,11 +409,11 @@ msgstr "Ukrep"
msgctxt "field:res.user-ir.action,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:res.user-ir.action,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:res.user-ir.action,id:"
msgid "ID"
@@ -437,11 +437,11 @@ msgstr "Zapisal"
msgctxt "field:res.user-res.group,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:res.user-res.group,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:res.user-res.group,group:"
msgid "Group"
@@ -473,11 +473,11 @@ msgstr "ID"
msgctxt "field:res.user.login.attempt,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:res.user.login.attempt,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:res.user.login.attempt,id:"
msgid "ID"
@@ -505,11 +505,11 @@ msgstr "Vedno"
msgctxt "field:res.user.warning,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:res.user.warning,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:res.user.warning,id:"
msgid "ID"
diff --git a/trytond/res/user.py b/trytond/res/user.py
index 78bea12..19e1ba8 100644
--- a/trytond/res/user.py
+++ b/trytond/res/user.py
@@ -23,12 +23,12 @@ except ImportError:
from ..model import ModelView, ModelSQL, fields
from ..wizard import Wizard, StateView, Button, StateTransition
-from ..tools import safe_eval
+from ..tools import grouped_slice
from .. import backend
from ..transaction import Transaction
from ..cache import Cache
from ..pool import Pool
-from ..config import CONFIG
+from ..config import config
from ..pyson import PYSONEncoder
from ..rpc import RPC
@@ -77,9 +77,9 @@ class User(ModelSQL, ModelView):
def __setup__(cls):
super(User, cls).__setup__()
cls.__rpc__.update({
- 'get_preferences': RPC(),
- 'set_preferences': RPC(readonly=False),
- 'get_preferences_fields_view': RPC(),
+ 'get_preferences': RPC(check_access=False),
+ 'set_preferences': RPC(readonly=False, check_access=False),
+ 'get_preferences_fields_view': RPC(check_access=False),
})
cls._sql_constraints += [
('login_key', 'UNIQUE (login)',
@@ -198,17 +198,14 @@ class User(ModelSQL, ModelView):
@staticmethod
def get_sessions(users, name):
Session = Pool().get('ir.session')
- cursor = Transaction().cursor
now = datetime.datetime.now()
- timeout = datetime.timedelta(seconds=int(CONFIG['session_timeout']))
+ timeout = datetime.timedelta(
+ seconds=config.getint('session', 'timeout'))
result = dict((u.id, 0) for u in users)
- for i in range(0, len(users), cursor.IN_MAX):
- sub_ids = [u.id for u in users[i:i + cursor.IN_MAX]]
-
- with Transaction().set_user(0):
- sessions = Session.search([
- ('create_uid', 'in', sub_ids),
- ], order=[('create_uid', 'ASC')])
+ 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
@@ -359,8 +356,7 @@ class User(ModelSQL, ModelView):
if preferences is not None:
return preferences.copy()
user = Transaction().user
- with Transaction().set_user(0, set_context=True):
- user = cls(user)
+ user = cls(user)
preferences = cls._get_preferences(user, context_only=context_only)
cls._get_preferences_cache.set(key, preferences)
return preferences.copy()
@@ -375,8 +371,7 @@ class User(ModelSQL, ModelView):
values_clean = values.copy()
fields = cls._preferences_fields + cls._context_fields
user_id = Transaction().user
- with Transaction().set_user(0):
- user = cls(user_id)
+ user = cls(user_id)
for field in values:
if field not in fields or field == 'groups':
del values_clean[field]
@@ -391,8 +386,7 @@ class User(ModelSQL, ModelView):
values_clean['language'] = langs[0].id
else:
del values_clean['language']
- with Transaction().set_user(0):
- cls.write([user], values_clean)
+ cls.write([user], values_clean)
@classmethod
def get_preferences_fields_view(cls):
@@ -558,7 +552,7 @@ class LoginAttempt(ModelSQL):
@staticmethod
def delay():
return (datetime.datetime.now()
- - datetime.timedelta(seconds=int(CONFIG['session_timeout'])))
+ - datetime.timedelta(seconds=config.getint('session', 'timeout')))
@classmethod
def add(cls, login):
diff --git a/trytond/rpc.py b/trytond/rpc.py
index 1e3e2a9..4fc1f7a 100644
--- a/trytond/rpc.py
+++ b/trytond/rpc.py
@@ -10,16 +10,19 @@ class RPC(object):
readonly: The transaction mode
instantiate: The position or the slice of the arguments to be instanciated
result: The function to transform the result
+ check_access: If access right must be checked
'''
- __slots__ = ('readonly', 'instantiate', 'result')
+ __slots__ = ('readonly', 'instantiate', 'result', 'check_access')
- def __init__(self, readonly=True, instantiate=None, result=None):
+ def __init__(self, readonly=True, instantiate=None, result=None,
+ check_access=True):
self.readonly = readonly
self.instantiate = instantiate
if result is None:
result = lambda r: r
self.result = result
+ self.check_access = check_access
def convert(self, obj, *args, **kwargs):
args = list(args)
@@ -32,6 +35,8 @@ class RPC(object):
if '_timestamp' in context:
timestamp = context['_timestamp']
del context['_timestamp']
+ if self.check_access:
+ context['_check_access'] = True
if self.instantiate is not None:
def instance(data):
diff --git a/trytond/security.py b/trytond/security.py
index 7d3e85b..4e92b00 100644
--- a/trytond/security.py
+++ b/trytond/security.py
@@ -1,7 +1,13 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
+import os
+try:
+ import crypt
+except ImportError:
+ pass
+
from trytond.pool import Pool
-from trytond.config import CONFIG
+from trytond.config import config
from trytond.transaction import Transaction
from trytond.exceptions import NotLogged
@@ -48,10 +54,10 @@ def logout(dbname, user, session):
def check_super(passwd):
- if passwd == CONFIG['admin_passwd']:
+ cryptedpasswd = config.get('session', 'super_pwd')
+ if cryptedpasswd and crypt.crypt(passwd, cryptedpasswd) == cryptedpasswd:
return True
- else:
- raise Exception('AccessDenied')
+ raise Exception('AccessDenied')
def check(dbname, user, session):
diff --git a/trytond/server.py b/trytond/server.py
index 720b224..a5df713 100644
--- a/trytond/server.py
+++ b/trytond/server.py
@@ -4,6 +4,7 @@
%prog [options]
"""
import logging
+import logging.config
import logging.handlers
import sys
import os
@@ -12,7 +13,7 @@ import time
from getpass import getpass
import threading
-from trytond.config import CONFIG
+from trytond.config import config, parse_listen
from trytond import backend
from trytond.pool import Pool
from trytond.monitor import monitor
@@ -22,59 +23,37 @@ from .transaction import Transaction
class TrytonServer(object):
def __init__(self, options):
- format = '[%(asctime)s] %(levelname)s:%(name)s:%(message)s'
- datefmt = '%a %b %d %H:%M:%S %Y'
- logging.basicConfig(level=logging.INFO, format=format,
+
+ config.update_etc(options.configfile)
+
+ if options.logconf:
+ logging.config.fileConfig(options.logconf)
+ 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)
- CONFIG.update_etc(options['configfile'])
- CONFIG.update_cmdline(options)
-
- if CONFIG['logfile']:
- logf = CONFIG['logfile']
- # test if the directories exist, else create them
- try:
- diff = 0
- if os.path.isfile(logf):
- diff = int(time.time()) - int(os.stat(logf)[-1])
- handler = logging.handlers.TimedRotatingFileHandler(
- logf, 'D', 1, 30)
- handler.rolloverAt -= diff
- except Exception, exception:
- sys.stderr.write(
- "ERROR: couldn't create the logfile directory:"
- + str(exception))
- else:
- formatter = logging.Formatter(format, datefmt)
- # tell the handler to use this format
- handler.setFormatter(formatter)
-
- # add the handler to the root logger
- logging.getLogger().addHandler(handler)
- logging.getLogger().setLevel(logging.INFO)
- elif os.name != 'nt':
- reverse = '\x1b[7m'
- reset = '\x1b[0m'
- # reverse color for error and critical messages
- for level in logging.ERROR, logging.CRITICAL:
- msg = reverse + logging.getLevelName(level) + reset
- logging.addLevelName(level, msg)
-
- self.logger = logging.getLogger("server")
-
- if CONFIG.configfile:
+ self.logger = logging.getLogger(__name__)
+
+ if options.configfile:
self.logger.info('using %s as configuration file'
- % CONFIG.configfile)
+ % options.configfile)
else:
self.logger.info('using default configuration')
self.logger.info('initialising distributed objects services')
self.xmlrpcd = []
self.jsonrpcd = []
self.webdavd = []
+ self.options = options
+
+ if time.tzname != ('UTC', 'UTC'):
+ self.logger.error('timezeone is not set to UTC')
def run(self):
"Run the server and never return"
- update = bool(CONFIG['init'] or CONFIG['update'])
init = {}
signal.signal(signal.SIGINT, lambda *a: self.stop())
@@ -84,22 +63,18 @@ class TrytonServer(object):
if hasattr(signal, 'SIGUSR1'):
signal.signal(signal.SIGUSR1, lambda *a: self.restart())
- if CONFIG['pidfile']:
- with open(CONFIG['pidfile'], 'w') as fd_pid:
+ if self.options.pidfile:
+ with open(self.options.pidfile, 'w') as fd_pid:
fd_pid.write("%d" % (os.getpid()))
- if not CONFIG["db_name"] \
- and bool(CONFIG['init'] or CONFIG['update']):
- raise Exception('Missing database option!')
-
- if not update:
+ if not self.options.update:
self.start_servers()
- for db_name in CONFIG["db_name"]:
+ for db_name in self.options.database_names:
init[db_name] = False
with Transaction().start(db_name, 0) as transaction:
cursor = transaction.cursor
- if CONFIG['init']:
+ if self.options.update:
if not cursor.test():
self.logger.info("init db")
backend.get('Database').init(cursor)
@@ -108,8 +83,8 @@ class TrytonServer(object):
elif not cursor.test():
raise Exception("'%s' is not a Tryton database!" % db_name)
- for db_name in CONFIG["db_name"]:
- if update:
+ for db_name in self.options.database_names:
+ if self.options.update:
with Transaction().start(db_name, 0) as transaction:
cursor = transaction.cursor
if not cursor.test():
@@ -120,12 +95,9 @@ class TrytonServer(object):
lang = [x[0] for x in cursor.fetchall()]
else:
lang = None
- Pool(db_name).init(update=update, lang=lang)
-
- for kind in ('init', 'update'):
- CONFIG[kind] = {}
+ Pool(db_name).init(update=self.options.update, lang=lang)
- for db_name in CONFIG['db_name']:
+ for db_name in self.options.database_names:
if init[db_name]:
# try to read password from environment variable
# TRYTONPASSFILE, empty TRYTONPASSFILE ignored
@@ -161,14 +133,14 @@ class TrytonServer(object):
})
transaction.cursor.commit()
- if update:
+ if self.options.update:
self.logger.info('Update/Init succeed!')
logging.shutdown()
sys.exit(0)
threads = {}
while True:
- if CONFIG['cron']:
+ if self.options.cron:
for dbname in Pool.database_list():
thread = threads.get(dbname)
if thread and thread.is_alive():
@@ -187,42 +159,41 @@ class TrytonServer(object):
args=(dbname,), kwargs={})
thread.start()
threads[dbname] = thread
- if CONFIG['auto_reload']:
+ if self.options.dev:
for _ in range(60):
- if monitor():
+ if monitor([self.options.configfile]
+ if self.options.configfile else []):
self.restart()
time.sleep(1)
else:
time.sleep(60)
def start_servers(self):
+ ssl = config.get('ssl', 'privatekey')
# Launch Server
- if CONFIG['jsonrpc']:
+ if config.get('jsonrpc', 'listen'):
from trytond.protocols.jsonrpc import JSONRPCDaemon
- for hostname, port in CONFIG['jsonrpc']:
- self.jsonrpcd.append(JSONRPCDaemon(hostname, port,
- CONFIG['ssl_jsonrpc']))
+ 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" %
- (CONFIG['ssl_jsonrpc'] and ' SSL' or '', hostname or '*',
- port))
+ (ssl and ' SSL' or '', hostname or '*', port))
- if CONFIG['xmlrpc']:
+ if config.get('xmlrpc', 'listen'):
from trytond.protocols.xmlrpc import XMLRPCDaemon
- for hostname, port in CONFIG['xmlrpc']:
- self.xmlrpcd.append(XMLRPCDaemon(hostname, port,
- CONFIG['ssl_xmlrpc']))
+ 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" %
- (CONFIG['ssl_xmlrpc'] and ' SSL' or '', hostname or '*',
- port))
+ (ssl and ' SSL' or '', hostname or '*', port))
- if CONFIG['webdav']:
+ if config.get('webdav', 'listen'):
from trytond.protocols.webdav import WebDAVServerThread
- for hostname, port in CONFIG['webdav']:
- self.webdavd.append(WebDAVServerThread(hostname, port,
- CONFIG['ssl_webdav']))
+ 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" %
- (CONFIG['ssl_webdav'] and ' SSL' or '', hostname or '*',
- port))
+ (ssl and ' SSL' or '', hostname or '*', port))
for servers in (self.xmlrpcd, self.jsonrpcd, self.webdavd):
for server in servers:
@@ -234,8 +205,8 @@ class TrytonServer(object):
server.stop()
server.join()
if exit:
- if CONFIG['pidfile']:
- os.unlink(CONFIG['pidfile'])
+ if self.options.pidfile:
+ os.unlink(self.options.pidfile)
logging.getLogger('server').info('stopped')
logging.shutdown()
sys.exit(0)
diff --git a/trytond/tests/__init__.py b/trytond/tests/__init__.py
index 8da0e0a..6e456ca 100644
--- a/trytond/tests/__init__.py
+++ b/trytond/tests/__init__.py
@@ -13,6 +13,7 @@ from .wizard import *
from .workflow import *
from .copy_ import *
from history import *
+from .field_context import *
def register():
@@ -97,6 +98,15 @@ def register():
URLObject,
ModelSQLRequiredField,
ModelSQLTimestamp,
+ Model4Union1,
+ Model4Union2,
+ Model4Union3,
+ Model4Union4,
+ Union,
+ UnionUnion,
+ Model4UnionTree1,
+ Model4UnionTree2,
+ UnionTree,
MPTT,
ImportDataBoolean,
ImportDataInteger,
@@ -141,6 +151,9 @@ def register():
Many2OneTarget,
Many2OneDomainValidation,
TestHistory,
+ TestHistoryLine,
+ FieldContextChild,
+ FieldContextParent,
module='tests', type_='model')
Pool.register(
TestWizard,
diff --git a/trytond/tests/field_context.py b/trytond/tests/field_context.py
new file mode 100644
index 0000000..e575b7d
--- /dev/null
+++ b/trytond/tests/field_context.py
@@ -0,0 +1,25 @@
+# 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
+from trytond.pyson import Eval
+
+__all__ = [
+ 'FieldContextParent',
+ 'FieldContextChild',
+ ]
+
+
+class FieldContextParent(ModelSQL):
+ 'Field Context Parent'
+ __name__ = 'test.field_context.parent'
+ name = fields.Char('Name')
+ child = fields.Many2One('test.field_context.child', 'Child',
+ context={
+ 'name': Eval('name'),
+ })
+
+
+class FieldContextChild(ModelSQL):
+ 'Field Context Child'
+ __name__ = 'test.field_context.child'
diff --git a/trytond/tests/history.py b/trytond/tests/history.py
index b23d8ec..ed1b24b 100644
--- a/trytond/tests/history.py
+++ b/trytond/tests/history.py
@@ -2,7 +2,7 @@
#this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, fields
-__all__ = ['TestHistory']
+__all__ = ['TestHistory', 'TestHistoryLine']
class TestHistory(ModelSQL):
@@ -10,3 +10,12 @@ class TestHistory(ModelSQL):
__name__ = 'test.history'
_history = True
value = fields.Integer('Value')
+ lines = fields.One2Many('test.history.line', 'history', 'Lines')
+
+
+class TestHistoryLine(ModelSQL):
+ 'Test History Line'
+ __name__ = 'test.history.line'
+ _history = True
+ history = fields.Many2One('test.history', 'History')
+ name = fields.Char('Name')
diff --git a/trytond/tests/model.py b/trytond/tests/model.py
index 80ed3d4..1815389 100644
--- a/trytond/tests/model.py
+++ b/trytond/tests/model.py
@@ -1,9 +1,12 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
-from trytond.model import ModelSingleton, ModelSQL, fields
+from trytond.model import ModelSingleton, ModelSQL, UnionMixin, fields
__all__ = [
'Singleton', 'URLObject', 'ModelSQLRequiredField', 'ModelSQLTimestamp',
+ 'Model4Union1', 'Model4Union2', 'Model4Union3', 'Model4Union4',
+ 'Union', 'UnionUnion',
+ 'Model4UnionTree1', 'Model4UnionTree2', 'UnionTree',
]
@@ -34,3 +37,74 @@ class ModelSQLRequiredField(ModelSQL):
class ModelSQLTimestamp(ModelSQL):
'Model to test timestamp'
__name__ = 'test.modelsql.timestamp'
+
+
+class Model4Union1(ModelSQL):
+ 'Model for union 1'
+ __name__ = 'test.model.union1'
+ name = fields.Char('Name')
+ optional = fields.Char('Optional')
+
+
+class Model4Union2(ModelSQL):
+ 'Model for union 2'
+ __name__ = 'test.model.union2'
+ name = fields.Char('Name')
+
+
+class Model4Union3(ModelSQL):
+ 'Model for union 3'
+ __name__ = 'test.model.union3'
+ name = fields.Char('Name')
+
+
+class Model4Union4(ModelSQL):
+ 'Model for union 4'
+ __name__ = 'test.model.union4'
+ name = fields.Char('Name')
+
+
+class Union(UnionMixin, ModelSQL):
+ 'Union'
+ __name__ = 'test.union'
+ name = fields.Char('Name')
+ optional = fields.Char('Optional')
+
+ @staticmethod
+ def union_models():
+ return ['test.model.union%s' % i for i in range(1, 4)]
+
+
+class UnionUnion(UnionMixin, ModelSQL):
+ 'Union of union'
+ __name__ = 'test.union.union'
+ name = fields.Char('Name')
+
+ @staticmethod
+ def union_models():
+ return ['test.union', 'test.model.union4']
+
+
+class Model4UnionTree1(ModelSQL):
+ 'Model for union tree 1'
+ __name__ = 'test.model.union.tree1'
+ name = fields.Char('Name')
+
+
+class Model4UnionTree2(ModelSQL):
+ 'Model for union tree 2'
+ __name__ = 'test.model.union.tree2'
+ name = fields.Char('Name')
+ parent = fields.Many2One('test.model.union.tree1', 'Parent')
+
+
+class UnionTree(UnionMixin, ModelSQL):
+ 'Union tree'
+ __name__ = 'test.union.tree'
+ name = fields.Char('Name')
+ parent = fields.Many2One('test.union.tree', 'Parent')
+ childs = fields.One2Many('test.union.tree', 'parent', 'Childs')
+
+ @staticmethod
+ def union_models():
+ return ['test.model.union.tree1', 'test.model.union.tree2']
diff --git a/trytond/tests/run-tests.py b/trytond/tests/run-tests.py
index f576c34..f6dd242 100755
--- a/trytond/tests/run-tests.py
+++ b/trytond/tests/run-tests.py
@@ -8,7 +8,8 @@ import time
import unittest
import sys
-from trytond.config import CONFIG
+from trytond.config import config
+from trytond import backend
if __name__ != '__main__':
raise ImportError('%s can not be imported' % __name__)
@@ -22,18 +23,17 @@ parser.add_argument("-m", "--modules", action="store_true", dest="modules",
parser.add_argument("-v", action="count", default=0, dest="verbosity",
help="Increase verbosity")
parser.add_argument('tests', metavar='test', nargs='*')
+parser.epilog = ('The database name can be specified in the DB_NAME '
+ 'environment variable.')
opt = parser.parse_args()
-CONFIG['db_type'] = 'sqlite'
-CONFIG.update_etc(opt.config)
-if not CONFIG['admin_passwd']:
- CONFIG['admin_passwd'] = 'admin'
+config.update_etc(opt.config)
-if CONFIG['db_type'] == 'sqlite':
+if backend.name() == 'sqlite':
database_name = ':memory:'
else:
database_name = 'test_' + str(int(time.time()))
-os.environ['DB_NAME'] = database_name
+os.environ.setdefault('DB_NAME', database_name)
from trytond.tests.test_tryton import all_suite, modules_suite
if not opt.modules:
diff --git a/trytond/tests/test.py b/trytond/tests/test.py
index f6cbaf7..5f812f2 100644
--- a/trytond/tests/test.py
+++ b/trytond/tests/test.py
@@ -604,6 +604,7 @@ class Selection(ModelSQL):
select = fields.Selection([
('', ''), ('arabic', 'Arabic'), ('hexa', 'Hexadecimal')],
'Selection')
+ select_string = select.translated('select')
dyn_select = fields.Selection('get_selection',
'Instance Dynamic Selection')
dyn_select_static = fields.Selection('static_selection',
diff --git a/trytond/tests/test_access.py b/trytond/tests/test_access.py
index 6322ac4..3251a77 100644
--- a/trytond/tests/test_access.py
+++ b/trytond/tests/test_access.py
@@ -5,6 +5,10 @@ import unittest
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
install_module
from trytond.transaction import Transaction
+from trytond.exceptions import UserError
+
+CONTEXT = CONTEXT.copy()
+CONTEXT['_check_access'] = True
class ModelAccessTestCase(unittest.TestCase):
@@ -42,7 +46,7 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access.write([model_access_wo_group], {
'perm_read': False,
})
- self.assertRaises(Exception, self.test_access.read, [test.id])
+ self.assertRaises(UserError, self.test_access.read, [test.id])
# Two access rules with one group allowed
group, = self.group.search([('users', '=', USER)])
@@ -70,11 +74,11 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access.write([model_access_wo_group], {
'perm_read': False,
})
- self.assertRaises(Exception, self.test_access.read, [test.id])
+ self.assertRaises(UserError, self.test_access.read, [test.id])
# One access disallowed for one group
self.model_access.delete([model_access_wo_group])
- self.assertRaises(Exception, self.test_access.read, [test.id])
+ self.assertRaises(UserError, self.test_access.read, [test.id])
# One access allowed for one group
self.model_access.write([model_access_w_group], {
@@ -123,7 +127,7 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access.write([model_access_wo_group], {
'perm_write': False,
})
- self.assertRaises(Exception, self.test_access.write, [test], {})
+ self.assertRaises(UserError, self.test_access.write, [test], {})
# Two access rules with one group allowed
group, = self.group.search([('users', '=', USER)])
@@ -150,11 +154,11 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access.write([model_access_wo_group], {
'perm_write': False,
})
- self.assertRaises(Exception, self.test_access.write, [test], {})
+ self.assertRaises(UserError, self.test_access.write, [test], {})
# One access disallowed for one group
self.model_access.delete([model_access_wo_group])
- self.assertRaises(Exception, self.test_access.write, [test], {})
+ self.assertRaises(UserError, self.test_access.write, [test], {})
# One access allowed for one group
self.model_access.write([model_access_w_group], {
@@ -201,7 +205,7 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access.write([model_access_wo_group], {
'perm_create': False,
})
- self.assertRaises(Exception, self.test_access.create, {})
+ self.assertRaises(UserError, self.test_access.create, {})
# Two access rules with one group allowed
group, = self.group.search([('users', '=', USER)])
@@ -229,11 +233,11 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access.write([model_access_wo_group], {
'perm_create': False,
})
- self.assertRaises(Exception, self.test_access.create, [{}])
+ self.assertRaises(UserError, self.test_access.create, [{}])
# One access disallowed for one group
self.model_access.delete([model_access_wo_group])
- self.assertRaises(Exception, self.test_access.create, [{}])
+ self.assertRaises(UserError, self.test_access.create, [{}])
# One access allowed for one group
self.model_access.write([model_access_w_group], {
@@ -282,7 +286,7 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access.write([model_access_wo_group], {
'perm_delete': False,
})
- self.assertRaises(Exception, self.test_access.delete,
+ self.assertRaises(UserError, self.test_access.delete,
[tests.pop()])
# Two access rules with one group allowed
@@ -311,12 +315,12 @@ class ModelAccessTestCase(unittest.TestCase):
self.model_access.write([model_access_wo_group], {
'perm_delete': False,
})
- self.assertRaises(Exception, self.test_access.delete,
+ self.assertRaises(UserError, self.test_access.delete,
[tests.pop()])
# One access disallowed for one group
self.model_access.delete([model_access_wo_group])
- self.assertRaises(Exception, self.test_access.delete,
+ self.assertRaises(UserError, self.test_access.delete,
[tests.pop()])
# One access allowed for one group
@@ -400,11 +404,11 @@ class ModelFieldAccessTestCase(unittest.TestCase):
'perm_read': False,
})
- self.assertRaises(Exception, self.test_access.read, [test.id],
+ self.assertRaises(UserError, self.test_access.read, [test.id],
['field1'])
self.test_access.read([test.id], ['field2'])
- self.assertRaises(Exception, self.test_access.read, [test.id])
- self.assertRaises(Exception, getattr, test, 'field1')
+ self.assertRaises(UserError, self.test_access.read, [test.id])
+ self.assertRaises(UserError, getattr, test, 'field1')
test.field2
transaction.cursor.cache.clear()
test = self.test_access(test.id)
@@ -453,22 +457,22 @@ class ModelFieldAccessTestCase(unittest.TestCase):
self.field_access.write([field_access_wo_group], {
'perm_read': False,
})
- self.assertRaises(Exception, self.test_access.read, [test.id],
+ self.assertRaises(UserError, self.test_access.read, [test.id],
['field1'])
self.test_access.read([test.id], ['field2'])
- self.assertRaises(Exception, self.test_access.read, [test.id])
- self.assertRaises(Exception, getattr, test, 'field1')
+ self.assertRaises(UserError, self.test_access.read, [test.id])
+ self.assertRaises(UserError, getattr, test, 'field1')
test.field2
transaction.cursor.cache.clear()
test = self.test_access(test.id)
# One access disallowed for one group
self.field_access.delete([field_access_wo_group])
- self.assertRaises(Exception, self.test_access.read, [test.id],
+ self.assertRaises(UserError, self.test_access.read, [test.id],
['field1'])
self.test_access.read([test.id], ['field2'])
- self.assertRaises(Exception, self.test_access.read, [test.id])
- self.assertRaises(Exception, getattr, test, 'field1')
+ self.assertRaises(UserError, self.test_access.read, [test.id])
+ self.assertRaises(UserError, getattr, test, 'field1')
test.field2
transaction.cursor.cache.clear()
test = self.test_access(test.id)
@@ -537,11 +541,11 @@ class ModelFieldAccessTestCase(unittest.TestCase):
'perm_read': False,
})
self.test_access.read([test.id], ['field1'])
- self.assertRaises(Exception, self.test_access.read, [test.id],
+ self.assertRaises(UserError, self.test_access.read, [test.id],
['field2'])
- self.assertRaises(Exception, self.test_access.read, [test.id])
+ self.assertRaises(UserError, self.test_access.read, [test.id])
test.field1
- self.assertRaises(Exception, getattr, test, 'field2')
+ self.assertRaises(UserError, getattr, test, 'field2')
transaction.cursor.cache.clear()
test = self.test_access(test.id)
@@ -549,13 +553,13 @@ class ModelFieldAccessTestCase(unittest.TestCase):
self.field_access.write([field_access1], {
'perm_read': False,
})
- self.assertRaises(Exception, self.test_access.read, [test.id],
+ self.assertRaises(UserError, self.test_access.read, [test.id],
['field1'])
- self.assertRaises(Exception, self.test_access.read, [test.id],
+ self.assertRaises(UserError, self.test_access.read, [test.id],
['field2'])
- self.assertRaises(Exception, self.test_access.read, [test.id])
- self.assertRaises(Exception, getattr, test, 'field1')
- self.assertRaises(Exception, getattr, test, 'field2')
+ self.assertRaises(UserError, self.test_access.read, [test.id])
+ self.assertRaises(UserError, getattr, test, 'field1')
+ self.assertRaises(UserError, getattr, test, 'field2')
transaction.cursor.cache.clear()
test = self.test_access(test.id)
@@ -607,10 +611,10 @@ class ModelFieldAccessTestCase(unittest.TestCase):
})
self.test_access.write([test], {})
- self.assertRaises(Exception, self.test_access.write, [test],
+ self.assertRaises(UserError, self.test_access.write, [test],
{'field1': 'ham'})
self.test_access.write([test], {'field2': 'spam'})
- self.assertRaises(Exception, self.test_access.write, [test], {
+ self.assertRaises(UserError, self.test_access.write, [test], {
'field1': 'ham',
'field2': 'spam',
})
@@ -660,10 +664,10 @@ class ModelFieldAccessTestCase(unittest.TestCase):
'perm_write': False,
})
self.test_access.write([test], {})
- self.assertRaises(Exception, self.test_access.write, [test],
+ self.assertRaises(UserError, self.test_access.write, [test],
{'field1': 'ham'})
self.test_access.write([test], {'field2': 'spam'})
- self.assertRaises(Exception, self.test_access.write, [test], {
+ self.assertRaises(UserError, self.test_access.write, [test], {
'field1': 'ham',
'field2': 'spam',
})
@@ -671,10 +675,10 @@ class ModelFieldAccessTestCase(unittest.TestCase):
# One access disallowed for one group
self.field_access.delete([field_access_wo_group])
self.test_access.write([test], {})
- self.assertRaises(Exception, self.test_access.write, [test],
+ self.assertRaises(UserError, self.test_access.write, [test],
{'field1': 'ham'})
self.test_access.write([test], {'field2': 'ham'})
- self.assertRaises(Exception, self.test_access.write, [test], {
+ self.assertRaises(UserError, self.test_access.write, [test], {
'field1': 'ham',
'field2': 'spam',
})
@@ -744,9 +748,9 @@ class ModelFieldAccessTestCase(unittest.TestCase):
})
self.test_access.write([test], {})
self.test_access.write([test], {'field1': 'ham'})
- self.assertRaises(Exception, self.test_access.write, [test], {
+ self.assertRaises(UserError, self.test_access.write, [test], {
'field2': 'spam'})
- self.assertRaises(Exception, self.test_access.write, [test], {
+ self.assertRaises(UserError, self.test_access.write, [test], {
'field1': 'ham',
'field2': 'spam',
})
@@ -756,11 +760,11 @@ class ModelFieldAccessTestCase(unittest.TestCase):
'perm_write': False,
})
self.test_access.write([test], {})
- self.assertRaises(Exception, self.test_access.write, [test], {
+ self.assertRaises(UserError, self.test_access.write, [test], {
'field1': 'ham'})
- self.assertRaises(Exception, self.test_access.write, [test], {
+ self.assertRaises(UserError, self.test_access.write, [test], {
'field2': 'spam'})
- self.assertRaises(Exception, self.test_access.write, [test], {
+ self.assertRaises(UserError, self.test_access.write, [test], {
'field1': 'ham',
'field2': 'spam',
})
diff --git a/trytond/tests/test_exportdata.py b/trytond/tests/test_exportdata.py
index 989d2d2..417b52b 100644
--- a/trytond/tests/test_exportdata.py
+++ b/trytond/tests/test_exportdata.py
@@ -216,8 +216,9 @@ class ExportDataTestCase(unittest.TestCase):
'selection': 'select1',
}])
self.assertEqual(
- self.export_data.export_data([export1], ['selection']),
- [['select1']])
+ self.export_data.export_data([export1], ['selection',
+ 'selection.translated']),
+ [['select1', 'Select 1']])
export2, = self.export_data.create([{
'selection': None,
diff --git a/trytond/tests/test_field_context.py b/trytond/tests/test_field_context.py
new file mode 100644
index 0000000..99a9cbc
--- /dev/null
+++ b/trytond/tests/test_field_context.py
@@ -0,0 +1,37 @@
+# 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
+from trytond.transaction import Transaction
+
+
+class FieldContextTestCase(unittest.TestCase):
+ "Test context on field"
+
+ def setUp(self):
+ install_module('tests')
+
+ def test_context(self):
+ Parent = POOL.get('test.field_context.parent')
+ Child = POOL.get('test.field_context.child')
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ child = Child()
+ child.save()
+ parent = Parent(name='foo', child=child)
+ parent.save()
+ self.assertEqual(parent.child._context['name'], 'foo')
+
+ parent.name = 'bar'
+ parent.save()
+ self.assertEqual(parent.child._context['name'], 'bar')
+
+
+def suite():
+ func = unittest.TestLoader().loadTestsFromTestCase
+ suite = unittest.TestSuite()
+ for testcase in (FieldContextTestCase,):
+ suite.addTests(func(testcase))
+ return suite
diff --git a/trytond/tests/test_fields.py b/trytond/tests/test_fields.py
index 8d260cc..2c436f6 100644
--- a/trytond/tests/test_fields.py
+++ b/trytond/tests/test_fields.py
@@ -567,7 +567,7 @@ class FieldsTestCase(unittest.TestCase):
'float': 'test',
})
- self.assertRaises(Exception, self.float_required.create, [{}])
+ self.assertRaises(UserError, self.float_required.create, [{}])
transaction.cursor.rollback()
float5, = self.float_required.create([{
@@ -581,17 +581,17 @@ class FieldsTestCase(unittest.TestCase):
}])
self.assert_(float6)
- self.assertRaises(Exception, self.float_digits.create, [{
+ self.assertRaises(UserError, self.float_digits.create, [{
'digits': 1,
'float': 1.11,
}])
- self.assertRaises(Exception, self.float_digits.write,
+ self.assertRaises(UserError, self.float_digits.write,
[float6], {
'float': 1.11,
})
- self.assertRaises(Exception, self.float_digits.write,
+ self.assertRaises(UserError, self.float_digits.write,
[float6], {
'digits': 0,
})
@@ -603,6 +603,42 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
+ def test0031float(self):
+ 'Test float search with None'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ float_none, float0, float1 = self.float.create([{
+ 'float': None,
+ }, {
+ 'float': 0,
+ }, {
+ 'float': 1,
+ }])
+ self.assertEqual([float_none], self.float.search([
+ ('float', '=', None),
+ ]))
+ self.assertEqual([float0], self.float.search([
+ ('float', '=', 0),
+ ]))
+ self.assertEqual([float1], self.float.search([
+ ('float', '>', 0),
+ ]))
+
+ self.assertEqual([float0, float1], self.float.search([
+ ('float', '!=', None),
+ ]))
+ self.assertEqual([float1], self.float.search([
+ ('float', '!=', 0),
+ ]))
+ self.assertEqual([float0], self.float.search([
+ ('float', '<', 1),
+ ]))
+
+ self.assertEqual([float_none, float1], self.float.search([
+ 'OR',
+ ('float', '>', 0),
+ ('float', '=', None),
+ ]))
+
def test0040numeric(self):
'Test Numeric'
with Transaction().start(DB_NAME, USER,
@@ -770,7 +806,7 @@ class FieldsTestCase(unittest.TestCase):
'numeric': 'test',
})
- self.assertRaises(Exception, self.numeric_required.create, [{}])
+ self.assertRaises(UserError, self.numeric_required.create, [{}])
transaction.cursor.rollback()
numeric5, = self.numeric_required.create([{
@@ -784,22 +820,22 @@ class FieldsTestCase(unittest.TestCase):
}])
self.assert_(numeric6)
- self.assertRaises(Exception, self.numeric_digits.create, [{
+ self.assertRaises(UserError, self.numeric_digits.create, [{
'digits': 1,
'numeric': Decimal('1.11'),
}])
- self.assertRaises(Exception, self.numeric_digits.write,
+ self.assertRaises(UserError, self.numeric_digits.write,
[numeric6], {
'numeric': Decimal('1.11'),
})
- self.assertRaises(Exception, self.numeric_digits.write,
+ self.assertRaises(UserError, self.numeric_digits.write,
[numeric6], {
'numeric': Decimal('0.10000000000000001'),
})
- self.assertRaises(Exception, self.numeric_digits.write,
+ self.assertRaises(UserError, self.numeric_digits.write,
[numeric6], {
'digits': 0,
})
@@ -825,6 +861,42 @@ class FieldsTestCase(unittest.TestCase):
])
self.assertEqual(numerics, [numeric1])
+ def test0042numeric(self):
+ 'Test numeric search with None'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ numeric_none, numeric0, numeric1 = self.numeric.create([{
+ 'numeric': None,
+ }, {
+ 'numeric': 0,
+ }, {
+ 'numeric': 1,
+ }])
+ self.assertEqual([numeric_none], self.numeric.search([
+ ('numeric', '=', None),
+ ]))
+ self.assertEqual([numeric0], self.numeric.search([
+ ('numeric', '=', 0),
+ ]))
+ self.assertEqual([numeric1], self.numeric.search([
+ ('numeric', '>', 0),
+ ]))
+
+ self.assertEqual([numeric0, numeric1], self.numeric.search([
+ ('numeric', '!=', None),
+ ]))
+ self.assertEqual([numeric1], self.numeric.search([
+ ('numeric', '!=', 0),
+ ]))
+ self.assertEqual([numeric0], self.numeric.search([
+ ('numeric', '<', 1),
+ ]))
+
+ self.assertEqual([numeric_none, numeric1], self.numeric.search([
+ 'OR',
+ ('numeric', '>', 0),
+ ('numeric', '=', None),
+ ]))
+
def test0050char(self):
'Test Char'
with Transaction().start(DB_NAME, USER,
@@ -1025,10 +1097,10 @@ class FieldsTestCase(unittest.TestCase):
})
self.assertEqual(char2.char, 'Test')
- self.assertRaises(Exception, self.char_required.create, [{}])
+ self.assertRaises(UserError, self.char_required.create, [{}])
transaction.cursor.rollback()
- self.assertRaises(Exception, self.char_required.create, [{
+ self.assertRaises(UserError, self.char_required.create, [{
'char': '',
}])
transaction.cursor.rollback()
@@ -1275,7 +1347,7 @@ class FieldsTestCase(unittest.TestCase):
})
self.assertEqual(text2.text, 'Test')
- self.assertRaises(Exception, self.text_required.create, [{}])
+ self.assertRaises(UserError, self.text_required.create, [{}])
transaction.cursor.rollback()
text5, = self.text_required.create([{
@@ -1288,11 +1360,11 @@ class FieldsTestCase(unittest.TestCase):
}])
self.assert_(text6)
- self.assertRaises(Exception, self.text_size.create, [{
+ self.assertRaises(UserError, self.text_size.create, [{
'text': 'foobar',
}])
- self.assertRaises(Exception, self.text_size.write, [text6], {
+ self.assertRaises(UserError, self.text_size.write, [text6], {
'text': 'foobar',
})
@@ -1546,7 +1618,7 @@ class FieldsTestCase(unittest.TestCase):
self.assert_(date5)
self.assertEqual(date5.date, datetime.date(2009, 1, 1))
- self.assertRaises(Exception, self.date_required.create, [{}])
+ self.assertRaises(UserError, self.date_required.create, [{}])
transaction.cursor.rollback()
date6, = self.date_required.create([{
@@ -1798,7 +1870,7 @@ class FieldsTestCase(unittest.TestCase):
self.assertEqual(datetime5.datetime,
datetime.datetime(2009, 1, 1, 12, 0, 0))
- self.assertRaises(Exception, self.datetime_required.create, [{}])
+ self.assertRaises(UserError, self.datetime_required.create, [{}])
transaction.cursor.rollback()
datetime6, = self.datetime_required.create([{
@@ -1826,7 +1898,7 @@ class FieldsTestCase(unittest.TestCase):
self.assert_(self.datetime_format.create([{
'datetime': datetime.datetime(2009, 1, 1, 12, 30),
}]))
- self.assertRaises(Exception, self.datetime_format.create, [{
+ self.assertRaises(UserError, self.datetime_format.create, [{
'datetime': datetime.datetime(2009, 1, 1, 12, 30, 25),
}])
@@ -2045,7 +2117,7 @@ class FieldsTestCase(unittest.TestCase):
self.assert_(time5)
self.assertEqual(time5.time, datetime.time(12, 0))
- self.assertRaises(Exception, self.time_required.create, [{}])
+ self.assertRaises(UserError, self.time_required.create, [{}])
transaction.cursor.rollback()
time6, = self.time_required.create([{
@@ -2068,7 +2140,7 @@ class FieldsTestCase(unittest.TestCase):
self.assert_(self.time_format.create([{
'time': datetime.time(12, 30),
}]))
- self.assertRaises(Exception, self.time_format.create, [{
+ self.assertRaises(UserError, self.time_format.create, [{
'time': datetime.time(12, 30, 25),
}])
@@ -2155,18 +2227,18 @@ class FieldsTestCase(unittest.TestCase):
})
self.assertEqual(one2one2.one2one, None)
- self.assertRaises(Exception, self.one2one.create, [{
+ self.assertRaises(UserError, self.one2one.create, [{
'name': 'one2one3',
'one2one': target1.id,
}])
transaction.cursor.rollback()
- self.assertRaises(Exception, self.one2one.write, [one2one2], {
+ self.assertRaises(UserError, self.one2one.write, [one2one2], {
'one2one': target1.id,
})
transaction.cursor.rollback()
- self.assertRaises(Exception, self.one2one_required.create, [{
+ self.assertRaises(UserError, self.one2one_required.create, [{
'name': 'one2one3',
}])
transaction.cursor.rollback()
@@ -2184,7 +2256,7 @@ class FieldsTestCase(unittest.TestCase):
target4, = self.one2one_target.create([{
'name': 'target4',
}])
- self.assertRaises(Exception, self.one2one_domain.create, [{
+ self.assertRaises(UserError, self.one2one_domain.create, [{
'name': 'one2one4',
'one2one': target4.id,
}])
@@ -2367,7 +2439,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
- self.assertRaises(Exception, self.one2many_required.create, [{
+ self.assertRaises(UserError, self.one2many_required.create, [{
'name': 'origin3',
}])
transaction.cursor.rollback()
@@ -2385,14 +2457,14 @@ class FieldsTestCase(unittest.TestCase):
self.one2many_size.create([{
'targets': [('create', [{}])] * 3,
}])
- self.assertRaises(Exception, self.one2many_size.create, [{
+ self.assertRaises(UserError, self.one2many_size.create, [{
'targets': [('create', [{}])] * 4,
}])
self.one2many_size_pyson.create([{
'limit': 4,
'targets': [('create', [{}])] * 4,
}])
- self.assertRaises(Exception, self.one2many_size_pyson.create, [{
+ self.assertRaises(UserError, self.one2many_size_pyson.create, [{
'limit': 2,
'targets': [('create', [{}])] * 4,
}])
@@ -2561,7 +2633,7 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
- self.assertRaises(Exception, self.many2many_required.create, [{
+ self.assertRaises(UserError, self.many2many_required.create, [{
'name': 'origin3',
}])
transaction.cursor.rollback()
@@ -2697,7 +2769,7 @@ class FieldsTestCase(unittest.TestCase):
}])
self.assert_(reference3)
- self.assertRaises(Exception, self.reference_required.create, [{
+ self.assertRaises(UserError, self.reference_required.create, [{
'name': 'reference4',
}])
transaction.cursor.rollback()
@@ -2988,12 +3060,14 @@ class FieldsTestCase(unittest.TestCase):
selection1, = self.selection.create([{'select': 'arabic'}])
self.assert_(selection1)
self.assertEqual(selection1.select, 'arabic')
+ self.assertEqual(selection1.select_string, 'Arabic')
selection2, = self.selection.create([{'select': None}])
self.assert_(selection2)
self.assertEqual(selection2.select, None)
+ self.assertEqual(selection2.select_string, '')
- self.assertRaises(Exception, self.selection.create,
+ self.assertRaises(UserError, self.selection.create,
[{'select': 'chinese'}])
selection3, = self.selection.create(
@@ -3014,15 +3088,15 @@ class FieldsTestCase(unittest.TestCase):
self.assertEqual(selection5.select, 'hexa')
self.assertEqual(selection5.dyn_select, None)
- self.assertRaises(Exception, self.selection.create,
+ self.assertRaises(UserError, self.selection.create,
[{'select': 'arabic', 'dyn_select': '0x3'}])
- self.assertRaises(Exception, self.selection.create,
+ self.assertRaises(UserError, self.selection.create,
[{'select': 'hexa', 'dyn_select': '3'}])
- self.assertRaises(Exception, self.selection_required.create, [{}])
+ self.assertRaises(UserError, self.selection_required.create, [{}])
transaction.cursor.rollback()
- self.assertRaises(Exception, self.selection_required.create,
+ self.assertRaises(UserError, self.selection_required.create,
[{'select': None}])
transaction.cursor.rollback()
@@ -3048,13 +3122,13 @@ class FieldsTestCase(unittest.TestCase):
dict3, = self.dict_default.create([{}])
self.assert_(dict3.dico == {'a': 1})
- self.assertRaises(Exception, self.dict_required.create, [{}])
+ self.assertRaises(UserError, self.dict_required.create, [{}])
transaction.cursor.rollback()
dict4, = self.dict_required.create([{'dico': dict(a=1)}])
self.assert_(dict4.dico == {'a': 1})
- self.assertRaises(Exception, self.dict_required.create,
+ self.assertRaises(UserError, self.dict_required.create,
[{'dico': {}}])
transaction.cursor.rollback()
@@ -3081,13 +3155,13 @@ class FieldsTestCase(unittest.TestCase):
bin3, = self.binary_default.create([{}])
self.assert_(bin3.binary == buffer('default'))
- self.assertRaises(Exception, self.binary_required.create, [{}])
+ self.assertRaises(UserError, self.binary_required.create, [{}])
transaction.cursor.rollback()
bin4, = self.binary_required.create([{'binary': buffer('baz')}])
self.assert_(bin4.binary == buffer('baz'))
- self.assertRaises(Exception, self.binary_required.create,
+ self.assertRaises(UserError, self.binary_required.create,
[{'binary': buffer('')}])
transaction.cursor.rollback()
diff --git a/trytond/tests/test_history.py b/trytond/tests/test_history.py
index 5b4c064..74c9176 100644
--- a/trytond/tests/test_history.py
+++ b/trytond/tests/test_history.py
@@ -7,7 +7,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.config import CONFIG
+from trytond import backend
class HistoryTestCase(unittest.TestCase):
@@ -16,6 +16,17 @@ class HistoryTestCase(unittest.TestCase):
def setUp(self):
install_module('tests')
+ def tearDown(self):
+ History = POOL.get('test.history')
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ cursor = transaction.cursor
+ table = History.__table__()
+ history_table = History.__table_history__()
+ cursor.execute(*table.delete())
+ cursor.execute(*history_table.delete())
+ cursor.commit()
+
def test0010read(self):
'Test read history'
History = POOL.get('test.history')
@@ -65,7 +76,7 @@ class HistoryTestCase(unittest.TestCase):
with Transaction().set_context(_datetime=datetime.datetime.min):
self.assertRaises(UserError, History.read, [history_id])
- @unittest.skipIf(CONFIG['db_type'] in ('sqlite', 'mysql'),
+ @unittest.skipIf(backend.name() in ('sqlite', 'mysql'),
'now() is not the start of the transaction')
def test0020read_same_timestamp(self):
'Test read history with same timestamp'
@@ -173,6 +184,204 @@ class HistoryTestCase(unittest.TestCase):
History.restore_history([history_id], datetime.datetime.min)
self.assertRaises(UserError, History.read, [history_id])
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ History.delete([History(history_id)])
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ History.restore_history([history_id], datetime.datetime.max)
+ self.assertRaises(UserError, History.read, [history_id])
+
+ @unittest.skipIf(backend.name() in ('sqlite', 'mysql'),
+ 'now() is not the start of the transaction')
+ def test0045restore_history_same_timestamp(self):
+ 'Test restore history with same timestamp'
+ History = POOL.get('test.history')
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(value=1)
+ history.save()
+ history_id = history.id
+ first = history.create_date
+ history.value = 2
+ history.save()
+ second = history.create_date
+
+ self.assertEqual(first, second)
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(history_id)
+ history.value = 3
+ history.save()
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ History.restore_history([history_id], first)
+ history = History(history_id)
+ self.assertEqual(history.value, 2)
+
+ def test0050ordered_search(self):
+ 'Test ordered search of history models'
+ History = POOL.get('test.history')
+ order = [('value', 'ASC')]
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(value=1)
+ history.save()
+ first_id = history.id
+ first_stamp = history.create_date
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(value=2)
+ history.save()
+ second_id = history.id
+ second_stamp = history.create_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ first, second = History.search([], order=order)
+
+ self.assertEqual(first.id, first_id)
+ self.assertEqual(second.id, second_id)
+
+ first.value = 3
+ first.save()
+ third_stamp = first.write_date
+ transaction.cursor.commit()
+
+ results = [
+ (first_stamp, [first]),
+ (second_stamp, [first, second]),
+ (third_stamp, [second, first]),
+ (datetime.datetime.now(), [second, first]),
+ (datetime.datetime.max, [second, first]),
+ ]
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ for timestamp, instances in results:
+ with Transaction().set_context(_datetime=timestamp):
+ records = History.search([], order=order)
+ self.assertEqual(records, instances)
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ to_delete, _ = History.search([], order=order)
+
+ self.assertEqual(to_delete.id, second.id)
+
+ History.delete([to_delete])
+ transaction.cursor.commit()
+
+ results = [
+ (first_stamp, [first]),
+ (second_stamp, [first, second]),
+ (third_stamp, [second, first]),
+ (datetime.datetime.now(), [first]),
+ (datetime.datetime.max, [first]),
+ ]
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ for timestamp, instances in results:
+ with Transaction().set_context(_datetime=timestamp,
+ from_test=True):
+ records = History.search([], order=order)
+ self.assertEqual(records, instances)
+
+ @unittest.skipIf(backend.name() in ('sqlite', 'mysql'),
+ 'now() is not the start of the transaction')
+ def test0060_ordered_search_same_timestamp(self):
+ 'Test ordered search with same timestamp'
+ History = POOL.get('test.history')
+ order = [('value', 'ASC')]
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(value=1)
+ history.save()
+ first_stamp = history.create_date
+ history.value = 4
+ history.save()
+ second_stamp = history.write_date
+
+ self.assertEqual(first_stamp, second_stamp)
+ transaction.cursor.commit()
+
+ results = [
+ (second_stamp, [history], [4]),
+ (datetime.datetime.now(), [history], [4]),
+ (datetime.datetime.max, [history], [4]),
+ ]
+
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ for timestamp, instances, values in results:
+ with Transaction().set_context(_datetime=timestamp,
+ last_test=True):
+ records = History.search([], order=order)
+ self.assertEqual(records, instances)
+ self.assertEqual([x.value for x in records], values)
+
+ def test0070_browse(self):
+ 'Test browsing history'
+ History = POOL.get('test.history')
+ Line = POOL.get('test.history.line')
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(value=1)
+ history.save()
+ history_id = history.id
+ line_a = Line(name='a', history=history)
+ line_a.save()
+ line_a_id = line_a.id
+ line_b = Line(name='b', history=history)
+ line_b.save()
+ line_b_id = line_b.id
+
+ first_stamp = line_b.create_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(history_id)
+ history.value = 2
+ history.save()
+
+ Line.delete([Line(line_b_id)])
+
+ line_a = Line(line_a_id)
+ line_a.name = 'c'
+ line_a.save()
+
+ second_stamp = line_a.write_date
+
+ transaction.cursor.commit()
+
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+ history = History(history_id)
+ self.assertEqual(history.value, 2)
+ self.assertEqual([l.name for l in history.lines], ['c'])
+
+ with Transaction().set_context(_datetime=first_stamp):
+ history = History(history_id)
+ self.assertEqual(history.value, 1)
+ self.assertEqual([l.name for l in history.lines], ['a', 'b'])
+
+ with Transaction().set_context(_datetime=second_stamp):
+ history = History(history_id)
+ self.assertEqual(history.value, 2)
+ self.assertEqual([l.name for l in history.lines], ['c'])
+
def suite():
return unittest.TestLoader().loadTestsFromTestCase(HistoryTestCase)
diff --git a/trytond/tests/test_modelsql.py b/trytond/tests/test_modelsql.py
index 233ed9a..e9ea26d 100644
--- a/trytond/tests/test_modelsql.py
+++ b/trytond/tests/test_modelsql.py
@@ -5,7 +5,7 @@
import unittest
import time
-from trytond.config import CONFIG
+from trytond import backend
from trytond.exceptions import UserError, ConcurrencyException
from trytond.transaction import Transaction
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
@@ -20,13 +20,12 @@ class ModelSQLTestCase(unittest.TestCase):
self.modelsql = POOL.get('test.modelsql')
self.modelsql_timestamp = POOL.get('test.modelsql.timestamp')
+ @unittest.skipIf(backend.name() == 'sqlite',
+ 'SQLite not concerned because tryton don\'t set "NOT NULL"'
+ 'constraint: "ALTER TABLE" don\'t support NOT NULL constraint'
+ 'without default value')
def test0010required_field_missing(self):
'Test error message when a required field is missing'
- if CONFIG['db_type'] not in ('postgresql', 'mysql'):
- # SQLite not concerned because tryton don't set "NOT NULL"
- # constraint: 'ALTER TABLE' don't support NOT NULL constraint
- # without default value
- return
fields = {
'desc': '',
'integer': 0,
@@ -56,7 +55,7 @@ class ModelSQLTestCase(unittest.TestCase):
timestamp = self.modelsql_timestamp.read([record.id],
['_timestamp'])[0]['_timestamp']
- if CONFIG['db_type'] in ('sqlite', 'mysql'):
+ if backend.name() in ('sqlite', 'mysql'):
# timestamp precision of sqlite is the second
time.sleep(1)
diff --git a/trytond/tests/test_mptt.py b/trytond/tests/test_mptt.py
index 7631456..6bbd37b 100644
--- a/trytond/tests/test_mptt.py
+++ b/trytond/tests/test_mptt.py
@@ -3,6 +3,7 @@
#this repository contains the full copyright notices and license terms.
import sys
import unittest
+from mock import patch
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
install_module
from trytond.transaction import Transaction
@@ -163,6 +164,23 @@ class MPTTTestCase(unittest.TestCase):
transaction.cursor.rollback()
+ def test0060_update_only_if_parent_is_modified(self):
+ 'The left and right fields must only be updated if parent is modified'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ records = self.mptt.search([
+ ('parent', '=', None),
+ ])
+ with patch.object(self.mptt, '_update_tree') as mock:
+ self.mptt.write(records, {'name': 'Parent Records'})
+ self.assertFalse(mock.called)
+
+ first_parent, second_parent = records[:2]
+ self.mptt.write(list(first_parent.childs), {
+ 'parent': second_parent.id,
+ })
+
+ self.assertTrue(mock.called)
+
def suite():
return unittest.TestLoader().loadTestsFromTestCase(MPTTTestCase)
diff --git a/trytond/tests/test_protocols.py b/trytond/tests/test_protocols.py
new file mode 100644
index 0000000..ee445ca
--- /dev/null
+++ b/trytond/tests/test_protocols.py
@@ -0,0 +1,76 @@
+#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
+import datetime
+from decimal import Decimal
+
+from trytond.protocols.jsonrpc import JSONEncoder, JSONDecoder
+from trytond.protocols.xmlrpc import xmlrpclib
+
+
+class JSONTestCase(unittest.TestCase):
+ 'Test JSON'
+
+ def dumps_loads(self, value):
+ self.assertEqual(json.loads(
+ json.dumps(value, cls=JSONEncoder),
+ object_hook=JSONDecoder()), value)
+
+ def test_datetime(self):
+ 'Test datetime'
+ self.dumps_loads(datetime.datetime.now())
+
+ def test_date(self):
+ 'Test date'
+ self.dumps_loads(datetime.date.today())
+
+ def test_time(self):
+ 'Test time'
+ self.dumps_loads(datetime.datetime.now().time())
+
+ def test_buffer(self):
+ 'Test buffer'
+ self.dumps_loads(buffer('foo'))
+
+ def test_decimal(self):
+ 'Test Decimal'
+ self.dumps_loads(Decimal('3.141592653589793'))
+
+
+class XMLTestCase(unittest.TestCase):
+ 'Test XML'
+
+ def dumps_loads(self, value):
+ s = xmlrpclib.dumps((value,))
+ result, _ = xmlrpclib.loads(s)
+ result, = result
+ self.assertEqual(value, result)
+
+ def test_decimal(self):
+ 'Test Decimal'
+ self.dumps_loads(Decimal('3.141592653589793'))
+
+ def test_buffer(self):
+ 'Test buffer'
+ self.dumps_loads(buffer('foo'))
+
+ def test_date(self):
+ 'Test date'
+ self.dumps_loads(datetime.date.today())
+
+ def test_time(self):
+ 'Test time'
+ self.dumps_loads(datetime.datetime.now().time())
+
+ def test_none(self):
+ 'Test None'
+ self.dumps_loads(None)
+
+
+def suite():
+ suite_ = unittest.TestSuite()
+ suite_.addTests(unittest.TestLoader().loadTestsFromTestCase(JSONTestCase))
+ suite_.addTests(unittest.TestLoader().loadTestsFromTestCase(XMLTestCase))
+ return suite_
diff --git a/trytond/tests/test_sequence.py b/trytond/tests/test_sequence.py
index 727bb1c..802372d 100644
--- a/trytond/tests/test_sequence.py
+++ b/trytond/tests/test_sequence.py
@@ -6,6 +6,7 @@ import datetime
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
install_module
from trytond.transaction import Transaction
+from trytond.exceptions import UserError
class SequenceTestCase(unittest.TestCase):
@@ -58,7 +59,7 @@ class SequenceTestCase(unittest.TestCase):
self.assertNotEqual(self.sequence.get_id(sequence), timestamp)
next_timestamp = self.sequence._timestamp(sequence)
- self.assertRaises(Exception, self.sequence.write, [sequence], {
+ self.assertRaises(UserError, self.sequence.write, [sequence], {
'last_timestamp': next_timestamp + 100,
})
@@ -82,7 +83,7 @@ class SequenceTestCase(unittest.TestCase):
self.assertNotEqual(self.sequence.get_id(sequence), timestamp)
next_timestamp = self.sequence._timestamp(sequence)
- self.assertRaises(Exception, self.sequence.write, [sequence], {
+ self.assertRaises(UserError, self.sequence.write, [sequence], {
'last_timestamp': next_timestamp + 100,
})
diff --git a/trytond/tests/test_tools.py b/trytond/tests/test_tools.py
index 0b9523c..3a5c3ad 100644
--- a/trytond/tests/test_tools.py
+++ b/trytond/tests/test_tools.py
@@ -60,7 +60,7 @@ class ToolsTestCase(unittest.TestCase):
def test0060safe_eval_builtin(self):
'Attempt to access a unsafe builtin'
- self.assertRaises(Exception, safe_eval, "open('test.txt', 'w')")
+ self.assertRaises(NameError, safe_eval, "open('test.txt', 'w')")
def test0061safe_eval_getattr(self):
'Attempt to get arround direct attr access'
@@ -68,12 +68,12 @@ class ToolsTestCase(unittest.TestCase):
def test0062safe_eval_func_globals(self):
'Attempt to access global enviroment where fun was defined'
- self.assertRaises(Exception, safe_eval,
+ self.assertRaises(SyntaxError, safe_eval,
"def x(): pass; print x.func_globals")
def test0063safe_eval_lowlevel(self):
"Lowlevel tricks to access 'object'"
- self.assertRaises(Exception, safe_eval,
+ self.assertRaises(ValueError, safe_eval,
"().__class__.mro()[1].__subclasses__()")
def test0070datetime_strftime(self):
diff --git a/trytond/tests/test_trigger.py b/trytond/tests/test_trigger.py
index 23bd815..200aedd 100644
--- a/trytond/tests/test_trigger.py
+++ b/trytond/tests/test_trigger.py
@@ -10,6 +10,7 @@ from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
install_module
from trytond.tests.trigger import TRIGGER_LOGS
from trytond.transaction import Transaction
+from trytond.exceptions import UserError
class TriggerTestCase(unittest.TestCase):
@@ -24,8 +25,7 @@ class TriggerTestCase(unittest.TestCase):
def test0010constraints(self):
'Test constraints'
- with Transaction().start(DB_NAME, USER,
- context=CONTEXT) as transaction:
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
model, = self.model.search([
('model', '=', 'test.triggered'),
])
@@ -43,25 +43,27 @@ class TriggerTestCase(unittest.TestCase):
}
self.assert_(self.trigger.create([values]))
- # on_exclusive
- for i in range(1, 4):
- for combination in combinations(
- ['create', 'write', 'delete'], i):
+ # on_exclusive
+ for i in range(1, 4):
+ for combination in combinations(
+ ['create', 'write', 'delete'], i):
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
combination_values = values.copy()
for mode in combination:
combination_values['on_%s' % mode] = True
- self.assertRaises(Exception, self.trigger.create,
+ self.assertRaises(UserError, self.trigger.create,
[combination_values])
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
# check_condition
condition_values = values.copy()
condition_values['condition'] = '='
- self.assertRaises(Exception, self.trigger.create,
+ self.assertRaises(UserError, self.trigger.create,
[condition_values])
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
# Restart the cache on the get_triggers method of ir.trigger
self.trigger._get_triggers_cache.clear()
- transaction.cursor.rollback()
def test0020on_create(self):
'Test on_create'
diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py
index dd0cf7d..94bfab9 100644
--- a/trytond/tests/test_tryton.py
+++ b/trytond/tests/test_tryton.py
@@ -7,15 +7,17 @@ import unittest
import doctest
from lxml import etree
-from trytond.config import CONFIG
from trytond.pool import Pool
from trytond import backend
-from trytond.protocols.dispatcher import create
+from trytond.protocols.dispatcher import create, drop
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', 'doctest_dropdb',
+ 'install_module', 'test_view', 'test_depends',
+ 'doctest_setup', 'doctest_teardown',
'suite', 'all_suite', 'modules_suite']
Pool.start()
@@ -26,6 +28,7 @@ DB_NAME = os.environ['DB_NAME']
DB = backend.get('Database')(DB_NAME)
Pool.test = True
POOL = Pool(DB_NAME)
+security.check_super = lambda *a, **k: True
class ModelViewTestCase(unittest.TestCase):
@@ -38,8 +41,8 @@ class ModelViewTestCase(unittest.TestCase):
def test0000test(self):
'Test test'
- self.assertRaises(Exception, install_module, 'nosuchmodule')
- self.assertRaises(Exception, test_view, 'nosuchmodule')
+ self.assertRaises(UserError, install_module, 'nosuchmodule')
+ self.assertRaises(UserError, test_view, 'nosuchmodule')
def test0010ir(self):
'Test ir'
@@ -71,13 +74,7 @@ def install_module(name):
'''
Install module for the tested database
'''
- Database = backend.get('Database')
- database = Database().connect()
- cursor = database.cursor()
- databases = database.list(cursor)
- cursor.close()
- if DB_NAME not in databases:
- create(DB_NAME, CONFIG['admin_passwd'], 'en_US', USER_PASSWORD)
+ create_db()
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
Module = POOL.get('ir.module.module')
@@ -176,16 +173,32 @@ def test_depends():
list(depends - set(model._fields)), mname, fname))
-def doctest_dropdb(test):
- '''Remove SQLite memory database'''
- from trytond.backend.sqlite.database import Database as SQLiteDatabase
- database = SQLiteDatabase().connect()
- cursor = database.cursor(autocommit=True)
- try:
- SQLiteDatabase.drop(cursor, ':memory:')
- cursor.commit()
- finally:
- cursor.close()
+def db_exist():
+ Database = backend.get('Database')
+ database = Database().connect()
+ cursor = database.cursor()
+ databases = database.list(cursor)
+ cursor.close()
+ return DB_NAME in databases
+
+
+def create_db():
+ if not db_exist():
+ create(DB_NAME, None, 'en_US', USER_PASSWORD)
+
+
+def drop_db():
+ if db_exist():
+ drop(DB_NAME, None)
+
+
+def drop_create():
+ if db_exist:
+ drop_db()
+ create_db()
+
+doctest_setup = lambda test: drop_create()
+doctest_teardown = lambda test: drop_db()
def suite():
diff --git a/trytond/tests/test_union.py b/trytond/tests/test_union.py
new file mode 100644
index 0000000..723c38c
--- /dev/null
+++ b/trytond/tests/test_union.py
@@ -0,0 +1,102 @@
+#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
+from trytond.transaction import Transaction
+
+
+class UnionMixinTestCase(unittest.TestCase):
+ 'Test UnionMixin'
+
+ def setUp(self):
+ install_module('tests')
+
+ def test_union(self):
+ 'Test union'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ Union = POOL.get('test.union')
+ for i in range(1, 4):
+ Model = POOL.get('test.model.union%s' % i)
+ for j in range(3):
+ model = Model(name='%s - %s' % (i, j))
+ if hasattr(Model, 'optional'):
+ model.optional = 'optional'
+ model.save()
+
+ self.assertEqual(len(Union.search([])), 9)
+ record, = Union.search([
+ ('name', '=', '2 - 2'),
+ ])
+ self.assertEqual(record.name, '2 - 2')
+ self.assertEqual(record.optional, None)
+
+ record, = Union.search([
+ ('optional', '=', 'optional'),
+ ], limit=1)
+ self.assertEqual(record.optional, 'optional')
+
+ def test_union_union(self):
+ 'Test union of union'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ Union = POOL.get('test.union.union')
+ for i in range(1, 5):
+ Model = POOL.get('test.model.union%s' % i)
+ for j in range(3):
+ model = Model(name='%s - %s' % (i, j))
+ model.save()
+
+ self.assertEqual(len(Union.search([])), 12)
+ record, = Union.search([
+ ('name', '=', '2 - 2'),
+ ])
+ self.assertEqual(record.name, '2 - 2')
+ record, = Union.search([
+ ('name', '=', '4 - 1'),
+ ])
+ self.assertEqual(record.name, '4 - 1')
+
+ def test_union_tree(self):
+ 'Test union tree'
+ with Transaction().start(DB_NAME, USER, context=CONTEXT):
+ Union = POOL.get('test.union.tree')
+ Model1 = POOL.get('test.model.union.tree1')
+ Model2 = POOL.get('test.model.union.tree2')
+
+ roots = Model1.create([{
+ 'name': 'Root1',
+ }, {
+ 'name': 'Root2',
+ }, {
+ 'name': 'Root3',
+ }])
+
+ Model2.create([{
+ 'name': 'Not child', # To shift ids
+ }, {
+ 'name': 'Child1',
+ 'parent': roots[1].id,
+ }, {
+ 'name': 'Child2',
+ 'parent': roots[1].id,
+ }, {
+ 'name': 'Child3',
+ 'parent': roots[2].id,
+ }])
+
+ uroots = Union.search([('parent', '=', None)],
+ order=[('name', 'ASC')])
+
+ self.assertEqual(len(uroots), 4)
+ names = [r.name for r in uroots]
+ self.assertEqual(names, ['Not child', 'Root1', 'Root2', 'Root3'])
+ childs = [len(r.childs) for r in uroots]
+ self.assertEqual(childs, [0, 0, 2, 1])
+ child_names = sorted((r.name for r in uroots[2].childs))
+ self.assertEqual(child_names, ['Child1', 'Child2'])
+ self.assertEqual(uroots[3].childs[0].name, 'Child3')
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(UnionMixinTestCase)
diff --git a/trytond/tools/misc.py b/trytond/tools/misc.py
index 80caf52..ebe7c66 100644
--- a/trytond/tools/misc.py
+++ b/trytond/tools/misc.py
@@ -12,10 +12,13 @@ import smtplib
import dis
from decimal import Decimal
from array import array
+from itertools import islice
from sql import Literal
from sql.operators import Or
-from trytond.config import CONFIG
+
from trytond.const import OPERATORS
+from trytond.config import config, parse_uri
+from trytond.transaction import Transaction
def find_in_path(name):
@@ -32,42 +35,7 @@ def find_in_path(name):
return name
-def find_pg_tool(name):
- if CONFIG['pg_path'] and CONFIG['pg_path'] != 'None':
- return os.path.join(CONFIG['pg_path'], name)
- else:
- return find_in_path(name)
-
-
-def exec_pg_command(name, *args):
- prog = find_pg_tool(name)
- if not prog:
- raise Exception('Couldn\'t find %s' % name)
- args2 = (os.path.basename(prog),) + args
- return os.spawnv(os.P_WAIT, prog, args2)
-
-
-def exec_pg_command_pipe(name, *args):
- prog = find_pg_tool(name)
- if not prog:
- raise Exception('Couldn\'t find %s' % name)
- if os.name == "nt":
- cmd = '"' + prog + '" ' + ' '.join(args)
- else:
- cmd = prog + ' ' + ' '.join(args)
-
- # if db_password is set in configuration we should pass
- # an environment variable PGPASSWORD to our subprocess
- # see libpg documentation
- child_env = dict(os.environ)
- if CONFIG['db_password']:
- child_env['PGPASSWORD'] = CONFIG['db_password']
- pipe = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
- stdout=subprocess.PIPE, env=child_env)
- return pipe
-
-
-def exec_command_pipe(name, *args):
+def exec_command_pipe(name, *args, **kwargs):
prog = find_in_path(name)
if not prog:
raise Exception('Couldn\'t find %s' % name)
@@ -75,7 +43,11 @@ def exec_command_pipe(name, *args):
cmd = '"' + prog + '" ' + ' '.join(args)
else:
cmd = prog + ' ' + ' '.join(args)
- return os.popen2(cmd, 'b')
+ child_env = dict(os.environ)
+ if kwargs.get('env'):
+ child_env.update(kwargs['env'])
+ return subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, env=child_env)
def file_open(name, mode="r", subdir='modules'):
@@ -132,17 +104,17 @@ def get_smtp_server():
:return: A SMTP instance. The quit() method must be call when all
the calls to sendmail() have been made.
"""
- if CONFIG['smtp_ssl']:
- smtp_server = smtplib.SMTP_SSL(CONFIG['smtp_server'],
- CONFIG['smtp_port'])
+ uri = parse_uri(config.get('email', 'uri'))
+ if uri.scheme.startswith('smtps'):
+ smtp_server = smtplib.SMTP_SSL(uri.hostname, uri.port)
else:
- smtp_server = smtplib.SMTP(CONFIG['smtp_server'], CONFIG['smtp_port'])
+ smtp_server = smtplib.SMTP(uri.hostname, uri.port)
- if CONFIG['smtp_tls']:
+ if 'tls' in uri.scheme:
smtp_server.starttls()
- if CONFIG['smtp_user'] and CONFIG['smtp_password']:
- smtp_server.login(CONFIG['smtp_user'], CONFIG['smtp_password'])
+ if uri.username and uri.password:
+ smtp_server.login(uri.username, uri.password)
return smtp_server
@@ -328,6 +300,7 @@ def reduce_ids(field, ids):
'''
Return a small SQL expression for the list of ids and the sql column
'''
+ ids = list(ids)
if not ids:
return Literal(False)
assert all(x.is_integer() for x in ids if isinstance(x, float)), \
@@ -442,3 +415,11 @@ def reduce_domain(domain):
else:
result.append(arg)
return result
+
+
+def grouped_slice(records, count=None):
+ 'Grouped slice'
+ if count is None:
+ count = Transaction().cursor.IN_MAX
+ for i in xrange(0, len(records), count):
+ yield islice(records, i, i + count)
diff --git a/trytond/url.py b/trytond/url.py
index ce6dd11..be17878 100644
--- a/trytond/url.py
+++ b/trytond/url.py
@@ -5,12 +5,12 @@ import encodings.idna
import urllib
import socket
-from trytond.config import CONFIG
+from trytond.config import config
from trytond.transaction import Transaction
__all__ = ['URLMixin']
-HOSTNAME = (CONFIG['hostname_jsonrpc']
+HOSTNAME = (config.get('jsonrpc', 'hostname')
or unicode(socket.getfqdn(), 'utf8'))
HOSTNAME = '.'.join(encodings.idna.ToASCII(part) if part else ''
for part in HOSTNAME.split('.'))
diff --git a/trytond/version.py b/trytond/version.py
index 063c087..a68a845 100644
--- a/trytond/version.py
+++ b/trytond/version.py
@@ -1,6 +1,6 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
PACKAGE = "trytond"
-VERSION = "3.2.3"
+VERSION = "3.4.0"
LICENSE = "GPL-3"
WEBSITE = "http://www.tryton.org/"
diff --git a/trytond/webdav/locale/sl_SI.po b/trytond/webdav/locale/es_EC.po
similarity index 72%
copy from trytond/webdav/locale/sl_SI.po
copy to trytond/webdav/locale/es_EC.po
index c9550c2..cac92a0 100644
--- a/trytond/webdav/locale/sl_SI.po
+++ b/trytond/webdav/locale/es_EC.po
@@ -7,28 +7,28 @@ msgid ""
"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 "
-"ustvariti, ker že obstaja zbirka z istim imenom."
+"No puede crear un adjunto llamado \"%(attachment)s\" en la colección "
+"\"%(collections)s\" porque ya existe una colección con este nombre."
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
-msgstr "Ime zbirke mora biti edinstveno znotraj zbirke."
+msgstr "¡El nombre de la colección debe ser único dentro de una colección!"
msgctxt "error:webdav.collection:"
msgid ""
"You can not create a collection named \"%(parent)s\" in collection "
"\"%(child)s\" because there is already a file with that name."
msgstr ""
-"Zbirke z imenom \"%(parent)s\" v zbirki \"%(child)s\" ni možno ustvariti, "
-"ker že obstaja datoteka z istim imenom."
+"No puede crear una colección llamada \"%(parent)s\" en la colección "
+"\"%(child)s\" porque ya existe un archivo con ese nombre."
msgctxt "field:ir.attachment,path:"
msgid "Path"
-msgstr "Pot"
+msgstr "Ruta"
msgctxt "field:ir.attachment,shares:"
msgid "Shares"
-msgstr "Skupne rabe"
+msgstr "Recursos Compartidos"
msgctxt "field:ir.attachment,url:"
msgid "URL"
@@ -36,23 +36,23 @@ msgstr "URL"
msgctxt "field:webdav.collection,childs:"
msgid "Children"
-msgstr "Podzbirke"
+msgstr "Hijos"
msgctxt "field:webdav.collection,complete_name:"
msgid "Complete Name"
-msgstr "Polno ime"
+msgstr "Nombre Completo"
msgctxt "field:webdav.collection,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Fecha de Creación"
msgctxt "field:webdav.collection,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Creado por Usuario"
msgctxt "field:webdav.collection,domain:"
msgid "Domain"
-msgstr "Domena"
+msgstr "Dominio"
msgctxt "field:webdav.collection,id:"
msgid "ID"
@@ -60,39 +60,39 @@ msgstr "ID"
msgctxt "field:webdav.collection,model:"
msgid "Model"
-msgstr "Model"
+msgstr "Modelo"
msgctxt "field:webdav.collection,name:"
msgid "Name"
-msgstr "Naziv"
+msgstr "Nombre"
msgctxt "field:webdav.collection,parent:"
msgid "Parent"
-msgstr "Prednik"
+msgstr "Padre"
msgctxt "field:webdav.collection,rec_name:"
msgid "Name"
-msgstr "Ime"
+msgstr "Nombre"
msgctxt "field:webdav.collection,write_date:"
msgid "Write Date"
-msgstr "Zapisano"
+msgstr "Fecha de Modificación"
msgctxt "field:webdav.collection,write_uid:"
msgid "Write User"
-msgstr "Zapisal"
+msgstr "Modificado por Usuario"
msgctxt "field:webdav.share,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Fecha de Creación"
msgctxt "field:webdav.share,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Creado por Usuario"
msgctxt "field:webdav.share,expiration_date:"
msgid "Expiration Date"
-msgstr "Datum veljavnosti"
+msgstr "Fecha de Expiración"
msgctxt "field:webdav.share,id:"
msgid "ID"
@@ -100,19 +100,19 @@ msgstr "ID"
msgctxt "field:webdav.share,key:"
msgid "Key"
-msgstr "Ključ"
+msgstr "Clave"
msgctxt "field:webdav.share,note:"
msgid "Note"
-msgstr "Opomba"
+msgstr "Nota"
msgctxt "field:webdav.share,path:"
msgid "Path"
-msgstr "Pot"
+msgstr "Ruta"
msgctxt "field:webdav.share,rec_name:"
msgid "Name"
-msgstr "Ime"
+msgstr "Nombre"
msgctxt "field:webdav.share,url:"
msgid "URL"
@@ -120,39 +120,39 @@ msgstr "URL"
msgctxt "field:webdav.share,user:"
msgid "User"
-msgstr "Uporabnik"
+msgstr "Usuario"
msgctxt "field:webdav.share,write_date:"
msgid "Write Date"
-msgstr "Zapisano"
+msgstr "Fecha de Modificación"
msgctxt "field:webdav.share,write_uid:"
msgid "Write User"
-msgstr "Zapisal"
+msgstr "Modificado por Usuario"
msgctxt "model:ir.action,name:act_collection_list"
msgid "Collections"
-msgstr "Zbirke"
+msgstr "Colecciones"
msgctxt "model:ir.action,name:act_collection_tree"
msgid "Collections"
-msgstr "Zbirke"
+msgstr "Colecciones"
msgctxt "model:ir.action,name:act_share_list"
msgid "Shares"
-msgstr "Skupne rabe"
+msgstr "Recursos Compartidos"
msgctxt "model:ir.ui.menu,name:menu_collection_list"
msgid "Collections"
-msgstr "Zbirke"
+msgstr "Colecciones"
msgctxt "model:ir.ui.menu,name:menu_collection_tree"
msgid "Collections"
-msgstr "Zbirke"
+msgstr "Colecciones"
msgctxt "model:ir.ui.menu,name:menu_share_list"
msgid "Shares"
-msgstr "Skupne rabe"
+msgstr "Recursos Compartidos"
msgctxt "model:ir.ui.menu,name:menu_webdav"
msgid "WebDAV"
@@ -160,11 +160,11 @@ msgstr "WebDAV"
msgctxt "model:webdav.collection,name:"
msgid "Collection"
-msgstr "Zbirka"
+msgstr "Colección"
msgctxt "model:webdav.share,name:"
msgid "Share"
-msgstr "Skupna raba"
+msgstr "Recurso Compartido"
msgctxt "view:ir.attachment:"
msgid "WebDAV"
@@ -172,16 +172,16 @@ msgstr "WebDAV"
msgctxt "view:webdav.collection:"
msgid "Collection"
-msgstr "Zbirka"
+msgstr "Colección"
msgctxt "view:webdav.collection:"
msgid "Collections"
-msgstr "Zbirke"
+msgstr "Colecciones"
msgctxt "view:webdav.share:"
msgid "Share"
-msgstr "Skupna raba"
+msgstr "Recurso Compartido"
msgctxt "view:webdav.share:"
msgid "Shares"
-msgstr "Skupne rabe"
+msgstr "Recursos Compartidos"
diff --git a/trytond/webdav/locale/fr_FR.po b/trytond/webdav/locale/fr_FR.po
index c94374f..1d0062d 100644
--- a/trytond/webdav/locale/fr_FR.po
+++ b/trytond/webdav/locale/fr_FR.po
@@ -7,20 +7,20 @@ msgid ""
"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 "
-"collection \"%(collection)s\" car il y a déjà une collection avec ce nom."
+"Vous ne pouvez créer une pièce jointe nommée « %(attachment)s » dans la "
+"collection « %(collection)s » car il y a déjà une collection avec ce nom."
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
-msgstr "Le nom de collection doit être unique au sein d'une collection !"
+msgstr "Le nom de collection doit être unique au sein d'une collection !"
msgctxt "error:webdav.collection:"
msgid ""
"You can not create a collection named \"%(parent)s\" in collection "
"\"%(child)s\" because there is already a file with that name."
msgstr ""
-"Vous ne pouvez pas créer une collection nommée \"%(parent)s\" dans la "
-"collection \"%(child)s\" car il y a déjà un fichier avec ce nom."
+"Vous ne pouvez pas créer une collection nommée « %(parent)s » dans la "
+"collection « %(child)s » car il y a déjà un fichier avec ce nom."
msgctxt "field:ir.attachment,path:"
msgid "Path"
diff --git a/trytond/webdav/locale/sl_SI.po b/trytond/webdav/locale/sl_SI.po
index c9550c2..b1f2bb1 100644
--- a/trytond/webdav/locale/sl_SI.po
+++ b/trytond/webdav/locale/sl_SI.po
@@ -8,7 +8,7 @@ msgid ""
"\"%(collection)s\" because there is already a collection with that name."
msgstr ""
"Priloge z imenom \"%(attachment)s\" v zbirki \"%(collection)s\" ni možno "
-"ustvariti, ker že obstaja zbirka z istim imenom."
+"izdelati, ker že obstaja zbirka z istim imenom."
msgctxt "error:webdav.collection:"
msgid "The collection name must be unique inside a collection!"
@@ -19,8 +19,8 @@ msgid ""
"You can not create a collection named \"%(parent)s\" in collection "
"\"%(child)s\" because there is already a file with that name."
msgstr ""
-"Zbirke z imenom \"%(parent)s\" v zbirki \"%(child)s\" ni možno ustvariti, "
-"ker že obstaja datoteka z istim imenom."
+"Zbirke z imenom \"%(parent)s\" v zbirki \"%(child)s\" ni možno izdelati, ker"
+" že obstaja datoteka z istim imenom."
msgctxt "field:ir.attachment,path:"
msgid "Path"
@@ -44,11 +44,11 @@ msgstr "Polno ime"
msgctxt "field:webdav.collection,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:webdav.collection,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:webdav.collection,domain:"
msgid "Domain"
@@ -84,11 +84,11 @@ msgstr "Zapisal"
msgctxt "field:webdav.share,create_date:"
msgid "Create Date"
-msgstr "Ustvarjeno"
+msgstr "Izdelano"
msgctxt "field:webdav.share,create_uid:"
msgid "Create User"
-msgstr "Ustvaril"
+msgstr "Izdelal"
msgctxt "field:webdav.share,expiration_date:"
msgid "Expiration Date"
diff --git a/trytond/webdav/webdav.py b/trytond/webdav/webdav.py
index ef55270..9817f49 100644
--- a/trytond/webdav/webdav.py
+++ b/trytond/webdav/webdav.py
@@ -18,8 +18,9 @@ from trytond.model import ModelView, ModelSQL, fields
from trytond.tools import reduce_ids
from trytond.transaction import Transaction
from trytond.pool import Pool
-from trytond.config import CONFIG
+from trytond.config import config
from trytond.pyson import Eval
+from trytond.tools import grouped_slice
__all__ = [
'Collection', 'Share', 'Attachment',
@@ -27,11 +28,11 @@ __all__ = [
def get_webdav_url():
- if CONFIG['ssl_webdav']:
+ if config.get('ssl', 'privatekey'):
protocol = 'https'
else:
protocol = 'http'
- hostname = (CONFIG['hostname_webdav']
+ hostname = (config.get('webdav', 'hostname')
or unicode(socket.getfqdn(), 'utf8'))
hostname = '.'.join(encodings.idna.ToASCII(part) for part in
hostname.split('.'))
@@ -434,8 +435,7 @@ class Collection(ModelSQL, ModelView):
res = None
cursor = Transaction().cursor
table = Model.__table__()
- for i in range(0, len(ids), cursor.IN_MAX):
- sub_ids = ids[i:i + cursor.IN_MAX]
+ for sub_ids in grouped_slice(ids):
red_sql = reduce_ids(table.id, sub_ids)
cursor.execute(*table.select(table.id,
Extract('EPOCH', table.create_date),
@@ -471,8 +471,7 @@ class Collection(ModelSQL, ModelView):
res = None
cursor = Transaction().cursor
table = Model.__table__()
- for i in range(0, len(ids), cursor.IN_MAX):
- sub_ids = ids[i:i + cursor.IN_MAX]
+ for sub_ids in grouped_slice(ids):
red_sql = reduce_ids(table.id, sub_ids)
cursor.execute(*table.select(table.id,
Extract('EPOCH',
diff --git a/trytond/wizard/wizard.py b/trytond/wizard/wizard.py
index c3c725e..e92c35c 100644
--- a/trytond/wizard/wizard.py
+++ b/trytond/wizard/wizard.py
@@ -13,10 +13,11 @@ from trytond.pool import Pool, PoolBase
from trytond.transaction import Transaction
from trytond.error import WarningErrorMixin
from trytond.url import URLMixin
-from trytond.protocols.jsonrpc import object_hook, JSONEncoder
+from trytond.protocols.jsonrpc import JSONDecoder, JSONEncoder
from trytond.model.fields import states_validate
from trytond.pyson import PYSONEncoder
from trytond.rpc import RPC
+from trytond.exceptions import UserError
class Button(object):
@@ -152,7 +153,7 @@ class Wizard(WarningErrorMixin, URLMixin, PoolBase):
cls.__rpc__ = {
'create': RPC(readonly=False),
'delete': RPC(readonly=False),
- 'execute': RPC(readonly=False),
+ 'execute': RPC(readonly=False, check_access=False),
}
cls._error_messages = {}
@@ -181,9 +182,31 @@ class Wizard(WarningErrorMixin, URLMixin, PoolBase):
Translation.register_error_messages(cls, module_name)
@classmethod
+ def check_access(cls):
+ pool = Pool()
+ ModelAccess = pool.get('ir.model.access')
+ ActionWizard = pool.get('ir.action.wizard')
+ User = pool.get('res.user')
+ context = Transaction().context
+
+ if Transaction().user == 0:
+ return
+
+ model = context.get('active_model')
+ if model:
+ ModelAccess.check(model, 'read')
+ ModelAccess.check(model, 'write')
+ groups = set(User.get_groups())
+ wizard_groups = ActionWizard.get_groups(cls.__name__,
+ action_id=context.get('action_id'))
+ if wizard_groups and not groups & wizard_groups:
+ raise UserError('Calling wizard %s is not allowed!' % cls.__name__)
+
+ @classmethod
def create(cls):
"Create a session"
Session = Pool().get('ir.session.wizard')
+ cls.check_access()
return (Session.create([{}])[0].id, cls.start_state, cls.end_state)
@classmethod
@@ -215,6 +238,7 @@ class Wizard(WarningErrorMixin, URLMixin, PoolBase):
- ``defaults``: a dictionary with default values
- ``buttons``: a list of buttons
'''
+ cls.check_access()
wizard = cls(session_id)
for key, values in data.iteritems():
record = getattr(wizard, key)
@@ -265,7 +289,7 @@ class Wizard(WarningErrorMixin, URLMixin, PoolBase):
self._session_id = session_id
session = Session(session_id)
data = json.loads(session.data.encode('utf-8'),
- object_hook=object_hook)
+ object_hook=JSONDecoder())
for state_name, state in self.states.iteritems():
if isinstance(state, StateView):
Target = pool.get(state.model_name)
--
tryton-server
More information about the tryton-debian-vcs
mailing list