+
+
+
+
Invoice and shipping address:
+
Invoice address:
+
+ <...>
+
+
+
+
+
+
+The main template calls translate_doc with ``partner_id.lang`` as a parameter,
+which means it uses :ref:`a custom report model
+
` to access a ``res.partner`` record.
+
+Barcodes
+--------
+
+Barcodes are images returned by a controller and can easily be embedded in
+reports thanks to the QWeb syntax:
+
+.. code-block:: html
+
+
+
+More parameters can be passed as a query string
+
+.. code-block:: html
+
+
+
+
+Useful Remarks
+--------------
+* Twitter Bootstrap and FontAwesome classes can be used in your report
+ template
+* Local CSS can be put directly in the template
+
+* Global CSS can be inserted in the main report layout by inheriting its
+ template and inserting your CSS::
+
+
+
+
+
+
+
+.. _reference/reports/paper_formats:
+
+Paper Format
+============
+
+Paper formats are records of ``report.paperformat`` and can contain the
+following attributes:
+
+``name`` (mandatory)
+ only useful as a mnemonic/description of the report when looking for one
+ in a list of some sort
+``description``
+ a small description of your format
+``format``
+ either a predefined format (A0 to A9, B0 to B10, Legal, Letter,
+ Tabloid,...) or ``custom``; A4 by default. You cannot use a non-custom
+ format if you define the page dimensions.
+``dpi``
+ output DPI; 90 by default
+``margin_top``, ``margin_bottom``, ``margin_left``, ``margin_right``
+ margin sizes in mm
+``page_height``, ``page_width``
+ page dimensions in mm
+``orientation``
+ Landscape or Portrait
+``header_line``
+ boolean to display a header line
+``header_spacing``
+ header spacing in mm
+
+Example::
+
+
+ French Bank Check
+
+ custom
+ 80
+ 175
+ Portrait
+ 3
+ 3
+ 3
+ 3
+
+ 3
+ 80
+
+
+.. _reference/reports/custom_reports:
+
+Custom Reports
+==============
+
+The report model has a default ``get_html`` function that looks for a model
+named :samp:`report.{module.report_name}`. If it exists, it will use it to
+call the QWeb engine; otherwise a generic function will be used. If you wish
+to customize your reports by including more things in the template (like
+records of others models, for example), you can define this model, overwrite
+the function ``render_html`` and pass objects in the ``docargs`` dictionnary:
.. code-block:: python
from openerp import api, models
-
class ParticularReport(models.AbstractModel):
- _name = 'report.<>'
+ _name = 'report.module.report_name'
@api.multi
def render_html(self, data=None):
report_obj = self.env['report']
- report = report_obj._get_report_from_name('<>')
+ report = report_obj._get_report_from_name('module.report_name')
docargs = {
'doc_ids': self._ids,
'doc_model': report.model,
'docs': self,
}
- return report_obj.render('<>', docargs)
+ return report_obj.render('module.report_name', docargs)
-.. _time: https://docs.python.org/2/library/time.html
+Reports are web pages
+=====================
+
+Reports are dynamically generated by the report module and can be accessed
+directly via URL:
+
+For example, you can access a Sale Order report in html mode by going to
+\http:///report/html/sale.report_saleorder/38
+
+Or you can access the pdf version at
+\http:///report/pdf/sale.report_saleorder/38
+
+.. _wkhtmltopdf: http://wkhtmltopdf.org
diff --git a/doc/setup/install.rst b/doc/setup/install.rst
index 7fb83ea4667..4221eb15225 100644
--- a/doc/setup/install.rst
+++ b/doc/setup/install.rst
@@ -316,7 +316,7 @@ Source installation requires manually installing dependencies:
they require compilation.
- add ``pypiwin32`` because it's needed under windows.
- Then use pip to install install the dependencies using the following
+ Then use pip to install the dependencies using the following
command from a cmd.exe prompt (replace ``\YourOdooPath`` by the actual
path where you downloaded Odoo):
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index bca23a9bbcd..6fd21c97d95 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -591,17 +591,15 @@ class ir_actions_server(osv.osv):
'condition': 'True',
'type': 'ir.actions.server',
'sequence': 5,
- 'code': """# You can use the following variables:
-# - self: ORM model of the record on which the action is triggered
+ 'code': """# Available locals:
+# - time, datetime, dateutil: Python libraries
+# - env: Odoo Environement
+# - model: Model of the record on which the action is triggered
# - object: Record on which the action is triggered if there is one, otherwise None
-# - pool: ORM model pool (i.e. self.pool)
-# - cr: database cursor
-# - uid: current user id
-# - context: current context
-# - time: Python time module
# - workflow: Workflow engine
# - log : log(message), function to log debug information in logging table
-# If you plan to return an action, assign: action = {...}""",
+# - Warning: Warning Exception to use with raise
+# To return an action, assign: action = {...}""",
'use_relational_model': 'base',
'use_create': 'new',
'use_write': 'current',
@@ -971,18 +969,30 @@ class ir_actions_server(osv.osv):
eval_context = super(ir_actions_server, self)._get_eval_context(cr, uid, action=action, context=context)
obj_pool = self.pool[action.model_id.model]
+ env = openerp.api.Environment(cr, uid, context)
+ model = env[action.model_id.model]
obj = None
if context.get('active_model') == action.model_id.model and context.get('active_id'):
- obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
+ obj = model.browse(context['active_id'])
eval_context.update({
- 'self': obj_pool,
+ # orm
+ 'env': env,
+ 'model': model,
+ 'workflow': workflow,
+ # Exceptions
+ 'Warning': openerp.exceptions.Warning,
+ # record
+ # TODO: When porting to master move badly named obj and object to
+ # deprecated and define record (active_id) and records (active_ids)
'object': obj,
'obj': obj,
+ # Deprecated use env or model instead
+ 'self': obj_pool,
'pool': self.pool,
'cr': cr,
'context': context,
- 'workflow': workflow,
- 'Warning': openerp.exceptions.Warning,
+ 'user': env.user,
+ # helpers
'log': log,
})
return eval_context
diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py
index 002e428acff..8beb80e3a3b 100644
--- a/openerp/addons/base/ir/ir_attachment.py
+++ b/openerp/addons/base/ir/ir_attachment.py
@@ -362,7 +362,7 @@ class ir_attachment(osv.osv):
ima.check(cr, uid, model, mode)
self.pool[model].check_access_rule(cr, uid, existing_ids, mode, context=context)
if require_employee:
- if not self.pool['res.users'].has_group(cr, uid, 'base.group_user'):
+ if not uid == SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_user'):
raise AccessError(_("Sorry, you are not allowed to access this document."))
def _search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
diff --git a/openerp/addons/base/ir/ir_mail_server.py b/openerp/addons/base/ir/ir_mail_server.py
index 3cb8405c647..ca411f5a70b 100644
--- a/openerp/addons/base/ir/ir_mail_server.py
+++ b/openerp/addons/base/ir/ir_mail_server.py
@@ -363,6 +363,27 @@ class ir_mail_server(osv.osv):
msg.attach(part)
return msg
+ def _get_default_bounce_address(self, cr, uid, context=None):
+ '''Compute the default bounce address.
+
+ The default bounce address is used to set the envelop address if no
+ envelop address is provided in the message. It is formed by properly
+ joining the parameters "mail.catchall.alias" and
+ "mail.catchall.domain".
+
+ If "mail.catchall.alias" is not set it defaults to "postmaster-odoo".
+
+ If "mail.catchall.domain" is not set, return None.
+
+ '''
+ get_param = self.pool['ir.config_parameter'].get_param
+ postmaster = get_param(cr, uid, 'mail.bounce.alias',
+ default='postmaster-odoo',
+ context=context,)
+ domain = get_param(cr, uid, 'mail.catchall.domain', context=context)
+ if postmaster and domain:
+ return '%s@%s' % (postmaster, domain)
+
def send_email(self, cr, uid, message, mail_server_id=None, smtp_server=None, smtp_port=None,
smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False,
context=None):
@@ -378,8 +399,9 @@ class ir_mail_server(osv.osv):
and fails if not found.
:param message: the email.message.Message to send. The envelope sender will be extracted from the
- ``Return-Path`` or ``From`` headers. The envelope recipients will be
- extracted from the combined list of ``To``, ``CC`` and ``BCC`` headers.
+ ``Return-Path`` (if present), or will be set to the default bounce address.
+ The envelope recipients will be extracted from the combined list of ``To``,
+ ``CC`` and ``BCC`` headers.
:param mail_server_id: optional id of ir.mail_server to use for sending. overrides other smtp_* arguments.
:param smtp_server: optional hostname of SMTP server to use
:param smtp_encryption: optional TLS mode, one of 'none', 'starttls' or 'ssl' (see ir.mail_server fields for explanation)
@@ -390,7 +412,14 @@ class ir_mail_server(osv.osv):
:return: the Message-ID of the message that was just sent, if successfully sent, otherwise raises
MailDeliveryException and logs root cause.
"""
- smtp_from = message['Return-Path'] or message['From']
+ # Use the default bounce address **only if** no Return-Path was
+ # provided by caller. Caller may be using Variable Envelope Return
+ # Path (VERP) to detect no-longer valid email addresses.
+ smtp_from = message['Return-Path']
+ if not smtp_from:
+ smtp_from = self._get_default_bounce_address(cr, uid, context=context)
+ if not smtp_from:
+ smtp_from = message['From']
assert smtp_from, "The Return-Path or From header is required for any outbound email"
# The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters.
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index 87223741f8d..65ff60b66db 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -1068,6 +1068,7 @@ class view(osv.osv):
if values is None:
values = dict()
qcontext = dict(
+ env=api.Environment(cr, uid, context),
keep_query=keep_query,
request=request, # might be unbound if we're not in an httprequest context
debug=request.debug if request else False,
diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py
index 8f7b1c1835b..2eebec90767 100644
--- a/openerp/addons/base/res/res_currency.py
+++ b/openerp/addons/base/res/res_currency.py
@@ -301,3 +301,22 @@ class res_currency_rate(osv.osv):
'name': lambda *a: time.strftime('%Y-%m-%d 00:00:00'),
}
_order = "name desc"
+
+ def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
+ if operator in ['=', '!=']:
+ try:
+ date_format = '%Y-%m-%d'
+ if context.get('lang'):
+ lang_obj = self.pool['res.lang']
+ lang_ids = lang_obj.search(cr, user, [('code', '=', context['lang'])], context=context)
+ if lang_ids:
+ date_format = lang_obj.browse(cr, user, lang_ids[0], context=context).date_format
+ name = time.strftime('%Y-%m-%d', time.strptime(name, date_format))
+ except ValueError:
+ try:
+ args.append(('rate', operator, float(name)))
+ except ValueError:
+ return []
+ name = ''
+ operator = 'ilike'
+ return super(res_currency_rate, self).name_search(cr, user, name, args=args, operator=operator, context=context, limit=limit)
diff --git a/openerp/fields.py b/openerp/fields.py
index ca308bcad5d..71d077143fa 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -317,8 +317,9 @@ class Field(object):
attrs['copy'] = attrs.get('copy', False)
attrs['readonly'] = attrs.get('readonly', not attrs.get('inverse'))
if attrs.get('related'):
- # by default, related fields are not stored
+ # by default, related fields are not stored and not copied
attrs['store'] = attrs.get('store', False)
+ attrs['copy'] = attrs.get('copy', False)
# fix for function fields overridden by regular columns
if not isinstance(attrs.get('column'), (NoneType, fields.function)):
diff --git a/openerp/http.py b/openerp/http.py
index 439be25c4ff..bdd009bfedf 100644
--- a/openerp/http.py
+++ b/openerp/http.py
@@ -398,14 +398,18 @@ def route(route=None, **kw):
response = f(*args, **kw)
if isinstance(response, Response) or f.routing_type == 'json':
return response
- elif isinstance(response, werkzeug.wrappers.BaseResponse):
+
+ if isinstance(response, basestring):
+ return Response(response)
+
+ if isinstance(response, werkzeug.exceptions.HTTPException):
+ response = response.get_response()
+ if isinstance(response, werkzeug.wrappers.BaseResponse):
response = Response.force_type(response)
response.set_default()
return response
- elif isinstance(response, basestring):
- return Response(response)
- else:
- _logger.warn(" returns an invalid response type for an http request" % (f.__module__, f.__name__))
+
+ _logger.warn(" returns an invalid response type for an http request" % (f.__module__, f.__name__))
return response
response_wrap.routing = routing
response_wrap.original_func = f
diff --git a/openerp/models.py b/openerp/models.py
index c3d47da103b..5937aaee264 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -188,7 +188,7 @@ def get_pg_type(f, type_override=None):
elif issubclass(field_type, (fields.char, fields.reference)):
pg_type = ('varchar', pg_varchar(f.size))
elif issubclass(field_type, fields.selection):
- if (isinstance(f.selection, list) and isinstance(f.selection[0][0], int))\
+ if (f.selection and isinstance(f.selection, list) and isinstance(f.selection[0][0], int))\
or getattr(f, 'size', None) == -1:
pg_type = ('int4', 'INTEGER')
else:
@@ -2504,7 +2504,7 @@ class BaseModel(object):
('numeric', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]),
('float8', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]),
]
- if f_pg_type == 'varchar' and f._type == 'char' and f_pg_size and (f.size is None or f_pg_size < f.size):
+ if f_pg_type == 'varchar' and f._type in ('char', 'selection') and f_pg_size and (f.size is None or f_pg_size < f.size):
try:
with cr.savepoint():
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" TYPE %s' % (self._table, k, pg_varchar(f.size)))
diff --git a/openerp/modules/module.py b/openerp/modules/module.py
index 952fe5bad5e..5df069506c5 100644
--- a/openerp/modules/module.py
+++ b/openerp/modules/module.py
@@ -31,6 +31,7 @@ import re
import sys
import time
import unittest
+import threading
from os.path import join as opj
import unittest2
@@ -461,6 +462,7 @@ def run_unit_tests(module_name, dbname, position=runs_at_install):
global current_test
current_test = module_name
mods = get_test_modules(module_name)
+ threading.currentThread().testing = True
r = True
for m in mods:
tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m))
@@ -478,6 +480,7 @@ def run_unit_tests(module_name, dbname, position=runs_at_install):
_logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors))
current_test = None
+ threading.currentThread().testing = False
return r
def unwrap_suite(test):
diff --git a/openerp/tools/config.py b/openerp/tools/config.py
index 1d0be528cbe..9b90c36394a 100644
--- a/openerp/tools/config.py
+++ b/openerp/tools/config.py
@@ -48,8 +48,7 @@ class MyOption (optparse.Option, object):
self.my_default = attrs.pop('my_default', None)
super(MyOption, self).__init__(*opts, **attrs)
-DEFAULT_LOG_HANDLER = [':INFO']
-
+DEFAULT_LOG_HANDLER = ':INFO'
def _get_default_datadir():
home = os.path.expanduser('~')
if os.path.isdir(home):
@@ -62,6 +61,20 @@ def _get_default_datadir():
# No "version" kwarg as session and filestore paths are shared against series
return func(appname=release.product_name, appauthor=release.author)
+def _deduplicate_loggers(loggers):
+ """ Avoid saving multiple logging levels for the same loggers to a save
+ file, that just takes space and the list can potentially grow unbounded
+ if for some odd reason people use :option`odoo.py --save`` all the time.
+ """
+ # dict(iterable) -> the last item of iterable for any given key wins,
+ # which is what we want and expect. Output order should not matter as
+ # there are no duplicates within the output sequence
+ return (
+ '{}:{}'.format(logger, level)
+ for logger, level in dict(it.split(':') for it in loggers).iteritems()
+ )
+
+
class configmanager(object):
def __init__(self, fname=None):
"""Constructor.
@@ -160,7 +173,7 @@ class configmanager(object):
group.add_option("--logfile", dest="logfile", help="file where the server log will be stored")
group.add_option("--logrotate", dest="logrotate", action="store_true", my_default=False, help="enable logfile rotation")
group.add_option("--syslog", action="store_true", dest="syslog", my_default=False, help="Send the log to the syslog server")
- group.add_option('--log-handler', action="append", default=DEFAULT_LOG_HANDLER, my_default=DEFAULT_LOG_HANDLER, metavar="PREFIX:LEVEL", help='setup a handler at LEVEL for a given PREFIX. An empty PREFIX indicates the root logger. This option can be repeated. Example: "openerp.orm:DEBUG" or "werkzeug:CRITICAL" (default: ":INFO")')
+ group.add_option('--log-handler', action="append", default=[], my_default=DEFAULT_LOG_HANDLER, metavar="PREFIX:LEVEL", help='setup a handler at LEVEL for a given PREFIX. An empty PREFIX indicates the root logger. This option can be repeated. Example: "openerp.orm:DEBUG" or "werkzeug:CRITICAL" (default: ":INFO")')
group.add_option('--log-request', action="append_const", dest="log_handler", const="openerp.http.rpc.request:DEBUG", help='shortcut for --log-handler=openerp.http.rpc.request:DEBUG')
group.add_option('--log-response', action="append_const", dest="log_handler", const="openerp.http.rpc.response:DEBUG", help='shortcut for --log-handler=openerp.http.rpc.response:DEBUG')
group.add_option('--log-web', action="append_const", dest="log_handler", const="openerp.http:DEBUG", help='shortcut for --log-handler=openerp.http:DEBUG')
@@ -369,14 +382,14 @@ class configmanager(object):
'email_from', 'smtp_server', 'smtp_user', 'smtp_password',
'db_maxconn', 'import_partial', 'addons_path',
'xmlrpc', 'syslog', 'without_demo',
- 'dbfilter', 'log_handler', 'log_level', 'log_db',
+ 'dbfilter', 'log_level', 'log_db',
'geoip_database',
]
for arg in keys:
# Copy the command-line argument (except the special case for log_handler, due to
# action=append requiring a real default, so we cannot use the my_default workaround)
- if getattr(opt, arg) and getattr(opt, arg) != DEFAULT_LOG_HANDLER:
+ if getattr(opt, arg):
self.options[arg] = getattr(opt, arg)
# ... or keep, but cast, the config file value.
elif isinstance(self.options[arg], basestring) and self.casts[arg].type in optparse.Option.TYPE_CHECKER:
@@ -384,6 +397,7 @@ class configmanager(object):
if isinstance(self.options['log_handler'], basestring):
self.options['log_handler'] = self.options['log_handler'].split(',')
+ self.options['log_handler'].extend(opt.log_handler)
# if defined but None take the configfile value
keys = [
@@ -566,6 +580,8 @@ class configmanager(object):
continue
if opt in ('log_level',):
p.set('options', opt, loglevelnames.get(self.options[opt], self.options[opt]))
+ elif opt == 'log_handler':
+ p.set('options', opt, ','.join(_deduplicate_loggers(self.options[opt])))
else:
p.set('options', opt, self.options[opt])
diff --git a/openerp/tools/safe_eval.py b/openerp/tools/safe_eval.py
index e30a3554364..4eef310f68c 100644
--- a/openerp/tools/safe_eval.py
+++ b/openerp/tools/safe_eval.py
@@ -32,6 +32,7 @@ condition/math builtins.
# - safe_eval in tryton http://hg.tryton.org/hgwebdir.cgi/trytond/rev/bbb5f73319ad
from opcode import HAVE_ARGUMENT, opmap, opname
+from psycopg2 import OperationalError
from types import CodeType
import logging
@@ -321,6 +322,10 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal
raise
except openerp.exceptions.AccessError:
raise
+ except OperationalError:
+ # Do not hide PostgreSQL low-level exceptions, to let the auto-replay
+ # of serialized transactions work its magic
+ raise
except openerp.exceptions.MissingError:
raise
except Exception, e: