diff --git a/CHANGELOG b/CHANGELOG
index e7cb59ece6..72c3b15b42 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,4 +1,9 @@
* Remove use of python code in workflow XML
+* Use SQL sequence on PostgreSQL for ir.sequence
+* Allow to specify more than one interface
+* Use unoconv for report format conversion
+* Allow to use any Opendocument as report template
+* Drop NetRPC and activate JSON-RPC by default
* Remove ir.action.wizard_size
* Rename expand and fill attributes into yexpand and yfill
* Add xalign and yalign as field attributes
diff --git a/COPYRIGHT b/COPYRIGHT
index 6c994d4a71..231a7a99bc 100644
--- a/COPYRIGHT
+++ b/COPYRIGHT
@@ -2,6 +2,7 @@ Copyright (C) 2004-2008 Tiny SPRL.
Copyright (C) 2007-2011 Cédric Krier.
Copyright (C) 2007-2011 Bertrand Chenal.
Copyright (C) 2008-2011 B2CK SPRL.
+Copyright (C) 2011 Openlabs Technologies & Consulting (P) Ltd.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/doc/topics/install.rst b/doc/topics/install.rst
index ed496ac66a..dfa3832437 100644
--- a/doc/topics/install.rst
+++ b/doc/topics/install.rst
@@ -17,7 +17,7 @@ Prerequisites
* Optional: pywebdav 0.9.3 or later (http://code.google.com/p/pywebdav/)
* Optional: pydot (http://code.google.com/p/pydot/)
* Optional: pytz (http://pytz.sourceforge.net/)
- * Optional: openoffice-python (http://pypi.python.org/pypi/openoffice-python/)
+ * Optional: unoconv http://dag.wieers.com/home-made/unoconv/)
* Optional: sphinx (http://sphinx.pocoo.org/)
Install Tryton
diff --git a/doc/topics/reports/index.rst b/doc/topics/reports/index.rst
index 96a70b11ad..7adeab0f2a 100644
--- a/doc/topics/reports/index.rst
+++ b/doc/topics/reports/index.rst
@@ -74,6 +74,8 @@ When defining an `ir.action.report` the following attributes are available:
example, my_module/my_style.odt. If no style is to be used this field
will be blank.
+ * ``template_extension``: The template format.
+
Report Usage
============
@@ -211,6 +213,7 @@ Then you must activate the new invoice report that exists in your new module:
account.invoice
my_module/invoice.odt
module_name/header_A4.odt
+ odt
Passing custom data to a report
diff --git a/etc/trytond.conf b/etc/trytond.conf
index f99ac12a04..d706c48199 100644
--- a/etc/trytond.conf
+++ b/etc/trytond.conf
@@ -3,33 +3,22 @@
[options]
# This is the hostname used when generating tryton URI
-#hostname =
-
-# Configure the IP address for trytond
-# If empty, the server will listen on all available interfaces
-#interface =
-
-# Activate the netrpc protocol
-#netrpc = True
-# Configure the port for the netrpc protocol to listen
-#netport = 8070
-
-# Activate the xml-rpc protocol
-#xmlrpc = False
-# Configure the port for the xml-rpc protocol to listen
-#xmlport = 8069
+#hostname =
# Activate the json-rpc protocol
-#jsonrpc = False
-# Configure the port for the json-rpc protocol to listen
-#jsonport = 8000
+jsonrpc = localhost:8000
+#ssl_jsonrpc = False
+
# 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 = False
-# Configure the port for the webdav protocol to listen
-#webdavport = 8080
+#webdav = *:8080
+#ssl_webdav = False
# Configure the database type
# allowed values are postgresql, sqlite, mysql
@@ -56,11 +45,6 @@
#pidfile = False
#logfile = False
-# Enable secure connection with SSL
-#secure_netrpc = False
-#secure_xmlrpc = False
-#secure_jsonrpc = False
-#secure_webdav = False
#privatekey = server.pem
#certificate = server.pem
@@ -75,9 +59,6 @@
# Configure the path to store attachments and sqlite database
#data_path = /var/lib/trytond
-# Limit the number of threads
-#max_thread = 40
-
# Allow to run more than one instance of trytond
#multi_server = False
@@ -96,3 +77,6 @@
# Enable cron
# cron = True
+
+# unoconv connection
+#unoconv = pipe,name=trytond;urp;StarOffice.ComponentContext
diff --git a/setup.py b/setup.py
index e21a377c6b..004c7b1dc8 100644
--- a/setup.py
+++ b/setup.py
@@ -63,7 +63,7 @@ setup(name=PACKAGE,
'PostgreSQL': ['psycopg2 >= 2.0'],
'MySQL': ['MySQL-python'],
'WebDAV': ['PyWebDAV >= 0.9.3'],
- 'PDF': ['openoffice-python'],
+ 'unoconv': ['unoconv'],
'SSL': ['pyOpenSSL'],
'graphviz': ['pydot'],
'timezone': ['pytz'],
diff --git a/trytond/config.py b/trytond/config.py
index e4c5fe374c..dea23a99f6 100644
--- a/trytond/config.py
+++ b/trytond/config.py
@@ -2,21 +2,36 @@
#this repository contains the full copyright notices and license terms.
import ConfigParser, os, sys
+def get_hostname(netloc):
+ if '[' in netloc and ']' in netloc:
+ return netloc.split(']')[0][1:]
+ elif ':' in netloc:
+ return netloc.split(':')[0]
+ else:
+ return netloc
+
+def get_port(netloc, protocol):
+ 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 = {
'hostname': None,
- 'interface': '',
- 'netrpc': True,
- 'netport': 8070,
- 'xmlrpc': False,
- 'xmlport': 8069,
- 'jsonrpc': False,
- 'jsonport': 8000,
+ 'jsonrpc': [('localhost', '8000')],
+ 'ssl_jsonrpc': False,
+ 'xmlrpc': [],
+ 'ssl_xmlrpc': False,
'jsondata_path': '/var/www/localhost/tryton',
- 'webdav': False,
- 'webdavport': 8080,
+ 'webdav': [],
+ 'ssl_webdav': False,
'db_type': 'postgresql',
'db_host': False,
'db_port': False,
@@ -31,10 +46,6 @@ class ConfigManager(object):
'debug_mode': False,
'pidfile': None,
'logfile': None,
- 'secure_netrpc': False,
- 'secure_xmlrpc': False,
- 'secure_jsonrpc': False,
- 'secure_webdav': False,
'privatekey': '/etc/ssl/trytond/server.key',
'certificate': '/etc/ssl/trytond/server.pem',
'smtp_server': 'localhost',
@@ -44,7 +55,6 @@ class ConfigManager(object):
'smtp_user': False,
'smtp_password': False,
'data_path': '/var/lib/trytond',
- 'max_thread': 40,
'multi_server': False,
'session_timeout': 600,
'psyco': False,
@@ -53,6 +63,7 @@ class ConfigManager(object):
'init': {},
'update': {},
'cron': True,
+ 'unoconv': 'pipe,name=trytond;urp;StarOffice.ComponentContext',
}
self.configfile = None
@@ -92,8 +103,9 @@ class ConfigManager(object):
value = True
if value == 'False' or value == 'false':
value = False
- if name in ('netport', 'xmlport', 'webdavport', 'jsonport'):
- value = int(value)
+ if name in ('netrpc', 'xmlrpc', 'jsonrpc', 'webdav'):
+ value = [(get_hostname(netloc).replace('*', ''),
+ get_port(netloc, name)) for netloc in value.split(',')]
self.options[name] = value
def get(self, key, default=None):
diff --git a/trytond/ir/action.py b/trytond/ir/action.py
index 8be92988f6..9198b1be24 100644
--- a/trytond/ir/action.py
+++ b/trytond/ir/action.py
@@ -226,10 +226,76 @@ class ActionReport(ModelSQL, ModelView):
style_content = fields.Function(fields.Binary('Style'),
'get_style_content')
direct_print = fields.Boolean('Direct Print')
- extension = fields.Selection(
- [('odt', 'ODT Document'),
- ('pdf', 'PDF Document'),],
- string='Extension', required=True)
+ template_extension = fields.Selection([
+ ('odt', 'OpenDocument Text'),
+ ('odp', 'OpenDocument Presentation'),
+ ('ods', 'OpenDocument Spreadsheet'),
+ ('odg', 'OpenDocument Graphics'),
+ ], string='Template Extension', required=True)
+ extension = fields.Selection([
+ ('', ''),
+ ('bib', 'BibTex'),
+ ('bmp', 'Windows Bitmap'),
+ ('csv', 'Text CSV'),
+ ('dbf', 'dBase'),
+ ('dif', 'Data Interchange Format'),
+ ('doc', 'Microsoft Word 97/2000/XP'),
+ ('doc6', 'Microsoft Word 6.0'),
+ ('doc95', 'Microsoft Word 95'),
+ ('docbook', 'DocBook'),
+ ('emf', 'Enhanced Metafile'),
+ ('eps', 'Encapsulated PostScript'),
+ ('gif', 'Graphics Interchange Format'),
+ ('html', 'HTML Document'),
+ ('jpg', 'Joint Photographic Experts Group'),
+ ('met', 'OS/2 Metafile'),
+ ('ooxml', 'Microsoft Office Open XML'),
+ ('pbm', 'Portable Bitmap'),
+ ('pct', 'Mac Pict'),
+ ('pdb', 'AportisDoc (Palm)'),
+ ('pdf', 'Portable Document Format'),
+ ('pgm', 'Portable Graymap'),
+ ('png', 'Portable Network Graphic'),
+ ('ppm', 'Portable Pixelmap'),
+ ('ppt', 'Microsoft PowerPoint 97/2000/XP'),
+ ('psw', 'Pocket Word'),
+ ('pwp', 'PlaceWare'),
+ ('pxl', 'Pocket Excel'),
+ ('ras', 'Sun Raster Image'),
+ ('rtf', 'Rich Text Format'),
+ ('latex', 'LaTeX 2e'),
+ ('sda', 'StarDraw 5.0 (OpenOffice.org Impress)'),
+ ('sdc', 'StarCalc 5.0'),
+ ('sdc4', 'StarCalc 4.0'),
+ ('sdc3', 'StarCalc 3.0'),
+ ('sdd', 'StarImpress 5.0'),
+ ('sdd3', 'StarDraw 3.0 (OpenOffice.org Impress)'),
+ ('sdd4', 'StarImpress 4.0'),
+ ('sdw', 'StarWriter 5.0'),
+ ('sdw4', 'StarWriter 4.0'),
+ ('sdw3', 'StarWriter 3.0'),
+ ('slk', 'SYLK'),
+ ('svg', 'Scalable Vector Graphics'),
+ ('svm', 'StarView Metafile'),
+ ('swf', 'Macromedia Flash (SWF)'),
+ ('sxc', 'OpenOffice.org 1.0 Spreadsheet'),
+ ('sxi', 'OpenOffice.org 1.0 Presentation'),
+ ('sxd', 'OpenOffice.org 1.0 Drawing'),
+ ('sxd3', 'StarDraw 3.0'),
+ ('sxd5', 'StarDraw 5.0'),
+ ('sxw', 'Open Office.org 1.0 Text Document'),
+ ('text', 'Text Encoded'),
+ ('tiff', 'Tagged Image File Format'),
+ ('txt', 'Plain Text'),
+ ('wmf', 'Windows Metafile'),
+ ('xhtml', 'XHTML Document'),
+ ('xls', 'Microsoft Excel 97/2000/XP'),
+ ('xls5', 'Microsoft Excel 5.0'),
+ ('xls95', 'Microsoft Excel 95'),
+ ('xpm', 'X PixMap'),
+ ],
+ string='Extension', help='Leave empty for the same as template, '
+ 'see unoconv documentation for compatible format')
module = fields.Char('Module', readonly=True, select=1)
email = fields.Char('Email')
pyson_email = fields.Function(fields.Char('PySON Email'), 'get_pyson')
@@ -269,6 +335,12 @@ class ActionReport(ModelSQL, ModelView):
TableHandler.dropTable(cursor, 'ir.action.report.outputformat',
'ir_action_report_outputformat')
+ # Migrate from 2.0 remove required on extension
+ table.not_null_action('extension', action='remove')
+ cursor.execute('UPDATE "' + self._table + '" '
+ 'SET extension = %s '
+ 'WHERE extension = %s', ('', 'odt'))
+
def default_type(self):
return 'ir.action.report'
@@ -278,9 +350,12 @@ class ActionReport(ModelSQL, ModelView):
def default_direct_print(self):
return False
- def default_extension(self):
+ def default_template_extension(self):
return 'odt'
+ def default_extension(self):
+ return ''
+
def default_module(self):
return Transaction().context.get('module') or ''
diff --git a/trytond/ir/action.xml b/trytond/ir/action.xml
index 08a28fbad6..9a5318655b 100644
--- a/trytond/ir/action.xml
+++ b/trytond/ir/action.xml
@@ -92,9 +92,11 @@ this repository contains the full copyright notices and license terms. -->
-
+
+
+
diff --git a/trytond/ir/sequence.py b/trytond/ir/sequence.py
index 9371bb102b..70313202eb 100644
--- a/trytond/ir/sequence.py
+++ b/trytond/ir/sequence.py
@@ -8,6 +8,10 @@ from trytond.tools import datetime_strftime
from trytond.pyson import In, Eval, Not
from trytond.transaction import Transaction
from trytond.pool import Pool
+from trytond.config import CONFIG
+from trytond.backend import TableHandler
+
+sql_sequence = CONFIG.options['db_type'] == 'postgresql'
class SequenceType(ModelSQL, ModelView):
@@ -24,6 +28,7 @@ class Sequence(ModelSQL, ModelView):
"Sequence"
_name = 'ir.sequence'
_description = __doc__
+ _strict = False
name = fields.Char('Sequence Name', required=True, translate=True)
code = fields.Selection('code_get', 'Sequence Code', required=True,
states={
@@ -37,10 +42,12 @@ class Sequence(ModelSQL, ModelView):
('decimal timestamp', 'Decimal Timestamp'),
('hexadecimal timestamp', 'Hexadecimal Timestamp'),
], 'Type')
- number_next = fields.Integer('Next Number',
+ number_next_internal = fields.Integer('Next Number',
states={
'invisible': Not(In(Eval('type'), ['incremental'])),
}, depends=['type'])
+ number_next = fields.Function(number_next_internal, 'get_number_next',
+ 'set_number_next')
number_increment = fields.Integer('Increment Number',
states={
'invisible': Not(In(Eval('type'), ['incremental'])),
@@ -77,6 +84,26 @@ class Sequence(ModelSQL, ModelView):
'future_last_timestamp': 'Last Timestamp could not be in future!',
})
+ def init(self, module_name):
+ cursor = Transaction().cursor
+ table = TableHandler(cursor, self, module_name)
+
+ # Migration from 2.0 rename number_next into number_next_internal
+ table.column_rename('number_next', 'number_next_internal')
+
+ super(Sequence, self).init(module_name)
+
+ # Migration from 2.0 create sql_sequence
+ if sql_sequence and not self._strict:
+ sequence_ids = self.search([])
+ for sequence in self.browse(sequence_ids):
+ if sequence.type != 'incremental':
+ continue
+ if not TableHandler.sequence_exist(cursor,
+ self._sql_sequence_name(sequence)):
+ self.create_sql_sequence(sequence,
+ sequence.number_next_internal)
+
def default_active(self):
return True
@@ -104,6 +131,53 @@ class Sequence(ModelSQL, ModelView):
def default_code(self):
return Transaction().context.get('code', False)
+ def get_number_next(self, ids, name):
+ cursor = Transaction().cursor
+ result = {}
+ for sequence in self.browse(ids):
+ sql_name = self._sql_sequence_name(sequence)
+ if sql_sequence and not self._strict:
+ cursor.execute('SELECT '
+ 'CASE WHEN NOT is_called THEN last_value '
+ 'ELSE last_value + increment_by '
+ 'END FROM "%s"' % sql_name)
+ value, = cursor.fetchone()
+ else:
+ value = sequence.number_next_internal
+ result[sequence.id] = value
+ return result
+
+ def set_number_next(self, ids, name, value):
+ cursor = Transaction().cursor
+ super(Sequence, self).write(ids, {
+ 'number_next_internal': value,
+ })
+
+ def create(self, values):
+ sequence_id = super(Sequence, self).create(values)
+ if sql_sequence and not self._strict:
+ sequence = self.browse(sequence_id)
+ self.update_sql_sequence(sequence, values.get('number_next',
+ self.default_number_next()))
+ return sequence_id
+
+ def write(self, ids, values):
+ result = super(Sequence, self).write(ids, values)
+ if sql_sequence and not self._strict:
+ ids = [ids] if isinstance(ids, (int, long)) else ids
+ sequences = self.browse(ids)
+ for sequence in sequences:
+ self.update_sql_sequence(sequence, values.get('number_next'))
+ return result
+
+ def delete(self, ids):
+ if sql_sequence and not self._strict:
+ ids = [ids] if isinstance(ids, (int, long)) else ids
+ sequences = self.browse(ids)
+ for sequence in sequences:
+ self.delete_sql_sequence(sequence)
+ return super(Sequence, self).delete(ids)
+
def code_get(self):
pool = Pool()
sequence_type_obj = pool.get('ir.sequence.type')
@@ -131,6 +205,47 @@ class Sequence(ModelSQL, ModelView):
return False
return True
+ def _sql_sequence_name(self, sequence):
+ 'Return SQL sequence name'
+ return '%s_%s' % (self._table, sequence.id)
+
+ def create_sql_sequence(self, sequence, number_next=None):
+ 'Create the SQL sequence'
+ cursor = Transaction().cursor
+ if sequence.type != 'incremental':
+ return
+ if number_next is None:
+ number_next = sequence.number_next
+ cursor.execute('CREATE SEQUENCE "' + self._sql_sequence_name(sequence)
+ + '" INCREMENT BY %s START WITH %s', (sequence.number_increment,
+ number_next))
+
+ def update_sql_sequence(self, sequence, number_next=None):
+ 'Update the SQL sequence'
+ cursor = Transaction().cursor
+ exist = TableHandler.sequence_exist(cursor,
+ self._sql_sequence_name(sequence))
+ if sequence.type != 'incremental':
+ if exist:
+ self.delete_sql_sequence(sequence)
+ return
+ if not exist:
+ self.create_sql_sequence(sequence, number_next)
+ return
+ if number_next is None:
+ number_next = sequence.number_next
+ cursor.execute('ALTER SEQUENCE "' + self._sql_sequence_name(sequence)
+ + '" INCREMENT BY %s RESTART WITH %s', (sequence.number_increment,
+ number_next))
+
+ def delete_sql_sequence(self, sequence):
+ 'Delete the SQL sequence'
+ cursor = Transaction().cursor
+ if sequence.type != 'incremental':
+ return
+ cursor.execute('DROP SEQUENCE "%s"'
+ % self._sql_sequence_name(sequence))
+
def _process(self, string, date=None):
pool = Pool()
date_obj = pool.get('ir.date')
@@ -151,13 +266,20 @@ class Sequence(ModelSQL, ModelView):
def _get_sequence(self, sequence):
if sequence.type == 'incremental':
- #Pre-fetch number_next
- number_next = sequence.number_next or 0
+ if sql_sequence and not self._strict:
+ cursor = Transaction().cursor
+ cursor.execute('SELECT nextval(\'"%s"\')'
+ % self._sql_sequence_name(sequence))
+ number_next, = cursor.fetchone()
+ else:
+ #Pre-fetch number_next
+ number_next = sequence.number_next_internal
- with Transaction().set_user(0):
- self.write(sequence.id, {
- 'number_next': number_next + sequence.number_increment,
- })
+ with Transaction().set_user(0):
+ self.write(sequence.id, {
+ '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
@@ -209,6 +331,7 @@ class SequenceStrict(Sequence):
"Sequence Strict"
_name = 'ir.sequence.strict'
_description = __doc__
+ _strict = True
def get_id(self, clause):
Transaction().cursor.lock(self._table)
diff --git a/trytond/ir/ui/board.rnc b/trytond/ir/ui/board.rnc
index 19fd9c112a..63f100fc3c 100644
--- a/trytond/ir/ui/board.rnc
+++ b/trytond/ir/ui/board.rnc
@@ -20,8 +20,8 @@ image = element image { attlist.image, empty }
attlist.image &= attribute name { text }
attlist.image &= [ a:defaultValue = "1" ] attribute colspan { text }?
attlist.image &=
- [ a:defaultValue = "0" ] attribute expand { "0" | "1" }?
-attlist.image &= [ a:defaultValue = "0" ] attribute fill { "0" | "1" }?
+ [ a:defaultValue = "0" ] attribute yexpand { "0" | "1" }?
+attlist.image &= [ a:defaultValue = "0" ] attribute yfill { "0" | "1" }?
attlist.image &= attribute help { text }?
attlist.image &= attribute states { text }?
separator = element separator { attlist.separator, empty }
@@ -29,22 +29,23 @@ attlist.separator &= [ a:defaultValue = "" ] attribute string { text }?
attlist.separator &= ( attribute name { text } | attribute name { text } )
attlist.separator &= attribute states { text }?
attlist.separator &=
- [ a:defaultValue = "0.0" ] attribute align { text }?
+ [ a:defaultValue = "0.0" ] attribute xalign { text }?
attlist.separator &=
[ a:defaultValue = "1" ] attribute colspan { text }?
attlist.separator &=
- [ a:defaultValue = "0" ] attribute expand { "0" | "1" }?
+ [ a:defaultValue = "0" ] attribute yexpand { "0" | "1" }?
attlist.separator &=
- [ a:defaultValue = "0" ] attribute fill { "0" | "1" }?
+ [ a:defaultValue = "0" ] attribute yfill { "0" | "1" }?
attlist.separator &= attribute help { text }?
label = element label { attlist.label, empty }
attlist.label &= [ a:defaultValue = "" ] attribute string { text }?
attlist.label &= ( attribute name { text } | attribute name { text } )
attlist.label &= attribute states { text }?
-attlist.label &= [ a:defaultValue = "0.0" ] attribute align { text }?
+attlist.label &= [ a:defaultValue = "0.0" ] attribute xalign { text }?
+attlist.label &= [ a:defaultValue = "0.5" ] attribute yalign { text }?
attlist.label &=
- [ a:defaultValue = "0" ] attribute expand { "0" | "1" }?
-attlist.label &= [ a:defaultValue = "0" ] attribute fill { "0" | "1" }?
+ [ a:defaultValue = "0" ] attribute yexpand { "0" | "1" }?
+attlist.label &= [ a:defaultValue = "0" ] attribute yfill { "0" | "1" }?
attlist.label &=
[ a:defaultValue = "0" ] attribute xexpand { "0" | "1" }?
attlist.label &= [ a:defaultValue = "1" ] attribute xfill { "0" | "1" }?
@@ -90,8 +91,8 @@ group =
attlist.group &= attribute string { text }?
attlist.group &= [ a:defaultValue = "1" ] attribute colspan { text }?
attlist.group &=
- [ a:defaultValue = "0" ] attribute expand { "0" | "1" }?
-attlist.group &= [ a:defaultValue = "1" ] attribute fill { "0" | "1" }?
+ [ a:defaultValue = "0" ] attribute yexpand { "0" | "1" }?
+attlist.group &= [ a:defaultValue = "1" ] attribute yfill { "0" | "1" }?
attlist.group &= [ a:defaultValue = "1" ] attribute rowspan { text }?
attlist.group &= [ a:defaultValue = "4" ] attribute col { text }?
attlist.group &= attribute id { text }
diff --git a/trytond/ir/ui/board.rng b/trytond/ir/ui/board.rng
index 06d68504d2..71a687bec2 100644
--- a/trytond/ir/ui/board.rng
+++ b/trytond/ir/ui/board.rng
@@ -44,7 +44,7 @@
-
+
0
1
@@ -54,7 +54,7 @@
-
+
0
1
@@ -96,7 +96,7 @@
-
+
@@ -106,7 +106,7 @@
-
+
0
1
@@ -116,7 +116,7 @@
-
+
0
1
@@ -153,12 +153,17 @@
-
+
-
+
+
+
+
+
+
0
1
@@ -168,7 +173,7 @@
-
+
0
1
@@ -309,7 +314,7 @@
-
+
0
1
@@ -319,7 +324,7 @@
-
+
0
1
diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py
index 73302f7bb5..8280788555 100644
--- a/trytond/ir/ui/view.py
+++ b/trytond/ir/ui/view.py
@@ -32,11 +32,11 @@ class View(ModelSQL, ModelView):
ondelete='CASCADE')
field_childs = fields.Char('Children Field', states={
'invisible': Not(Equal(Eval('type'), 'tree')),
- })
+ }, depends=['type'])
module = fields.Char('Module', readonly=True)
domain = fields.Char('Domain', states={
'invisible': Not(Bool(Eval('inherit'))),
- })
+ }, depends=['inherit'])
def __init__(self):
super(View, self).__init__()
diff --git a/trytond/protocols/common.py b/trytond/protocols/common.py
index e6d0a5cc8b..1b972b2135 100644
--- a/trytond/protocols/common.py
+++ b/trytond/protocols/common.py
@@ -4,6 +4,13 @@ import errno
import os
import socket
import threading
+import select
+import sys
+import gzip
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
def endsocket(sock):
if os.name != 'nt':
@@ -17,21 +24,104 @@ class daemon(threading.Thread):
def __init__(self, interface, port, secure, name=None):
threading.Thread.__init__(self, name=name)
self.secure = secure
- self.running = False
self.ipv6 = False
- if socket.has_ipv6:
- try:
- socket.getaddrinfo(interface or None, port, socket.AF_INET6)
+ for family, _, _, _, _ in socket.getaddrinfo(interface or None, port,
+ socket.AF_UNSPEC, socket.SOCK_STREAM):
+ if family == socket.AF_INET6:
self.ipv6 = True
- except Exception:
- pass
+ break
def stop(self):
- self.running = False
+ self.server.shutdown()
+ return
def run(self):
- self.running = True
- while self.running:
- self.server.handle_request()
- endsocket(self.server.socket)
+ self.server.serve_forever()
return True
+
+class GZipRequestHandlerMixin:
+
+ if sys.version_info[:2] <= (2, 6):
+ # Copy from SimpleXMLRPCServer.py with gzip encoding added
+ def do_POST(self):
+ """Handles the HTTP POST request.
+
+ Attempts to interpret all HTTP POST requests as XML-RPC calls,
+ which are forwarded to the server's _dispatch method for handling.
+ """
+
+ # Check that the path is legal
+ if not self.is_rpc_path_valid():
+ self.report_404()
+ return
+
+ try:
+ # Get arguments by reading body of request.
+ # We read this in chunks to avoid straining
+ # socket.read(); around the 10 or 15Mb mark, some platforms
+ # begin to have problems (bug #792570).
+ max_chunk_size = 10*1024*1024
+ size_remaining = int(self.headers["content-length"])
+ L = []
+ while size_remaining:
+ chunk_size = min(size_remaining, max_chunk_size)
+ L.append(self.rfile.read(chunk_size))
+ size_remaining -= len(L[-1])
+ data = ''.join(L)
+
+ data = self.decode_request_content(data)
+ if data is None:
+ return #response has been sent
+
+ # In previous versions of SimpleXMLRPCServer, _dispatch
+ # could be overridden in this class, instead of in
+ # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
+ # check to see if a subclass implements _dispatch and dispatch
+ # using that method if present.
+ response = self.server._marshaled_dispatch(
+ data, getattr(self, '_dispatch', None)
+ )
+ except Exception: # This should only happen if the module is buggy
+ # internal error, report as HTTP server error
+ self.send_response(500)
+ self.end_headers()
+ else:
+ # got a valid XML RPC response
+ self.send_response(200)
+ self.send_header("Content-type", "text/xml")
+
+ # Handle gzip encoding
+ if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \
+ and len(response) > self.encode_threshold:
+ buffer = StringIO.StringIO()
+ output = gzip.GzipFile(mode='wb', fileobj=buffer)
+ output.write(response)
+ output.close()
+ buffer.seek(0)
+ response = buffer.getvalue()
+ self.send_header('Content-Encoding', 'gzip')
+
+ self.send_header("Content-length", str(len(response)))
+ self.end_headers()
+ self.wfile.write(response)
+
+ def decode_request_content(self, data):
+ #support gzip encoding of request
+ encoding = self.headers.get("content-encoding", "identity").lower()
+ if encoding == "identity":
+ return data
+ if encoding == "gzip":
+ f = StringIO.StringIO(data)
+ gzf = gzip.GzipFile(mode="rb", fileobj=f)
+ try:
+ decoded = gzf.read()
+ except IOError:
+ self.send_response(400, "error decoding gzip content")
+ f.close()
+ gzf.close()
+ return decoded
+ else:
+ self.send_response(501, "encoding %r not supported" % encoding)
+ self.send_header("Content-length", "0")
+ self.end_headers()
+
diff --git a/trytond/protocols/jsonrpc.py b/trytond/protocols/jsonrpc.py
index 8933d6b438..ea950b22b7 100644
--- a/trytond/protocols/jsonrpc.py
+++ b/trytond/protocols/jsonrpc.py
@@ -4,7 +4,7 @@ from trytond.protocols.sslsocket import SSLSocket
from trytond.protocols.dispatcher import dispatch
from trytond.config import CONFIG
from trytond.protocols.datatype import Float
-from trytond.protocols.common import daemon
+from trytond.protocols.common import daemon, GZipRequestHandlerMixin
import SimpleXMLRPCServer
import SimpleHTTPServer
import SocketServer
@@ -12,8 +12,6 @@ import traceback
import socket
import sys
import os
-import gzip
-import StringIO
try:
import fcntl
except ImportError:
@@ -31,6 +29,8 @@ def object_hook(dct):
dct['hour'], dct['minute'], dct['second'])
elif dct['__class__'] == 'date':
return datetime.date(dct['year'], dct['month'], dct['day'])
+ elif dct['__class__'] == 'Decimal':
+ return Decimal(dct['decimal'])
return dct
class JSONEncoder(json.JSONEncoder):
@@ -52,7 +52,9 @@ class JSONEncoder(json.JSONEncoder):
'day': obj.day,
}
elif isinstance(obj, Decimal):
- return float(obj)
+ return {'__class__': 'Decimal',
+ 'decimal': str(obj),
+ }
return super(JSONEncoder, self).default(obj)
@@ -64,7 +66,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
reason to instantiate this class directly.
"""
- def _marshaled_dispatch(self, data, dispatch_method=None):
+ def _marshaled_dispatch(self, data, dispatch_method=None, path=None):
"""Dispatches an JSON-RPC method from marshalled (JSON) data.
JSON-RPC methods are dispatched from the marshalled (JSON) data
@@ -75,7 +77,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, parse_float=Float)
+ rawreq = json.loads(data, object_hook=object_hook)
req_id = rawreq.get('id', 0)
method = rawreq['method']
@@ -104,7 +106,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
traceb = sys.exc_info()[2]
pdb.post_mortem(traceb)
# report exception back to server
- response['error'] = "%s:\n%s" % (sys.exc_value, tb_s)
+ response['error'] = (str(sys.exc_value), tb_s)
return json.dumps(response, cls=JSONEncoder)
@@ -126,7 +128,8 @@ class GenericJSONRPCRequestHandler:
return res
-class SimpleJSONRPCRequestHandler(GenericJSONRPCRequestHandler,
+class SimpleJSONRPCRequestHandler(GZipRequestHandlerMixin,
+ GenericJSONRPCRequestHandler,
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler,
SimpleHTTPServer.SimpleHTTPRequestHandler):
"""Simple JSON-RPC request handler class.
@@ -134,71 +137,15 @@ class SimpleJSONRPCRequestHandler(GenericJSONRPCRequestHandler,
Handles all HTTP POST requests and attempts to decode them as
JSON-RPC requests.
"""
+ protocol_version = "HTTP/1.1"
rpc_paths = None
encode_threshold = 1400 # common MTU
- # Copy from SimpleXMLRPCServer.py with gzip encoding added
- def do_POST(self):
- """Handles the HTTP POST request.
-
- Attempts to interpret all HTTP POST requests as JSON-RPC calls,
- which are forwarded to the server's _dispatch method for handling.
- """
-
- # Check that the path is legal
- if not self.is_rpc_path_valid():
- self.report_404()
- return
-
- try:
- # Get arguments by reading body of request.
- # We read this in chunks to avoid straining
- # socket.read(); around the 10 or 15Mb mark, some platforms
- # begin to have problems (bug #792570).
- max_chunk_size = 10*1024*1024
- size_remaining = int(self.headers["content-length"])
- L = []
- while size_remaining:
- chunk_size = min(size_remaining, max_chunk_size)
- L.append(self.rfile.read(chunk_size))
- size_remaining -= len(L[-1])
- data = ''.join(L)
-
- # In previous versions of SimpleXMLRPCServer, _dispatch
- # could be overridden in this class, instead of in
- # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
- # check to see if a subclass implements _dispatch and dispatch
- # using that method if present.
- response = self.server._marshaled_dispatch(
- data, getattr(self, '_dispatch', None)
- )
- except Exception: # This should only happen if the module is buggy
- # internal error, report as HTTP server error
- self.send_response(500)
- self.end_headers()
- else:
- # got a valid JSON RPC response
- self.send_response(200)
- self.send_header("Content-type", "application/json-rpc")
-
- # Handle gzip encoding
- if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \
- and len(response) > self.encode_threshold:
- buffer = StringIO.StringIO()
- output = gzip.GzipFile(mode='wb', fileobj=buffer)
- output.write(response)
- output.close()
- buffer.seek(0)
- response = buffer.getvalue()
- self.send_header('Content-Encoding', 'gzip')
-
- self.send_header("Content-length", str(len(response)))
- self.end_headers()
- self.wfile.write(response)
-
- # shut down the connection
- self.wfile.flush()
- self.connection.shutdown(1)
+ def send_header(self, keyword, value):
+ if keyword == 'Content-type' and value == 'text/xml':
+ value = 'application/json-rpc'
+ SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.send_header(self,
+ keyword, value)
def translate_path(self, path):
"""Translate a /-separated PATH to the local filename syntax.
@@ -274,6 +221,7 @@ class SimpleJSONRPCServer(SocketServer.TCPServer,
class SimpleThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
SimpleJSONRPCServer):
timeout = 1
+ daemon_threads = True
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET,
@@ -304,7 +252,6 @@ class JSONRPCDaemon(daemon):
def __init__(self, interface, port, secure=False):
daemon.__init__(self, interface, port, secure, name='JSONRPCDaemon')
-
if self.secure:
handler_class = SecureJSONRPCRequestHandler
server_class = SecureThreadedJSONRPCServer
diff --git a/trytond/protocols/netrpc.py b/trytond/protocols/netrpc.py
deleted file mode 100644
index 238fb208bb..0000000000
--- a/trytond/protocols/netrpc.py
+++ /dev/null
@@ -1,139 +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.
-from trytond.pysocket import PySocket
-from trytond.protocols.sslsocket import SSLSocket
-from trytond.config import CONFIG
-from trytond.protocols.dispatcher import dispatch
-from trytond.protocols.common import endsocket
-import threading
-import select
-import traceback
-import socket
-import sys
-
-
-class NetRPCClientThread(threading.Thread):
- def __init__(self, sock, threads, secure):
- threading.Thread.__init__(self, name='NetRPCClientThread')
- self.sock = sock
- self.threads = threads
- self.running = False
- self.secure = secure
-
- def run(self):
- self.running = True
- try:
- pysocket = PySocket(self.sock)
- except Exception:
- self.sock.close()
- self.threads.remove(self)
- return False
- timeout = 0
- while self.running:
- try:
- rlist, _, _ = select.select([self.sock], [], [], 1)
- except select.error:
- continue
- if not rlist:
- timeout += 1
- if timeout > 600:
- break
- continue
- timeout = 0
- try:
- msg = pysocket.receive()
- except Exception:
- pysocket.disconnect()
- self.threads.remove(self)
- return False
- host, port = self.sock.getpeername()[:2]
- try:
- res = dispatch(host, port, 'NetRPC', *msg)
- pysocket.send(res)
- except Exception, exception:
- tb_s = ''
- for line in traceback.format_exception(*sys.exc_info()):
- try:
- line = line.encode('utf-8', 'ignore')
- except Exception:
- continue
- tb_s += line
- for path in sys.path:
- tb_s = tb_s.replace(path, '')
- if CONFIG['debug_mode']:
- import pdb
- tback = sys.exc_info()[2]
- pdb.post_mortem(tback)
- try:
- pysocket.send(exception.args, exception=True,
- traceback=tb_s)
- except Exception:
- pysocket.disconnect()
- self.threads.remove(self)
- return False
- pysocket.disconnect()
- self.threads.remove(self)
- return True
-
- def stop(self):
- self.running = False
-
-
-class NetRPCServerThread(threading.Thread):
- def __init__(self, interface, port, secure=False):
- threading.Thread.__init__(self, name='NetRPCServerThread')
- self.socket = None
- if socket.has_ipv6:
- try:
- socket.getaddrinfo(interface or None, port, socket.AF_INET6)
- self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
- except Exception:
- pass
- if self.socket is None:
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- if secure:
- self.socket = SSLSocket(self.socket)
- self.socket.bind((interface, port))
- self.socket.listen(5)
- self.threads = []
- self.secure = secure
- self.running = False
-
- def run(self):
- try:
- self.running = True
- while self.running:
- if not int(CONFIG['max_thread']) \
- or len(self.threads) < int(CONFIG['max_thread']):
- try:
- rlist, _, _ = select.select([self.socket], [], [], 1)
- except select.error:
- continue
- if not rlist:
- continue
- (clientsocket, _) = self.socket.accept()
- c_thread = NetRPCClientThread(clientsocket, self.threads,
- self.secure)
- self.threads.append(c_thread)
- c_thread.start()
- except Exception:
- try:
- self.socket.close()
- except Exception:
- pass
- return False
-
- def stop(self):
- self.running = False
- while len(self.threads):
- try:
- thread = self.threads[0]
- thread.stop()
- thread.join()
- except Exception:
- pass
- try:
- endsocket(self.socket)
- except Exception:
- return False
diff --git a/trytond/protocols/webdav.py b/trytond/protocols/webdav.py
index b0be2a9010..96aaa5308d 100644
--- a/trytond/protocols/webdav.py
+++ b/trytond/protocols/webdav.py
@@ -78,7 +78,6 @@ if not hasattr(WebDAVServer.DAVRequestHandler, '_config'):
class BaseThreadedHTTPServer(SocketServer.ThreadingMixIn,
BaseHTTPServer.HTTPServer):
timeout = 1
- max_children = CONFIG['max_thread']
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET,
diff --git a/trytond/protocols/xmlrpc.py b/trytond/protocols/xmlrpc.py
index ded00e6c3d..137ae45518 100644
--- a/trytond/protocols/xmlrpc.py
+++ b/trytond/protocols/xmlrpc.py
@@ -4,7 +4,7 @@ from trytond.protocols.sslsocket import SSLSocket
from trytond.protocols.dispatcher import dispatch
from trytond.config import CONFIG
from trytond.protocols.datatype import Float
-from trytond.protocols.common import daemon
+from trytond.protocols.common import daemon, GZipRequestHandlerMixin
from trytond import security
import SimpleXMLRPCServer
import SocketServer
@@ -13,8 +13,6 @@ import traceback
import socket
import sys
import os
-import gzip
-import StringIO
import base64
import datetime
from types import DictType
@@ -102,8 +100,10 @@ class GenericXMLRPCRequestHandler:
security.logout(database_name, user, session)
-class SimpleXMLRPCRequestHandler(GenericXMLRPCRequestHandler,
+class SimpleXMLRPCRequestHandler(GZipRequestHandlerMixin,
+ GenericXMLRPCRequestHandler,
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
+ protocol_version = "HTTP/1.1"
rpc_paths = None
encode_threshold = 1400 # common MTU
@@ -127,69 +127,6 @@ class SimpleXMLRPCRequestHandler(GenericXMLRPCRequestHandler,
self.send_header("WWW-Authenticate", 'Basic realm="Tryton"')
return False
- # Copy from SimpleXMLRPCServer.py with gzip encoding added
- def do_POST(self):
- """Handles the HTTP POST request.
-
- Attempts to interpret all HTTP POST requests as XML-RPC calls,
- which are forwarded to the server's _dispatch method for handling.
- """
-
- # Check that the path is legal
- if not self.is_rpc_path_valid():
- self.report_404()
- return
-
- try:
- # Get arguments by reading body of request.
- # We read this in chunks to avoid straining
- # socket.read(); around the 10 or 15Mb mark, some platforms
- # begin to have problems (bug #792570).
- max_chunk_size = 10*1024*1024
- size_remaining = int(self.headers["content-length"])
- L = []
- while size_remaining:
- chunk_size = min(size_remaining, max_chunk_size)
- L.append(self.rfile.read(chunk_size))
- size_remaining -= len(L[-1])
- data = ''.join(L)
-
- # In previous versions of SimpleXMLRPCServer, _dispatch
- # could be overridden in this class, instead of in
- # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
- # check to see if a subclass implements _dispatch and dispatch
- # using that method if present.
- response = self.server._marshaled_dispatch(
- data, getattr(self, '_dispatch', None)
- )
- except Exception: # This should only happen if the module is buggy
- # internal error, report as HTTP server error
- self.send_response(500)
- self.end_headers()
- else:
- # got a valid XML RPC response
- self.send_response(200)
- self.send_header("Content-type", "text/xml")
-
- # Handle gzip encoding
- if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \
- and len(response) > self.encode_threshold:
- buffer = StringIO.StringIO()
- output = gzip.GzipFile(mode='wb', fileobj=buffer)
- output.write(response)
- output.close()
- buffer.seek(0)
- response = buffer.getvalue()
- self.send_header('Content-Encoding', 'gzip')
-
- self.send_header("Content-length", str(len(response)))
- self.end_headers()
- self.wfile.write(response)
-
- # shut down the connection
- self.wfile.flush()
- self.connection.shutdown(1)
-
class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
@@ -202,6 +139,7 @@ class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
class SimpleThreadedXMLRPCServer(SocketServer.ThreadingMixIn,
SimpleXMLRPCServer.SimpleXMLRPCServer):
timeout = 1
+ daemon_threads = True
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET,
diff --git a/trytond/pysocket.py b/trytond/pysocket.py
deleted file mode 100644
index abbcf7daae..0000000000
--- a/trytond/pysocket.py
+++ /dev/null
@@ -1,253 +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.
-import socket
-# can't use/fall-back pickle due to different interface :-(
-import cPickle
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
-try:
- import ssl
-except ImportError:
- ssl = None
-import gzip
-try:
- import hashlib
-except ImportError:
- hashlib = None
- import sha
-import os
-
-DNS_CACHE = {}
-MAX_SIZE = 999999999
-MAX_LENGHT = len(str(MAX_SIZE))
-CONNECT_TIMEOUT = 5
-TIMEOUT = 3600
-GZIP_THRESHOLD = 1400 # common MTU
-
-_ALLOWED_MODULES = {'datetime': ['datetime', 'date'], 'decimal': ['Decimal']}
-
-def checkfunction(module, klass):
- if module in _ALLOWED_MODULES and klass in _ALLOWED_MODULES[module]:
- mod = __import__(module, {}, {}, ['__all__'])
- _class = getattr(mod, klass)
- return _class
- raise ValueError('Not supported: %s/%s' % (module, klass))
-
-
-class PySocket:
-
- def __init__(self, sock=None, fingerprints=None, ca_certs=None):
- self.sock = sock
- self.host = None
- self.hostname = None
- self.port = None
- self.ssl = False
- self.ssl_sock = None
- self.connected = False
- self.buffer = ''
- self.fingerprints = fingerprints
- if ca_certs and os.path.isfile(ca_certs):
- self.ca_certs = ca_certs
- else:
- self.ca_certs = None
-
- def connect(self, host, port=False):
- if not port:
- buf = host.split('//')[1]
- host, port = buf.rsplit(':', 1)
- hostname = host
- if host in DNS_CACHE:
- host = DNS_CACHE[host]
- self.sock = None
- if socket.has_ipv6:
- try:
- socket.getaddrinfo(host, int(port), socket.AF_INET6)
- self.sock = socket.socket(socket.AF_INET6,
- socket.SOCK_STREAM)
- self.sock.settimeout(CONNECT_TIMEOUT)
- self.sock.connect((host, int(port)))
- except socket.error:
- self.sock = None
- if self.sock is None:
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.sock.settimeout(CONNECT_TIMEOUT)
- self.sock.connect((host, int(port)))
- DNS_CACHE[hostname], port = self.sock.getpeername()[:2]
- try:
- sock = None
- if socket.has_ipv6:
- try:
- socket.getaddrinfo(host, int(port), socket.AF_INET6)
- sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
- sock.settimeout(CONNECT_TIMEOUT)
- sock.connect((host, int(port)))
- except socket.error:
- sock = None
- if sock is None:
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.settimeout(CONNECT_TIMEOUT)
- sock.connect((host, int(port)))
- if ssl:
- ssl_sock = ssl.wrap_socket(sock)
- self.ssl = True
- elif hasattr(socket, 'ssl'):
- ssl_sock = socket.ssl(sock)
- self.ssl = True
- except socket.error:
- pass
- self.sock.settimeout(TIMEOUT)
- fingerprint = None
- if self.ssl:
- if ssl:
- self.ssl_sock = ssl.wrap_socket(self.sock,
- ca_certs=self.ca_certs, cert_reqs=(self.ca_certs and
- ssl.CERT_REQUIRED or ssl.CERT_NONE))
- elif hasattr(socket, 'ssl'):
- self.ssl_sock = socket.ssl(self.sock, certfile=self.ca_certs)
- try:
- peercert = self.ssl_sock.getpeercert(True)
- except socket.error:
- peercert = None
- def format_hash(value):
- return reduce(lambda x, y: x + y[1].upper() +
- ((y[0] % 2 and y[0] + 1 < len(value)) and ':' or ''),
- enumerate(value), '')
- if peercert and hashlib:
- fingerprint = format_hash(hashlib.sha1(peercert).hexdigest())
- elif peercert:
- fingerprint = format_hash(sha1.new(peercert).hexdigest())
- self.host = host
- self.hostname = hostname
- self.port = port
- self.connected = True
- self.buffer = ''
- if self.fingerprints is not None:
- key = (self.hostname, str(self.port))
- if key in self.fingerprints:
- if self.fingerprints[key] != fingerprint:
- self.disconnect()
- raise Exception('BadFingerprint')
- elif fingerprint:
- self.fingerprints[key] = fingerprint
-
- def disconnect(self):
- try:
- sock = self.sock
- if self.ssl:
- sock = self.ssl_sock
- try:
- shutdown_value = 2
- if hasattr(socket, 'SHUT_RDWR'):
- shutdown_value = socket.SHUT_RDWR
- if hasattr(sock, 'sock_shutdown'):
- sock.sock_shutdown(shutdown_value)
- else:
- sock.shutdown(shutdown_value)
- except socket.error:
- pass
- sock.close()
- except socket.error:
- pass
- self.sock = None
- self.ssl = False
- self.ssl_sock = None
- self.connected = False
- self.buffer = ''
-
- def reconnect(self):
- if self.hostname and self.port:
- self.disconnect()
- self.connect(self.hostname, self.port)
-
- def send(self, msg, exception=False, traceback=None):
- msg = cPickle.dumps([msg, traceback], protocol=2)
- gzip_p = False
- if len(msg) > GZIP_THRESHOLD:
- buffer = StringIO.StringIO()
- output = gzip.GzipFile(mode='wb', fileobj=buffer)
- output.write(msg)
- output.close()
- buffer.seek(0)
- msg = buffer.getvalue()
- gzip_p = True
- size = len(msg)
- msg = str(size) + ' ' + (exception and "1" or "0") \
- + (gzip_p and "1" or "0") + msg
- size = len(msg)
-
- if self.ssl:
- totalsent = 0
- while totalsent < size:
- sent = self.ssl_sock.write(msg[totalsent:])
- if sent == 0:
- raise RuntimeError("socket connection broken")
- totalsent = totalsent + sent
- else:
- self.sock.sendall(msg)
-
- def receive(self):
- buf = self.buffer
- L = []
- size_remaining = MAX_LENGHT
- while size_remaining:
- chunk_size = min(size_remaining, 4096)
- if self.ssl:
- chunk = self.ssl_sock.read(chunk_size)
- else:
- chunk = self.sock.recv(chunk_size)
- if chunk == '':
- raise RuntimeError("socket connection broken")
- L.append(chunk)
- size_remaining -= len(chunk)
- if ' ' in chunk:
- break
- if size_remaining < 0:
- raise RuntimeError("socket connection broken")
- buf += ''.join(L)
- size, msg = buf.split(' ', 1)
- size = int(size)
- if size > MAX_SIZE:
- raise RuntimeError("socket connection broken")
- while len(msg) < 2:
- chunk_size = min(size + 2, 4096)
- if self.ssl:
- msg += self.ssl_sock.read(chunk_size)
- else:
- msg += self.sock.recv(chunk_size)
- if msg == '':
- raise RuntimeError("socket connection broken")
- exception = msg[0] != "0"
- gzip_p = msg[1] != "0"
- L = [msg[2:]]
- size_remaining = size - len(L[0])
- while size_remaining:
- chunk_size = min(size_remaining, 4096)
- if self.ssl:
- chunk = self.ssl_sock.read(chunk_size)
- else:
- chunk = self.sock.recv(chunk_size)
- if chunk == '':
- raise RuntimeError("socket connection broken")
- L.append(chunk)
- size_remaining -= len(chunk)
- msg = ''.join(L)
- if len(msg) > size:
- self.buffer = msg[size:]
- msg = msg[:size]
- else:
- self.buffer = ''
- msgio = StringIO.StringIO(msg)
- if gzip_p:
- output = gzip.GzipFile(mode='r', fileobj=msgio)
- msgio = StringIO.StringIO(output.read(-1))
- output.close()
- unpickler = cPickle.Unpickler(msgio)
- # cPickle mechanism to import instances (pickle differs here)
- unpickler.find_global = checkfunction
- res = unpickler.load()
- if exception:
- raise Exception(*(list(res[0]) + [res[1]]))
- else:
- return res[0]
diff --git a/trytond/report/report.py b/trytond/report/report.py
index d25cb3da98..1dfe2258d0 100644
--- a/trytond/report/report.py
+++ b/trytond/report/report.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 copy
-import xml
-import sys
import base64
try:
import cStringIO as StringIO
@@ -13,11 +10,10 @@ import time
import os
import datetime
from base64 import decodestring
-import traceback
import inspect
-import logging
import tempfile
import warnings
+import subprocess
warnings.simplefilter("ignore")
import relatorio.reporting
warnings.resetwarnings()
@@ -28,11 +24,36 @@ except ImportError:
from genshi.filters import Translator
import lxml.etree
from trytond.config import CONFIG
-from trytond.backend import DatabaseIntegrityError
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.url import URLMixin
+MIMETYPES = {
+ 'odt': 'application/vnd.oasis.opendocument.text',
+ 'odp': 'application/vnd.oasis.opendocument.presentation',
+ 'ods': 'application/vnd.oasis.opendocument.spreadsheet',
+ 'odg': 'application/vnd.oasis.opendocument.graphics',
+ }
+FORMAT2EXT = {
+ 'doc6': 'doc',
+ 'doc95': 'doc',
+ 'docbook': 'xml',
+ 'ooxml': 'xml',
+ 'latex': 'ltx',
+ 'sdc4': 'sdc',
+ 'sdc3': 'sdc',
+ 'sdd3': 'sdd',
+ 'sdd4': 'sdd',
+ 'sdw4': 'sdw',
+ 'sdw3': 'sdw',
+ 'sxd3': 'sxd',
+ 'sxd5': 'sxd',
+ 'text': 'txt',
+ 'xhtml': 'html',
+ 'xls5': 'xls',
+ 'xls95': 'xls',
+ }
+
class ReportFactory:
@@ -150,7 +171,9 @@ class Report(URLMixin):
if not report.report_content:
raise Exception('Error', 'Missing report file!')
- fd, path = tempfile.mkstemp(suffix='.odt', prefix='trytond')
+ fd, path = tempfile.mkstemp(
+ suffix=(os.extsep + report.template_extension),
+ prefix='trytond_')
outzip = zipfile.ZipFile(path, mode='w')
content_io = StringIO.StringIO()
@@ -220,7 +243,8 @@ class Report(URLMixin):
# Since Genshi >= 0.6, Translator requires a function type
translator = Translator(lambda text: translate(text))
- rel_report = relatorio.reporting.Report(path, 'application/vnd.oasis.opendocument.text',
+ mimetype = MIMETYPES[report.template_extension]
+ rel_report = relatorio.reporting.Report(path, mimetype,
ReportFactory(), relatorio.reporting.MIMETemplateLoader())
rel_report.filters.insert(0, translator)
#convert unicode key into str
@@ -236,61 +260,31 @@ class Report(URLMixin):
data = data.getvalue()
os.close(fd)
os.remove(path)
- output_format = report.extension
- if output_format == 'pdf':
- data = self.convert_pdf(data)
- return (output_format, data)
+ output_format = report.extension or report.template_extension
+ if output_format not in MIMETYPES:
+ data = self.unoconv(data, report.template_extension, output_format)
+ oext = FORMAT2EXT.get(output_format, output_format)
+ return (oext, data)
- def convert_pdf(self, data):
- """
- Convert report to PDF using OpenOffice.org.
- This requires OpenOffice.org, pyuno and openoffice-python to
- be installed.
- """
- import tempfile
+ def unoconv(self, data, input_format, output_format):
+ '''
+ Call unoconv to convert the OpenDocument
+ '''
+ fd, path = tempfile.mkstemp(suffix=(os.extsep + input_format),
+ prefix='trytond_')
+ oext = FORMAT2EXT.get(output_format, output_format)
+ with os.fdopen(fd, 'wb+') as fp:
+ fp.write(data)
+ cmd = ['unoconv', '--connection=%s' % CONFIG['unoconv'],
+ '-f', oext, '--stdout', path]
try:
- import unohelper # installs import-hook
- import openoffice.interact
- import openoffice.officehelper as officehelper
- from openoffice.streams import OutputStream
- from com.sun.star.beans import PropertyValue
- except ImportError, exception:
- raise Exception('ImportError', str(exception))
- try:
- # connect to OOo
- desktop = openoffice.interact.Desktop()
- except officehelper.BootstrapException:
- raise Exception('Error', "Can't connect to (bootstrap) OpenOffice.org")
-
- res_data = None
- # Create temporary file (with name) and write data there.
- # We can not use NamedTemporaryFile here, since this would be
- # deleted as soon as we close it to allow OOo reading.
- #TODO use an input stream here
- fd_odt, odt_name = tempfile.mkstemp()
- fh_odt = os.fdopen(fd_odt, 'wb+')
- try:
- fh_odt.write(data)
- del data # save memory
- fh_odt.close()
- doc = desktop.openFile(odt_name, hidden=False)
- # Export as PDF
- buffer = StringIO.StringIO()
- out_props = (
- PropertyValue("FilterName", 0, "writer_pdf_Export", 0),
- PropertyValue("Overwrite", 0, True, 0),
- PropertyValue("OutputStream", 0, OutputStream(buffer), 0),
- )
- doc.storeToURL("private:stream", out_props)
- res_data = buffer.getvalue()
- del buffer
- doc.dispose()
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = proc.communicate()
+ if proc.wait() != 0:
+ raise Exception(stderrdata)
+ return stdoutdata
finally:
- fh_odt.close()
- os.remove(odt_name)
- if not res_data:
- Exception('Error', 'Error converting to PDF')
- return res_data
+ os.remove(path)
def format_lang(self, value, lang, digits=2, grouping=True, monetary=False,
date=False, currency=None, symbol=True):
diff --git a/trytond/res/request.py b/trytond/res/request.py
index 33d9d7cda8..235b8cd971 100644
--- a/trytond/res/request.py
+++ b/trytond/res/request.py
@@ -25,38 +25,39 @@ _READONLY = If(In(Eval('state'), ['waiting', 'closed']),
If(Equal(Eval('state'), 'chatting'),
Not(Equal(Eval('act_from'), Eval('_user'))),
False))
+_DEPENDS = ['state', 'act_from']
class Request(ModelSQL, ModelView):
"Request"
_name = 'res.request'
_description = __doc__
name = fields.Char('Subject', states={
- 'readonly': _READONLY,
- }, required=True)
+ 'readonly': _READONLY,
+ }, required=True, depends=_DEPENDS)
active = fields.Boolean('Active')
priority = fields.Selection(_PRIORITIES, 'Priority', states={
- 'readonly': _READONLY,
- }, required=True, order_field='priority')
+ 'readonly': _READONLY,
+ }, required=True, order_field='priority', depends=_DEPENDS)
act_from = fields.Many2One('res.user', 'From', required=True,
readonly=True)
act_to = fields.Many2One('res.user', 'To', required=True,
domain=[('active', '=', True)],
states={
'readonly': _READONLY,
- })
+ }, depends=_DEPENDS)
body = fields.Text('Body', states={
- 'readonly': _READONLY,
- })
+ 'readonly': _READONLY,
+ }, depends=_DEPENDS)
date_sent = fields.DateTime('Date', readonly=True)
trigger_date = fields.DateTime('Trigger Date', states={
- 'readonly': _READONLY,
- })
+ 'readonly': _READONLY,
+ }, depends=_DEPENDS)
references = fields.One2Many('res.request.reference', 'request',
- 'References', states={
- 'readonly': If(Equal(Eval('state'), 'closed'),
- True,
- Not(Equal(Eval('act_from', 0), Eval('_user', 0)))),
- })
+ 'References', states={
+ 'readonly': If(Equal(Eval('state'), 'closed'),
+ True,
+ Not(Equal(Eval('act_from', 0), Eval('_user', 0)))),
+ }, depends=['state', 'act_from'])
number_references = fields.Function(fields.Integer('Number of References',
on_change_with=['references']), 'get_number_references')
state = fields.Selection(_STATES, 'State', required=True, readonly=True)
diff --git a/trytond/res/user.py b/trytond/res/user.py
index 082d5d1df3..d0371e2952 100644
--- a/trytond/res/user.py
+++ b/trytond/res/user.py
@@ -16,7 +16,7 @@ from trytond.backend import TableHandler
from trytond.security import get_connections
from trytond.transaction import Transaction
from trytond.cache import Cache
-from trytond.pyson import Eval, Greater
+from trytond.pyson import Eval, Bool
from trytond.pool import Pool
@@ -31,8 +31,8 @@ class User(ModelSQL, ModelView):
signature = fields.Text('Signature')
active = fields.Boolean('Active')
action = fields.Many2One('ir.action', 'Home Action', states={
- 'required': Greater(Eval('active_id', 0), 0),
- })
+ 'required': Bool(Eval('id')),
+ }, depends=['id'])
menu = fields.Many2One('ir.action', 'Menu Action',
domain=[('usage','=','menu')], required=True)
groups = fields.Many2Many('res.user-res.group',
diff --git a/trytond/server.py b/trytond/server.py
index ccb5f96a32..c1ae7cea63 100644
--- a/trytond/server.py
+++ b/trytond/server.py
@@ -154,55 +154,45 @@ class TrytonServer(object):
sys.exit(0)
# Launch Server
- if CONFIG['xmlrpc']:
- from trytond.protocols.xmlrpc import XMLRPCDaemon
- xmlrpcd = XMLRPCDaemon(CONFIG['interface'], CONFIG['xmlport'],
- CONFIG['secure_xmlrpc'])
- self.logger.info("starting XML-RPC%s protocol, port %d" % \
- (CONFIG['secure_xmlrpc'] and ' Secure' or '',
- CONFIG['xmlport']))
-
+ jsonrpcd = []
if CONFIG['jsonrpc']:
from trytond.protocols.jsonrpc import JSONRPCDaemon
- jsonrpcd = JSONRPCDaemon(CONFIG['interface'], CONFIG['jsonport'],
- CONFIG['secure_jsonrpc'])
- self.logger.info("starting JSON-RPC%s protocol, port %d" % \
- (CONFIG['secure_jsonrpc'] and ' Secure' or '',
- CONFIG['jsonport']))
+ for hostname, port in CONFIG['jsonrpc']:
+ jsonrpcd.append(JSONRPCDaemon(hostname, port,
+ CONFIG['ssl_jsonrpc']))
+ self.logger.info("starting JSON-RPC%s protocol on %s:%d" %
+ (CONFIG['ssl_jsonrpc'] and ' SSL' or '', hostname or '*',
+ port))
- if CONFIG['netrpc']:
- from trytond.protocols.netrpc import NetRPCServerThread
- netrpcd = NetRPCServerThread(CONFIG['interface'], CONFIG['netport'],
- CONFIG['secure_netrpc'])
- self.logger.info("starting NetRPC%s protocol, port %d" % \
- (CONFIG['secure_netrpc'] and ' Secure' or '',
- CONFIG['netport']))
+ xmlrpcd = []
+ if CONFIG['xmlrpc']:
+ from trytond.protocols.xmlrpc import XMLRPCDaemon
+ for hostname, port in CONFIG['xmlrpc']:
+ xmlrpcd.append(XMLRPCDaemon(hostname, port,
+ CONFIG['ssl_xmlrpc']))
+ self.logger.info("starting XML-RPC%s protocol on %s:%d" %
+ (CONFIG['ssl_xmlrpc'] and ' SSL' or '', hostname or '*',
+ port))
+ webdavd = []
if CONFIG['webdav']:
from trytond.protocols.webdav import WebDAVServerThread
- webdavd = WebDAVServerThread(CONFIG['interface'],
- CONFIG['webdavport'], CONFIG['secure_webdav'])
- self.logger.info("starting WebDAV%s protocol, port %d" % \
- (CONFIG['secure_webdav'] and ' Secure' or '',
- CONFIG['webdavport']))
+ for hostname, port in CONFIG['webdav']:
+ webdavd.append(WebDAVServerThread(hostname, port,
+ CONFIG['ssl_webdav']))
+ self.logger.info("starting WebDAV%s protocol on %s:%d" %
+ (CONFIG['ssl_webdav'] and ' SSL' or '', hostname or '*',
+ port))
def handler(signum, frame):
if hasattr(signal, 'SIGUSR1'):
if signum == signal.SIGUSR1:
Pool.start()
return
- if CONFIG['netrpc']:
- netrpcd.stop()
- netrpcd.join()
- if CONFIG['xmlrpc']:
- xmlrpcd.stop()
- xmlrpcd.join()
- if CONFIG['jsonrpc']:
- jsonrpcd.stop()
- jsonrpcd.join()
- if CONFIG['webdav']:
- webdavd.stop()
- webdavd.join()
+ for servers in (xmlrpcd, jsonrpcd, webdavd):
+ for server in servers:
+ server.stop()
+ server.join()
if CONFIG['pidfile']:
os.unlink(CONFIG['pidfile'])
logging.getLogger('server').info('stopped')
@@ -221,14 +211,9 @@ class TrytonServer(object):
signal.signal(signal.SIGUSR1, handler)
self.logger.info('waiting for connections...')
- if CONFIG['netrpc']:
- netrpcd.start()
- if CONFIG['xmlrpc']:
- xmlrpcd.start()
- if CONFIG['jsonrpc']:
- jsonrpcd.start()
- if CONFIG['webdav']:
- webdavd.start()
+ for servers in (xmlrpcd, jsonrpcd, webdavd):
+ for server in servers:
+ server.start()
if CONFIG['psyco']:
import psyco
diff --git a/trytond/test/test.py b/trytond/test/test.py
index abca5df215..01c1092d55 100644
--- a/trytond/test/test.py
+++ b/trytond/test/test.py
@@ -100,7 +100,8 @@ class FloatDigits(ModelSQL):
_description = __doc__
digits = fields.Integer('Digits')
float = fields.Float(string='Float', help='Test float',
- required=False, digits=(16, Eval('digits', 2)))
+ required=False, digits=(16, Eval('digits', 2)),
+ depends=['digits'])
FloatDigits()
@@ -144,7 +145,8 @@ class NumericDigits(ModelSQL):
_description = __doc__
digits = fields.Integer('Digits')
numeric = fields.Numeric(string='Numeric', help='Test numeric',
- required=False, digits=(16, Eval('digits', 2)))
+ required=False, digits=(16, Eval('digits', 2)),
+ depends=['digits'])
NumericDigits()
diff --git a/trytond/tests/test_mixins.py b/trytond/tests/test_mixins.py
index 3146726758..735913e52e 100644
--- a/trytond/tests/test_mixins.py
+++ b/trytond/tests/test_mixins.py
@@ -26,28 +26,28 @@ class UrlTestCase(unittest.TestCase):
"Test model URLs"
with Transaction().start(DB_NAME, USER, CONTEXT):
self.assertEqual(self.urlmodel.get_url(),
- 'tryton://%s:%s/%s/model/test.urlobject' % (self.hostname,
- CONFIG['netport'], urllib.quote(DB_NAME)))
+ 'tryton://%s/%s/model/test.urlobject' % (self.hostname,
+ urllib.quote(DB_NAME)))
server_name = 'michaelscott.paper.test'
CONFIG['hostname'] = server_name
self.assertEqual(self.urlmodel.get_url(),
- 'tryton://%s:%s/%s/model/test.urlobject' % (server_name,
- CONFIG['netport'], urllib.quote(DB_NAME)))
+ 'tryton://%s/%s/model/test.urlobject' % (server_name,
+ urllib.quote(DB_NAME)))
def testWizardURL(self):
"Test wizard URLs"
with Transaction().start(DB_NAME, USER, CONTEXT):
CONFIG['hostname'] = None
self.assertEqual(self.urlwizard.get_url(),
- 'tryton://%s:%s/%s/wizard/test.test_wizard' % (self.hostname,
- CONFIG['netport'], urllib.quote(DB_NAME)))
+ 'tryton://%s/%s/wizard/test.test_wizard' % (self.hostname,
+ urllib.quote(DB_NAME)))
server_name = 'michaelscott.paper.test'
CONFIG['hostname'] = server_name
self.assertEqual(self.urlwizard.get_url(),
- 'tryton://%s:%s/%s/wizard/test.test_wizard' % (server_name,
- CONFIG['netport'], urllib.quote(DB_NAME)))
+ 'tryton://%s/%s/wizard/test.test_wizard' % (server_name,
+ urllib.quote(DB_NAME)))
def suite():
diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py
index 1ca1ea971c..e93ca23685 100644
--- a/trytond/tests/test_tryton.py
+++ b/trytond/tests/test_tryton.py
@@ -33,6 +33,7 @@ from trytond.pool import Pool
from trytond.backend import Database
from trytond.protocols.dispatcher import create
from trytond.transaction import Transaction
+from trytond.pyson import PYSONEncoder, Eval
register_classes()
@@ -87,6 +88,22 @@ class ModelViewTestCase(unittest.TestCase):
'''
test_view('webdav')
+
+class FieldDependsTestCase(unittest.TestCase):
+ '''
+ Test Field depends
+ '''
+
+ def setUp(self):
+ install_module('ir')
+ install_module('res')
+ install_module('workflow')
+ install_module('webdav')
+
+ def test0010depends(self):
+ test_depends()
+
+
def install_module(name):
'''
Install module for the tested database
@@ -154,6 +171,39 @@ def test_view(module_name):
% field)
transaction.cursor.rollback()
+def test_depends():
+ '''
+ Test for missing depends
+ '''
+ class Encoder(PYSONEncoder):
+
+ def __init__(self, *args, **kwargs):
+ super(Encoder, self).__init__(*args, **kwargs)
+ self.fields = set()
+
+ def default(self, obj):
+ if isinstance(obj, Eval):
+ fname = obj._value
+ if not fname.startswith('_parent_'):
+ self.fields.add(fname)
+ return super(Encoder, self).default(obj)
+
+ with Transaction().start(DB_NAME, USER, CONTEXT) as transaction:
+ for mname, model in Pool().iterobject():
+ for fname, field in model._columns.iteritems():
+ encoder = Encoder()
+ for pyson in (field.domain, field.states):
+ encoder.encode(pyson)
+ if hasattr(field, 'digits'):
+ encoder.encode(field.digits)
+ encoder.fields.discard(fname)
+ encoder.fields.discard('context')
+ encoder.fields.discard('_user')
+ depends = set(field.depends)
+ assert encoder.fields <= depends, (
+ 'Missing depends %s in "%s"."%s"' % (
+ list(encoder.fields - depends), mname, fname))
+
def suite():
'''
Return test suite for other modules
@@ -175,6 +225,8 @@ def all_suite():
suite_.addTests(test_modelsingleton.suite())
suite_.addTests(unittest.TestLoader(
).loadTestsFromTestCase(ModelViewTestCase))
+ suite_.addTests(unittest.TestLoader(
+ ).loadTestsFromTestCase(FieldDependsTestCase))
import trytond.tests.test_mptt as test_mptt
suite_.addTests(test_mptt.suite())
import trytond.tests.test_importdata as test_importdata
diff --git a/trytond/url.py b/trytond/url.py
index cd5a1177a8..e98c970e66 100644
--- a/trytond/url.py
+++ b/trytond/url.py
@@ -15,13 +15,12 @@ class URLMixin(object):
from trytond.model import Model
from trytond.wizard import Wizard
from trytond.report import Report
- url_part = {}
hostname = CONFIG['hostname'] or unicode(socket.getfqdn(), 'utf8')
- url_part['hostname'] = '.'.join(encodings.idna.ToASCII(part) for part in
+ hostname = '.'.join(encodings.idna.ToASCII(part) for part in
hostname.split('.'))
- url_part['port'] = CONFIG['netport']
+ url_part = {}
if isinstance(self, Model):
url_part['type'] = 'model'
elif isinstance(self, Wizard):
@@ -34,6 +33,5 @@ class URLMixin(object):
url_part['name'] = self._name
url_part['database'] = Transaction().cursor.database_name
- host_part = '%(hostname)s:%(port)s' % url_part
local_part = urllib.quote('%(database)s/%(type)s/%(name)s' % url_part)
- return 'tryton://%s/%s' % (host_part, local_part)
+ return 'tryton://%s/%s' % (hostname, local_part)
diff --git a/trytond/workflow/workflow.py b/trytond/workflow/workflow.py
index ad4592a896..43fc63d34f 100644
--- a/trytond/workflow/workflow.py
+++ b/trytond/workflow/workflow.py
@@ -6,7 +6,7 @@ from trytond.model import ModelView, ModelSQL, fields
from trytond.report import Report
from trytond.tools import exec_command_pipe
from trytond.backend import TableHandler
-from trytond.pyson import Eval, Equal, Not, Bool
+from trytond.pyson import Eval, Not, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool