diff --git a/bin/addons/base/__terp__.py b/bin/addons/base/__terp__.py
index ae83f7ea164..d90d56a3a72 100644
--- a/bin/addons/base/__terp__.py
+++ b/bin/addons/base/__terp__.py
@@ -38,7 +38,7 @@
"init_xml" : [
"base_data.xml",
"base_menu.xml",
- "base_security.xml",
+ "security/base_security.xml",
"res/res_security.xml",
],
"demo_xml" : [
@@ -66,8 +66,8 @@
"res/partner/crm_view.xml",
"res/partner/partner_data.xml",
"res/ir_property_view.xml",
- "base_security.xml",
- "ir.model.access.csv",
+ "security/base_security.xml",
+ "security/ir.model.access.csv",
],
"active": True,
"installable": True,
diff --git a/bin/addons/base/ir/ir_ui_menu.py b/bin/addons/base/ir/ir_ui_menu.py
index a76b8ec1e76..555a5b4b7a1 100644
--- a/bin/addons/base/ir/ir_ui_menu.py
+++ b/bin/addons/base/ir/ir_ui_menu.py
@@ -66,16 +66,25 @@ class ir_ui_menu(osv.osv):
context=context)
if uid==1:
return ids
- user_groups = self.pool.get('res.users').read(cr, uid, [uid])[0]['groups_id']
+ user_groups = set(self.pool.get('res.users').read(cr, uid, uid)['groups_id'])
result = []
for menu in self.browse(cr, uid, ids):
- if not len(menu.groups_id):
- result.append(menu.id)
+ restrict_to_groups = menu.groups_id
+ if not restrict_to_groups:
+ if menu.action:
+ # if the menu itself has no restrictions, we get the groups of
+ # the action of the menu
+ try:
+ m, oid = menu.action.split(',', 1)
+ data = self.pool.get(m).read(cr, uid, int(oid), context=context)
+ if data and 'groups_id' in data:
+ restrict_to_groups = data['groups_id']
+ except:
+ pass
+
+ if restrict_to_groups and not user_groups.intersection(restrict_to_groups):
continue
- for g in menu.groups_id:
- if g.id in user_groups:
- result.append(menu.id)
- break
+ result.append(menu.id)
return result
def _get_full_name(self, cr, uid, ids, name, args, context):
@@ -122,8 +131,7 @@ class ir_ui_menu(osv.osv):
('key2', '=', 'tree_but_open'), ('res_id', 'in', ids)],
context=context)
values_action = {}
- for value in values_obj.browse(cursor, user, value_ids,
- context=context):
+ for value in values_obj.browse(cursor, user, value_ids, context=context):
values_action[value.res_id] = value.value
for menu_id in ids:
res[menu_id] = values_action.get(menu_id, False)
diff --git a/bin/addons/base/module/module.py b/bin/addons/base/module/module.py
index fa8ce19fd57..ca25c010ccc 100644
--- a/bin/addons/base/module/module.py
+++ b/bin/addons/base/module/module.py
@@ -268,7 +268,7 @@ class module(osv.osv):
raise orm.except_orm(_('Error'),
_('You try to remove a module that is installed or will be installed'))
return super(module, self).unlink(cr, uid, ids, context=context)
-
+
def state_update(self, cr, uid, ids, newstate, states_to_update, context={}, level=50):
if level<1:
raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
@@ -471,14 +471,14 @@ class module(osv.osv):
match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
version = '0'
if match:
- version = match.group(1)
+ version = match.group(1)
if vercmp(mod.installed_version or '0', version) >= 0:
continue
res.append(mod.url)
if not download:
continue
zipfile = urllib.urlopen(mod.url).read()
- fname = addons.get_module_path(mod.name+'.zip')
+ fname = addons.get_module_path(mod.name+'.zip')
try:
fp = file(fname, 'wb')
fp.write(zipfile)
@@ -528,12 +528,12 @@ class module(osv.osv):
self.write(cr, uid, [id], {'category_id': p_id})
def action_install(self,cr,uid,ids,context=None):
- self.write(cr , uid, ids ,{'state' : 'to install'})
+ self.write(cr , uid, ids ,{'state' : 'to install'})
self.download(cr, uid, ids, context=context)
for id in ids:
cr.execute("select m.id as id from ir_module_module_dependency d inner join ir_module_module m on (m.name=d.name) where d.module_id=%d and m.state='uninstalled'",(id,))
dep_ids = map(lambda x:x[0],cr.fetchall())
- if len(dep_ids):
+ if len(dep_ids):
self.action_install(cr,uid,dep_ids,context=context)
def update_translations(self, cr, uid, ids, filter_lang=None):
@@ -550,7 +550,7 @@ class module(osv.osv):
for mod in self.browse(cr, uid, ids):
if mod.state != 'installed':
continue
-
+
for lang in filter_lang:
f = os.path.join(tools.config['addons_path'], mod.name, 'i18n', lang + '.po')
if os.path.exists(f):
@@ -680,6 +680,18 @@ class module_configuration(osv.osv_memory):
'type': item.action_id.type,
'target':item.action_id.target,
}
+ user_action=self.pool.get('res.users').browse(cr,uid,uid)
+ act_obj=self.pool.get(user_action.menu_id.type)
+ action_ids=act_obj.search(cr,uid,[('name','=',user_action.menu_id.name)])
+ action_open=act_obj.browse(cr,uid,action_ids)[0]
+ if context.get('menu',False):
+ return{
+ 'view_type': action_open.view_type,
+ 'view_id':action_open.view_id and [action_open.view_id.id] or False,
+ 'res_model': action_open.res_model,
+ 'type': action_open.type,
+ 'domain':action_open.domain
+ }
return {'type':'ir.actions.act_window_close' }
module_configuration()
diff --git a/bin/addons/base/res/res_user.py b/bin/addons/base/res/res_user.py
index d168a69de38..35b008806e1 100644
--- a/bin/addons/base/res/res_user.py
+++ b/bin/addons/base/res/res_user.py
@@ -118,12 +118,18 @@ class users(osv.osv):
'context_tz': fields.selection(_tz_get, 'Timezone', size=64)
}
def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
+ def override_password(o):
+ if 'password' in o:
+ o['password'] = '********'
+ return o
+
result = super(users, self).read(cr, uid, ids, fields, context, load)
canwrite = self.pool.get('ir.model.access').check(cr, uid, 'res.users', 'write', raise_exception=False)
if not canwrite:
- for r in result:
- if 'password' in r:
- r['password'] = '********'
+ if isinstance(ids, (int, float)):
+ result = override_password(result)
+ else:
+ result = map(override_password, result)
return result
_sql_constraints = [
diff --git a/bin/addons/base/base_security.xml b/bin/addons/base/security/base_security.xml
similarity index 88%
rename from bin/addons/base/base_security.xml
rename to bin/addons/base/security/base_security.xml
index d109ae05e9c..c72f31418f3 100644
--- a/bin/addons/base/base_security.xml
+++ b/bin/addons/base/security/base_security.xml
@@ -41,6 +41,9 @@
+
diff --git a/bin/addons/base/ir.model.access.csv b/bin/addons/base/security/ir.model.access.csv
similarity index 100%
rename from bin/addons/base/ir.model.access.csv
rename to bin/addons/base/security/ir.model.access.csv
diff --git a/bin/osv/fields.py b/bin/osv/fields.py
index 825420b1ec6..daee0e4e40a 100644
--- a/bin/osv/fields.py
+++ b/bin/osv/fields.py
@@ -223,7 +223,7 @@ class binary(_column):
val = v[name]
break
res.setdefault(i, val)
- if context.get('get_binary_size', True):
+ if context.get('bin_size', False):
res[i] = tools.human_size(val)
return res
@@ -620,7 +620,7 @@ class function(_column):
else:
res = self._fnct(cr, obj._table, ids, name, self._arg, context)
- if self._type == 'binary' and context.get('get_binary_size', True):
+ if self._type == 'binary' and context.get('bin_size', False):
# convert the data returned by the function with the size of that data...
res = dict(map(lambda (x, y): (x, tools.human_size(len(y))), res.items()))
return res
@@ -648,8 +648,8 @@ class related(function):
# return True
- def _fnct_write(self,obj,cr, uid, ids, field_name, args, context=None):
- raise 'Not Implemented Yet'
+# def _fnct_write(self,obj,cr, uid, ids,values, field_name, args, context=None):
+# raise 'Not Implemented Yet'
def _fnct_read(self,obj,cr, uid, ids, field_name, args, context=None):
if not ids: return {}
@@ -678,7 +678,7 @@ class related(function):
def __init__(self,*arg,**args):
print arg
self.arg = arg
- super(related, self).__init__(self._fnct_read, arg, fnct_inv=self._fnct_write, fnct_inv_arg=arg,method=True, fnct_search=self._fnct_search,**args)
+ super(related, self).__init__(self._fnct_read, arg, fnct_inv_arg=arg,method=True, fnct_search=self._fnct_search,**args)
# TODO: call field_get on the object, not in the DB
def _field_get(self, cr, uid, obj, model_name, prop):
diff --git a/bin/osv/orm.py b/bin/osv/orm.py
index 00e2f30b35f..0fb4fdfcbf2 100644
--- a/bin/osv/orm.py
+++ b/bin/osv/orm.py
@@ -647,7 +647,7 @@ class orm_template(object):
if not fun(self, cr, uid, ids):
translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, source=msg) or msg
error_msgs.append(
- _("Error occur when validation the fields %s: %s") % (','.join(fields), translated_msg)
+ _("Error occured while validating the field(s) %s: %s") % (','.join(fields), translated_msg)
)
if error_msgs:
cr.rollback()
@@ -775,23 +775,15 @@ class orm_template(object):
if result:
node.setAttribute('string', result.decode('utf-8'))
if node.nodeType == node.ELEMENT_NODE and node.hasAttribute('groups'):
- groups = None
- group_str = node.getAttribute('groups')
- if ',' in group_str:
- groups = group_str.split(',')
+ if node.getAttribute('groups'):
+ groups = node.getAttribute('groups').split(',')
readonly = False
access_pool = self.pool.get('ir.model.access')
for group in groups:
- readonly = readonly and access_pool.check_groups(cr, user, group)
-
- if readonly:
- parent = node.parentNode
- parent.removeChild(node)
-
- else:
- if not self.pool.get('ir.model.access').check_groups(cr, user, group_str):
- parent = node.parentNode
- parent.removeChild(node)
+ readonly = readonly or access_pool.check_groups(cr, user, group)
+ if not readonly:
+ node.setAttribute('invisible', '1')
+ node.removeAttribute('groups')
if node.nodeType == node.ELEMENT_NODE:
# translate view
@@ -837,7 +829,6 @@ class orm_template(object):
button.setAttribute('readonly', '0')
arch = node.toxml(encoding="utf-8").replace('\t', '')
-
fields = self.fields_get(cr, user, fields_def.keys(), context)
for field in fields_def:
fields[field].update(fields_def[field])
@@ -1122,12 +1113,14 @@ class orm_memory(orm_template):
fields_to_read = self._columns.keys()
result = []
if self.datas:
+ if isinstance(ids, (int, long)):
+ ids = [ids]
for id in ids:
r = {'id': id}
for f in fields_to_read:
if id in self.datas:
r[f] = self.datas[id].get(f, False)
- if r[f] and isinstance(self._columns[f], fields.binary) and context.get('get_binary_size', True):
+ if r[f] and isinstance(self._columns[f], fields.binary) and context.get('bin_size', False):
r[f] = len(r[f])
result.append(r)
if id in self.datas:
@@ -1137,6 +1130,8 @@ class orm_memory(orm_template):
res2 = self._columns[f].get_memory(cr, self, ids, f, user, context=context, values=result)
for record in result:
record[f] = res2[record['id']]
+ if isinstance(ids, (int, long)):
+ return result[0]
return result
def write(self, cr, user, ids, vals, context=None):
@@ -1721,7 +1716,7 @@ class orm(orm_template):
def convert_field(f):
if f in ('create_date', 'write_date'):
return "date_trunc('second', %s) as %s" % (f, f)
- if isinstance(self._columns[f], fields.binary) and context.get('get_binary_size', True):
+ if isinstance(self._columns[f], fields.binary) and context.get('bin_size', False):
return "length(%s) as %s" % (f,f)
return '"%s"' % (f,)
#fields_pre2 = map(lambda x: (x in ('create_date', 'write_date')) and ('date_trunc(\'second\', '+x+') as '+x) or '"'+x+'"', fields_pre)
diff --git a/bin/report/interface.py b/bin/report/interface.py
index fafc52367b6..95e6ec31946 100644
--- a/bin/report/interface.py
+++ b/bin/report/interface.py
@@ -64,6 +64,7 @@ class report_int(netsvc.Service):
self.name = name
self.id = 0
self.name2 = '.'.join(name.split('.')[1:])
+ self.title = None
self.joinGroup('report')
self.exportMethod(self.create)
@@ -101,10 +102,10 @@ class report_rml(report_int):
ir_actions_report_xml_obj = pool.get('ir.actions.report.xml')
report_xml_ids = ir_actions_report_xml_obj.search(cr, uid,
[('report_name', '=', self.name[7:])], context=context)
- ftitle = ir_actions_report_xml_obj.browse(cr,uid,report_xml_ids)[0].name
+ self.title = ir_actions_report_xml_obj.browse(cr,uid,report_xml_ids)[0].name
report_type = datas.get('report_type', 'pdf')
create_doc = self.generators[report_type]
- pdf = create_doc(rml,title=ftitle)
+ pdf = create_doc(rml, title=self.title)
return (pdf, report_type)
def create_xml(self, cr, uid, ids, datas, context=None):
@@ -196,7 +197,7 @@ class report_rml(report_int):
result.freeDoc()
return xml
- def create_pdf(self, xml, logo=None,title=None):
+ def create_pdf(self, xml, logo=None, title=None):
if logo:
self.bin_datas['logo'] = logo
else:
@@ -206,15 +207,15 @@ class report_rml(report_int):
obj.render()
return obj.get()
- def create_html(self, xml, logo=None,title=None):
+ def create_html(self, xml, logo=None, title=None):
obj = render.rml2html(xml, self.bin_datas)
obj.render()
return obj.get()
- def create_raw(self, xml, logo=None,title=None):
+ def create_raw(self, xml, logo=None, title=None):
return xml
- def create_sxw(self, path, logo=None,title=None):
+ def create_sxw(self, path, logo=None, title=None):
return path
from report_sxw import report_sxw
diff --git a/bin/report/printscreen/ps_form.py b/bin/report/printscreen/ps_form.py
index 6e8b2de602f..1e7757e3f17 100644
--- a/bin/report/printscreen/ps_form.py
+++ b/bin/report/printscreen/ps_form.py
@@ -39,7 +39,7 @@ import libxml2
import libxslt
import time, os
-ftitle=""
+
class report_printscreen_list(report_int):
def __init__(self, name):
report_int.__init__(self, name)
@@ -63,7 +63,6 @@ class report_printscreen_list(report_int):
return self._parse_node(dom)
def create(self, cr, uid, ids, datas, context=None):
- global ftitle
if not context:
context={}
datas['ids'] = ids
@@ -71,10 +70,10 @@ class report_printscreen_list(report_int):
model_id = pool.get('ir.model').search(cr, uid, [('model','=',model._name)])
if model_id:
model_desc = pool.get('ir.model').browse(cr, uid, model_id, context).name
- ftitle=model_desc
+ self.title = model_desc
else:
model_desc = model._description
- ftitle=model_desc
+ self.title = model_desc
model = pool.get(datas['model'])
result = model.fields_view_get(cr, uid, view_type='tree', context=context)
@@ -158,8 +157,7 @@ class report_printscreen_list(report_int):
doc = libxml2.parseDoc(new_doc.toxml())
rml_obj = style.applyStylesheet(doc, None)
rml = style.saveResultToString(rml_obj)
- global ftitle
- self.obj = render.rml(rml,ftitle)
+ self.obj = render.rml(rml, self.title)
self.obj.render()
return True
report_printscreen_list('report.printscreen.form')
diff --git a/bin/report/printscreen/ps_list.py b/bin/report/printscreen/ps_list.py
index 4717fe03437..29242a3119e 100644
--- a/bin/report/printscreen/ps_list.py
+++ b/bin/report/printscreen/ps_list.py
@@ -39,7 +39,7 @@ import libxml2
import libxslt
import time, os
-ftitle=""
+
class report_printscreen_list(report_int):
def __init__(self, name):
report_int.__init__(self, name)
@@ -63,7 +63,6 @@ class report_printscreen_list(report_int):
return self._parse_node(dom)
def create(self, cr, uid, ids, datas, context=None):
- global ftitle
if not context:
context={}
pool = pooler.get_pool(cr.dbname)
@@ -71,10 +70,10 @@ class report_printscreen_list(report_int):
model_id = pool.get('ir.model').search(cr, uid, [('model','=',model._name)])
if model_id:
model_desc = pool.get('ir.model').browse(cr, uid, model_id[0], context).name
- ftitle=model_desc
+ self.title = model_desc
else:
model_desc = model._description
- ftitle=model_desc
+ self.title = model_desc
datas['ids'] = ids
model = pooler.get_pool(cr.dbname).get(datas['model'])
@@ -202,8 +201,7 @@ class report_printscreen_list(report_int):
doc = libxml2.parseDoc(new_doc.toxml())
rml_obj = style.applyStylesheet(doc, None)
rml = style.saveResultToString(rml_obj)
- global ftitle
- self.obj = render.rml(rml,title=ftitle)
+ self.obj = render.rml(rml, title=self.title)
self.obj.render()
return True
report_printscreen_list('report.printscreen.list')
diff --git a/bin/report/render/rml2pdf/trml2pdf.py b/bin/report/render/rml2pdf/trml2pdf.py
index aeee1220d08..e0cc3cc8220 100644
--- a/bin/report/render/rml2pdf/trml2pdf.py
+++ b/bin/report/render/rml2pdf/trml2pdf.py
@@ -60,7 +60,7 @@ from reportlab import platypus
import utils
import color
import os
-ftitle=""
+
#
# Change this to UTF-8 if you plan tu use Reportlab's UTF-8 support
#
@@ -180,11 +180,12 @@ class _rml_styles(object):
return style
class _rml_doc(object):
- def __init__(self, data, images={}, path='.'):
+ def __init__(self, data, images={}, path='.', title=None):
self.dom = xml.dom.minidom.parseString(data)
self.filename = self.dom.documentElement.getAttribute('filename')
self.images = images
self.path = path
+ self.title = title
def docinit(self, els):
from reportlab.lib.fonts import addMapping
@@ -229,28 +230,28 @@ class _rml_doc(object):
el = self.dom.documentElement.getElementsByTagName('template')
if len(el):
- pt_obj = _rml_template(out, el[0], self, images=self.images, path=self.path)
+ pt_obj = _rml_template(out, el[0], self, images=self.images, path=self.path, title=self.title)
pt_obj.render(self.dom.documentElement.getElementsByTagName('story'))
else:
self.canvas = canvas.Canvas(out)
pd = self.dom.documentElement.getElementsByTagName('pageDrawing')[0]
- pd_obj = _rml_canvas(self.canvas, None, self, self.images, path=self.path)
+ pd_obj = _rml_canvas(self.canvas, None, self, self.images, path=self.path, title=self.title)
pd_obj.render(pd)
self.canvas.showPage()
self.canvas.save()
class _rml_canvas(object):
- def __init__(self, canvas, doc_tmpl=None, doc=None, images={}, path='.'):
+ def __init__(self, canvas, doc_tmpl=None, doc=None, images={}, path='.', title=None):
self.canvas = canvas
self.styles = doc.styles
self.doc_tmpl = doc_tmpl
self.doc = doc
self.images = images
self.path = path
- global ftitle
- if ftitle:
- self.canvas.setTitle(ftitle)
+ self.title = title
+ if self.title:
+ self.canvas.setTitle(self.title)
def _textual(self, node, x=0, y=0):
rc = ''
@@ -324,7 +325,7 @@ class _rml_canvas(object):
self.canvas.circle(x_cen=utils.unit_get(node.getAttribute('x')), y_cen=utils.unit_get(node.getAttribute('y')), r=utils.unit_get(node.getAttribute('radius')), **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
def _place(self, node):
- flows = _rml_flowable(self.doc, images=self.images, path=self.path).render(node)
+ flows = _rml_flowable(self.doc, images=self.images, path=self.path, title=self.title).render(node)
infos = utils.attr_get(node, ['x','y','width','height'])
infos['y']+=infos['height']
@@ -450,25 +451,27 @@ class _rml_canvas(object):
break
class _rml_draw(object):
- def __init__(self, node, styles, images={}, path='.'):
+ def __init__(self, node, styles, images={}, path='.', title=None):
self.node = node
self.styles = styles
self.canvas = None
self.images = images
self.path = path
+ self.canvas_title = title
def render(self, canvas, doc):
canvas.saveState()
- cnv = _rml_canvas(canvas, doc, self.styles, images=self.images, path=self.path)
+ cnv = _rml_canvas(canvas, doc, self.styles, images=self.images, path=self.path, title=self.canvas_title)
cnv.render(self.node)
canvas.restoreState()
class _rml_flowable(object):
- def __init__(self, doc, images={}, path='.'):
+ def __init__(self, doc, images={}, path='.', title=None):
self.doc = doc
self.styles = doc.styles
self.images = images
self.path = path
+ self.title = title
def _textual(self, node):
rc = ''
@@ -569,7 +572,7 @@ class _rml_flowable(object):
return (self.width, self.height)
def draw(self):
canvas = self.canv
- drw = _rml_draw(self.node, self.styles, images=self.self2.images, path=self.self2.path)
+ drw = _rml_draw(self.node, self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
drw.render(self.canv, None)
return Illustration(node, self.styles, self)
@@ -741,9 +744,10 @@ class TinyDocTemplate(platypus.BaseDocTemplate):
self.canv._pageNumber = 0
class _rml_template(object):
- def __init__(self, out, node, doc, images={}, path='.'):
+ def __init__(self, out, node, doc, images={}, path='.', title=None):
self.images= images
self.path = path
+ self.title = title
if not node.hasAttribute('pageSize'):
pageSize = (utils.unit_get('21cm'), utils.unit_get('29.7cm'))
else:
@@ -764,7 +768,7 @@ class _rml_template(object):
frames.append( frame )
gr = pt.getElementsByTagName('pageGraphics')
if len(gr):
- drw = _rml_draw(gr[0], self.doc, images=images, path=self.path)
+ drw = _rml_draw(gr[0], self.doc, images=images, path=self.path, title=self.title)
self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
else:
self.page_templates.append( platypus.PageTemplate(frames=frames, **utils.attr_get(pt, [], {'id':'str'}) ))
@@ -772,7 +776,7 @@ class _rml_template(object):
def render(self, node_stories):
fis = []
- r = _rml_flowable(self.doc,images=self.images, path=self.path)
+ r = _rml_flowable(self.doc,images=self.images, path=self.path, title=self.title)
for node_story in node_stories:
fis += r.render(node_story)
if node_story==node_stories[-1]:
@@ -784,9 +788,7 @@ class _rml_template(object):
self.doc_tmpl.build(fis)
def parseString(data, fout=None, images={}, path='.',title=None):
- r = _rml_doc(data, images, path)
- global ftitle
- ftitle=title
+ r = _rml_doc(data, images, path, title=title)
if fout:
fp = file(fout,'wb')
r.render(fp)
diff --git a/bin/report/report_sxw.py b/bin/report/report_sxw.py
index 5b9a78c630a..a5f7ee8d7ef 100644
--- a/bin/report/report_sxw.py
+++ b/bin/report/report_sxw.py
@@ -566,7 +566,6 @@ class report_sxw(report_rml):
if not context:
context={}
context = context.copy()
- context['get_binary_size'] = False
pool = pooler.get_pool(cr.dbname)
ir_actions_report_xml_obj = pool.get('ir.actions.report.xml')
diff --git a/bin/tools/misc.py b/bin/tools/misc.py
index 0034a1e0bce..f29f1731ddb 100644
--- a/bin/tools/misc.py
+++ b/bin/tools/misc.py
@@ -327,7 +327,7 @@ def reverse_enumerate(l):
#----------------------------------------------------------
# Emails
#----------------------------------------------------------
-def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, on_error=False, reply_to=False, tinycrm=False):
+def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, on_error=False, reply_to=False, tinycrm=False, ssl=False, debug=False):
"""Send an email."""
if not email_cc:
email_cc=[]