[MERGE] various bugfixes/improvements (coming from EDI development)

- _original_module is now available on model/browse_records
 - context usage in res.partner.*
 - proper name_search() + default values for res.currency
 - active_model in wkf exec context
 - safe_eval allows try/except/finally
 - yaml_import: !ref {id: xml_id} works
 - ir_mail_server: support for alternative body/subtype
 - default value for web.base.url config parameter
 - consistency rename: Model.*get_xml_id* -> *get_external_id*

bzr revid: odo@openerp.com-20111005100954-c8mbd4kz6kkqaj84
This commit is contained in:
Olivier Dony 2011-10-05 12:09:54 +02:00
commit d13fb7fe7e
8 changed files with 86 additions and 31 deletions

View File

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="parameter_web_base_url" model="ir.config_parameter">
<field name="key">web.base.url</field>
<field name="value">http://localhost:8069</field>
</record>
<record id="view_menu" model="ir.ui.view">
<field name="name">ir.ui.menu.tree</field>
<field name="model">ir.ui.menu</field>

View File

@ -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)

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
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 []

View File

@ -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 = []

View File

@ -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.

View File

@ -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')

View File

@ -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":

View File

@ -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