diff --git a/openerp/addons/base/base_data.xml b/openerp/addons/base/base_data.xml index feaac206723..94a3780653f 100644 --- a/openerp/addons/base/base_data.xml +++ b/openerp/addons/base/base_data.xml @@ -1,6 +1,10 @@ + + web.base.url + http://localhost:8069 + ir.ui.menu.tree ir.ui.menu diff --git a/openerp/addons/base/ir/ir_mail_server.py b/openerp/addons/base/ir/ir_mail_server.py index e8d97160068..f175a27077f 100644 --- a/openerp/addons/base/ir/ir_mail_server.py +++ b/openerp/addons/base/ir/ir_mail_server.py @@ -236,15 +236,18 @@ class ir_mail_server(osv.osv): return connection def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, - attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None): + attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None, + body_alternative=None, subtype_alternative='plain'): """Constructs an RFC2822 email.message.Message object based on the keyword arguments passed, and returns it. :param string email_from: sender email address :param list email_to: list of recipient addresses (to be joined with commas) :param string subject: email subject (no pre-encoding/quoting necessary) - :param string body: email body, according to the ``subtype`` (by default, plaintext). + :param string body: email body, of the type ``subtype`` (by default, plaintext). If html subtype is used, the message will be automatically converted - to plaintext and wrapped in multipart/alternative. + to plaintext and wrapped in multipart/alternative, unless an explicit + ``body_alternative`` version is passed. + :param string body_alternative: optional alternative body, of the type specified in ``subtype_alternative`` :param string reply_to: optional value of Reply-To header :param string object_id: optional tracking identifier, to be included in the message-id for recognizing replies. Suggested format for object-id is "res_id-model", @@ -252,6 +255,8 @@ class ir_mail_server(osv.osv): :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'), must match the format of the ``body`` parameter. Default is 'plain', making the content part of the mail "text/plain". + :param string subtype_alternative: optional mime subtype of ``body_alternative`` (usually 'plain' + or 'html'). Default is 'plain'. :param list attachments: list of (filename, filecontents) pairs, where filecontents is a string containing the bytes of the attachment :param list email_cc: optional list of string values for CC header (to be joined with commas) @@ -276,7 +281,7 @@ class ir_mail_server(osv.osv): if not body: body = u'' email_body_utf8 = ustr(body).encode('utf-8') - email_text_part = MIMEText(email_body_utf8 or '', _subtype=subtype, _charset='utf-8') + email_text_part = MIMEText(email_body_utf8, _subtype=subtype, _charset='utf-8') msg = MIMEMultipart() if not message_id: @@ -304,13 +309,21 @@ class ir_mail_server(osv.osv): for key, value in headers.iteritems(): msg[ustr(key).encode('utf-8')] = encode_header(value) - if html2text and subtype == 'html': - # Always provide alternative text body if possible. + if subtype == 'html' and not body_alternative and html2text: + # Always provide alternative text body ourselves if possible. text_utf8 = tools.html2text(email_body_utf8.decode('utf-8')).encode('utf-8') alternative_part = MIMEMultipart(_subtype="alternative") alternative_part.attach(MIMEText(text_utf8, _charset='utf-8', _subtype='plain')) alternative_part.attach(email_text_part) msg.attach(alternative_part) + elif body_alternative: + # Include both alternatives, as specified, within a multipart/alternative part + alternative_part = MIMEMultipart(_subtype="alternative") + body_alternative_utf8 = ustr(body_alternative).encode('utf-8') + alternative_body_part = MIMEText(body_alternative_utf8, _subtype=subtype_alternative, _charset='utf-8') + alternative_part.attach(alternative_body_part) + alternative_part.attach(email_text_part) + msg.attach(alternative_part) else: msg.attach(email_text_part) diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index 01d8b745e6f..4e7029d5172 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -18,6 +18,7 @@ # along with this program. If not, see . # ############################################################################## +import re import time import netsvc from osv import fields, osv @@ -26,6 +27,8 @@ import tools from tools.misc import currency from tools.translate import _ +CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?') + class res_currency(osv.osv): def _current_rate(self, cr, uid, ids, name, arg, context=None): if context is None: @@ -68,6 +71,8 @@ class res_currency(osv.osv): _defaults = { 'active': lambda *a: 1, 'position' : 'after', + 'rounding': 0.01, + 'accuracy': 4, } _sql_constraints = [ # this constraint does not cover all cases due to SQL NULL handling for company_id, @@ -101,6 +106,18 @@ class res_currency(osv.osv): r['date'] = currency_date return res + def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): + if not args: + args = [] + if name: + ids = self.search(cr, user, ([('name','=',name)] + args), limit=limit, context=context) + name_match = CURRENCY_DISPLAY_PATTERN.match(name) + if not ids and name_match: + ids = self.search(cr, user, [('name','=', name_match.group(1))] + args, limit=limit, context=context) + else: + ids = self.search(cr, user, args, limit=limit, context=context) + return self.name_get(cr, user, ids, context=context) + def name_get(self, cr, uid, ids, context=None): if not ids: return [] diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 9746db40cc1..ae58c9a9f33 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -185,7 +185,7 @@ class res_partner(osv.osv): def name_get(self, cr, uid, ids, context={}): if not len(ids): return [] - if context.get('show_ref', False): + if context and context.get('show_ref'): rec_name = 'ref' else: rec_name = 'name' @@ -196,8 +196,6 @@ class res_partner(osv.osv): def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100): if not args: args=[] - if not context: - context={} if name: ids = self.search(cr, uid, [('ref', '=', name)] + args, limit=limit, context=context) if not ids: @@ -312,6 +310,8 @@ class res_partner_address(osv.osv): } def name_get(self, cr, user, ids, context={}): + if context is None: + context = {} if not len(ids): return [] res = [] diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 9c6d493e48c..47f6ac34e1b 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -304,7 +304,8 @@ class browse_record(object): self._cr = cr self._uid = uid self._id = id - self._table = table + self._table = table # deprecated, use _model! + self._model = table self._table_name = self._table._name self.__logger = logging.getLogger( 'osv.browse_record.' + self._table_name) @@ -816,13 +817,16 @@ class BaseModel(object): raise TypeError('_name is mandatory in case of multiple inheritance') for parent_name in ((type(parent_names)==list) and parent_names or [parent_names]): - parent_class = pool.get(parent_name).__class__ - if not pool.get(parent_name): + parent_model = pool.get(parent_name) + if not getattr(cls, '_original_module', None) and name == parent_model._name: + cls._original_module = parent_model._original_module + if not parent_model: raise TypeError('The model "%s" specifies an unexisting parent class "%s"\n' 'You may need to add a dependency on the parent class\' module.' % (name, parent_name)) + parent_class = parent_model.__class__ nattr = {} for s in attributes: - new = copy.copy(getattr(pool.get(parent_name), s, {})) + new = copy.copy(getattr(parent_model, s, {})) if s == '_columns': # Don't _inherit custom fields. for c in new.keys(): @@ -850,6 +854,8 @@ class BaseModel(object): new.extend(cls.__dict__.get(s, [])) nattr[s] = new cls = type(name, (cls, parent_class), dict(nattr, _register=False)) + if not getattr(cls, '_original_module', None): + cls._original_module = cls._module obj = object.__new__(cls) obj.__init__(pool, cr) return obj @@ -1045,6 +1051,7 @@ class BaseModel(object): 'name': n, 'model': self._name, 'res_id': r['id'], + 'module': '__export__', }) r = n else: @@ -4612,17 +4619,21 @@ class BaseModel(object): return False return True - def _get_xml_ids(self, cr, uid, ids, *args, **kwargs): - """Find out the XML ID(s) of any database record. + def _get_external_ids(self, cr, uid, ids, *args, **kwargs): + """Retrieve the External ID(s) of any database record. **Synopsis**: ``_get_xml_ids(cr, uid, ids) -> { 'id': ['module.xml_id'] }`` - :return: map of ids to the list of their fully qualified XML IDs - (empty list when there's none). + :return: map of ids to the list of their fully qualified External IDs + in the form ``module.key``, or an empty list when there's no External + ID for a record, e.g.:: + + { 'id': ['module.ext_id', 'module.ext_id_bis'], + 'id2': [] } """ - model_data_obj = self.pool.get('ir.model.data') - data_ids = model_data_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)]) - data_results = model_data_obj.read(cr, uid, data_ids, ['module', 'name', 'res_id']) + ir_model_data = self.pool.get('ir.model.data') + data_ids = ir_model_data.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)]) + data_results = ir_model_data.read(cr, uid, data_ids, ['module', 'name', 'res_id']) result = {} for id in ids: # can't use dict.fromkeys() as the list would be shared! @@ -4631,29 +4642,35 @@ class BaseModel(object): result[record['res_id']].append('%(module)s.%(name)s' % record) return result - def get_xml_id(self, cr, uid, ids, *args, **kwargs): - """Find out the XML ID of any database record, if there + def get_external_id(self, cr, uid, ids, *args, **kwargs): + """Retrieve the External ID of any database record, if there is one. This method works as a possible implementation for a function field, to be able to add it to any - model object easily, referencing it as ``osv.osv.get_xml_id``. + model object easily, referencing it as ``Model.get_external_id``. - When multiple XML IDs exist for a record, only one + When multiple External IDs exist for a record, only one of them is returned (randomly). - **Synopsis**: ``get_xml_id(cr, uid, ids) -> { 'id': 'module.xml_id' }`` - :return: map of ids to their fully qualified XML ID, defaulting to an empty string when there's none - (to be usable as a function field). + (to be usable as a function field), + e.g.:: + + { 'id': 'module.ext_id', + 'id2': '' } """ results = self._get_xml_ids(cr, uid, ids) - for k, v in results.items(): + for k, v in results.iteritems(): if results[k]: results[k] = v[0] else: results[k] = '' return results + # backwards compatibility + get_xml_id = get_external_id + _get_xml_ids = _get_external_ids + # Transience def is_transient(self): """ Return whether the model is transient. diff --git a/openerp/tools/safe_eval.py b/openerp/tools/safe_eval.py index be7b222f67f..02fcbc19c63 100644 --- a/openerp/tools/safe_eval.py +++ b/openerp/tools/safe_eval.py @@ -64,7 +64,7 @@ _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [ 'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3', # New in Python 2.7 - http://bugs.python.org/issue4715 : 'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE', - 'POP_JUMP_IF_TRUE' + 'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY' ] if x in opmap)) _logger = logging.getLogger('safe_eval') diff --git a/openerp/tools/yaml_import.py b/openerp/tools/yaml_import.py index bb4ce9db87d..91cdda1b896 100644 --- a/openerp/tools/yaml_import.py +++ b/openerp/tools/yaml_import.py @@ -340,6 +340,7 @@ class YamlInterpreter(object): return record_dict def process_ref(self, node, column=None): + assert node.search or node.id, '!ref node should have a `search` attribute or `id` attribute' if node.search: if node.model: model_name = node.model @@ -377,7 +378,10 @@ class YamlInterpreter(object): if column._type in ("many2many", "one2many"): value = [(6, 0, elements)] else: # many2one - value = self._get_first_result(elements) + if isinstance(elements, (list,tuple)): + value = self._get_first_result(elements) + else: + value = elements elif column._type == "many2one": value = self.get_id(expression) elif column._type == "one2many": diff --git a/openerp/workflow/wkf_expr.py b/openerp/workflow/wkf_expr.py index 468e5225212..608af34d118 100644 --- a/openerp/workflow/wkf_expr.py +++ b/openerp/workflow/wkf_expr.py @@ -60,7 +60,7 @@ def _eval_expr(cr, ident, workitem, action): def execute_action(cr, ident, workitem, activity): obj = pooler.get_pool(cr.dbname).get('ir.actions.server') - ctx = {'active_id':ident[2], 'active_ids':[ident[2]]} + ctx = {'active_model':ident[1], 'active_id':ident[2], 'active_ids':[ident[2]]} result = obj.run(cr, ident[0], [activity['action_id']], ctx) return result