diff --git a/bin/addons/base/base_update.xml b/bin/addons/base/base_update.xml
index 2475de60db4..8b1e633191f 100644
--- a/bin/addons/base/base_update.xml
+++ b/bin/addons/base/base_update.xml
@@ -226,22 +226,35 @@
-
+
-
- Next
- gtk-go-forward
-
+ action_addAdd Usergtk-add
+
+
+
+ action_next
+ Done
+ gtk-go-forward
+
@@ -268,18 +281,11 @@
-
+
-
+
-
+
@@ -314,7 +320,7 @@
- 5
+ 1
diff --git a/bin/addons/base/ir/ir.xml b/bin/addons/base/ir/ir.xml
index 31e2f8a9eb3..a3ef10f2c92 100644
--- a/bin/addons/base/ir/ir.xml
+++ b/bin/addons/base/ir/ir.xml
@@ -1342,7 +1342,7 @@
-
+
diff --git a/bin/addons/base/res/res_config.py b/bin/addons/base/res/res_config.py
index 204be08c9d0..2885a4059cf 100644
--- a/bin/addons/base/res/res_config.py
+++ b/bin/addons/base/res/res_config.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -15,11 +15,14 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
+from operator import attrgetter
+
from osv import osv, fields
+from tools.translate import _
import netsvc
import pooler
@@ -57,11 +60,31 @@ class res_config_configurable(osv.osv_memory):
'getting next %s' % todos)
active_todos = todos.search(cr, uid, [('state','=','open'),
('active','=',True)],
- limit=1, context=None)
+ limit=1)
if active_todos:
return todos.browse(cr, uid, active_todos[0], context=None)
return None
+ def _set_previous_todo(self, cr, uid, state):
+ """ lookup the previous (which is still the next at this point)
+ ir.actions.todo, set it to whatever state was provided.
+
+ Raises
+ `LookupError`: if we couldn't find *any* previous todo
+ `ValueError`: if no state is provided
+ anything ir_actions_todo.write can throw
+ """
+ # this is ultra brittle, but apart from storing the todo id
+ # into the res.config view, I'm not sure how to get the
+ # "previous" todo
+ previous_todo = self._next_action(cr, uid)
+ if not previous_todo:
+ raise LookupError(_("Couldn't find previous ir.actions.todo"))
+ if not state:
+ raise ValueError(_("Can't set an ir.actions.todo's state to "
+ "nothingness"))
+ previous_todo.write({'state':state})
+
def _next(self, cr, uid):
self.logger.notifyChannel('actions', netsvc.LOG_INFO,
'getting next operation')
@@ -69,9 +92,6 @@ class res_config_configurable(osv.osv_memory):
self.logger.notifyChannel('actions', netsvc.LOG_INFO,
'next action is %s' % next)
if next:
- self.pool.get('ir.actions.todo').write(cr, uid, next.id, {
- 'state':'done',
- }, context=None)
action = next.action_id
return {
'view_mode': action.view_mode,
@@ -90,42 +110,221 @@ class res_config_configurable(osv.osv_memory):
# return the action associated with the menu
return self.pool.get(current_user_menu.type)\
.read(cr, uid, current_user_menu.id)
+
def next(self, cr, uid, ids, context=None):
+ """ Returns the next todo action to execute (using the default
+ sort order)
+ """
return self._next(cr, uid)
def execute(self, cr, uid, ids, context=None):
+ """ Method called when the user clicks on the ``Next`` button.
+
+ Execute *must* be overloaded unless ``action_next`` is overloaded
+ (which is something you generally don't need to do).
+
+ If ``execute`` returns an action dictionary, that action is executed
+ rather than just going to the next configuration item.
+ """
raise NotImplementedError(
'Configuration items need to implement execute')
def cancel(self, cr, uid, ids, context=None):
+ """ Method called when the user click on the ``Skip`` button.
+
+ ``cancel`` should be overloaded instead of ``action_skip``. As with
+ ``execute``, if it returns an action dictionary that action is
+ executed in stead of the default (going to the next configuration item)
+
+ The default implementation is a NOOP.
+
+ ``cancel`` is also called by the default implementation of
+ ``action_cancel``.
+ """
pass
def action_next(self, cr, uid, ids, context=None):
+ """ Action handler for the ``next`` event.
+
+ Sets the status of the todo the event was sent from to
+ ``done``, calls ``execute`` and -- unless ``execute`` returned
+ an action dictionary -- executes the action provided by calling
+ ``next``.
+ """
+ self._set_previous_todo(cr, uid, state='done')
next = self.execute(cr, uid, ids, context=None)
if next: return next
return self.next(cr, uid, ids, context=context)
def action_skip(self, cr, uid, ids, context=None):
+ """ Action handler for the ``skip`` event.
+
+ Sets the status of the todo the event was sent from to
+ ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
+ an action dictionary -- executes the action provided by calling
+ ``next``.
+ """
+ self._set_previous_todo(cr, uid, state='skip')
+ next = self.cancel(cr, uid, ids, context=None)
+ if next: return next
+ return self.next(cr, uid, ids, context=context)
+
+ def action_cancel(self, cr, uid, ids, context=None):
+ """ Action handler for the ``cancel`` event. That event isn't
+ generated by the res.config.view.base inheritable view, the
+ inherited view has to overload one of the buttons (or add one
+ more).
+
+ Sets the status of the todo the event was sent from to
+ ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
+ an action dictionary -- executes the action provided by calling
+ ``next``.
+ """
+ self._set_previous_todo(cr, uid, state='cancel')
next = self.cancel(cr, uid, ids, context=None)
if next: return next
return self.next(cr, uid, ids, context=context)
res_config_configurable()
class res_config_installer(osv.osv_memory):
- ''' New-style configuration base specialized for modules selection
+ """ New-style configuration base specialized for addons selection
and installation.
- '''
+
+ Basic usage
+ -----------
+
+ Subclasses can simply define a number of _columns as
+ fields.boolean objects. The keys (column names) should be the
+ names of the addons to install (when selected). Upon action
+ execution, selected boolean fields (and those only) will be
+ interpreted as addons to install, and batch-installed.
+
+ Additional addons
+ -----------------
+
+ It is also possible to require the installation of an additional
+ addon set when a specific preset of addons has been marked for
+ installation (in the basic usage only, additionals can't depend on
+ one another).
+
+ These additionals are defined through the ``_install_if``
+ property. This property is a mapping of a collection of addons (by
+ name) to a collection of addons (by name) [#]_, and if all the *key*
+ addons are selected for installation, then the *value* ones will
+ be selected as well. For example::
+
+ _install_if = {
+ ('sale','crm'): ['sale_crm'],
+ }
+
+ This will install the ``sale_crm`` addon if and only if both the
+ ``sale`` and ``crm`` addons are selected for installation.
+
+ You can define as many additionals as you wish, and additionals
+ can overlap in key and value. For instance::
+
+ _install_if = {
+ ('sale','crm'): ['sale_crm'],
+ ('sale','project'): ['project_mrp'],
+ }
+
+ will install both ``sale_crm`` and ``project_mrp`` if all of
+ ``sale``, ``crm`` and ``project`` are selected for installation.
+
+ Hook methods
+ ------------
+
+ Subclasses might also need to express dependencies more complex
+ than that provided by additionals. In this case, it's possible to
+ define methods of the form ``_if_%(name)s`` where ``name`` is the
+ name of a boolean field. If the field is selected, then the
+ corresponding module will be marked for installation *and* the
+ hook method will be executed.
+
+ Hook methods take the usual set of parameters (cr, uid, ids,
+ context) and can return a collection of additional addons to
+ install (if they return anything, otherwise they should not return
+ anything, though returning any "falsy" value such as None or an
+ empty collection will have the same effect).
+
+ Complete control
+ ----------------
+
+ The last hook is to simply overload the ``modules_to_install``
+ method, which implements all the mechanisms above. This method
+ takes the usual set of parameters (cr, uid, ids, context) and
+ returns a ``set`` of addons to install (addons selected by the
+ above methods minus addons from the *basic* set which are already
+ installed) [#]_ so an overloader can simply manipulate the ``set``
+ returned by ``res_config_installer.modules_to_install`` to add or
+ remove addons.
+
+ Skipping the installer
+ ----------------------
+
+ Unless it is removed from the view, installers have a *skip*
+ button which invokes ``action_skip`` (and the ``cancel`` hook from
+ ``res.config``). Hooks and additionals *are not run* when skipping
+ installation, even for already installed addons.
+
+ Again, setup your hooks accordinly.
+
+ .. [#] note that since a mapping key needs to be hashable, it's
+ possible to use a tuple or a frozenset, but not a list or a
+ regular set
+
+ .. [#] because the already-installed modules are only pruned at
+ the very end of ``modules_to_install``, additionals and
+ hooks depending on them *are guaranteed to execute*. Setup
+ your hooks accordingly.
+ """
_name = 'res.config.installer'
_inherit = 'res.config'
_install_if = {}
- def _modules_to_install(self, cr, uid, ids, context=None):
+ def _already_installed(self, cr, uid, context=None):
+ """ For each module (boolean fields in a res.config.installer),
+ check if it's already installed (neither uninstallable nor uninstalled)
+ and if it is, check it by default
+ """
+ modules = self.pool.get('ir.module.module')
+
+ selectable = [field for field in self._columns
+ if type(self._columns[field]) is fields.boolean]
+ return modules.browse(
+ cr, uid,
+ modules.search(cr, uid,
+ [('name','in',selectable),
+ ('state','not in',['uninstallable', 'uninstalled'])],
+ context=context),
+ context=context)
+
+
+ def modules_to_install(self, cr, uid, ids, context=None):
+ """ selects all modules to install:
+
+ * checked boolean fields
+ * return values of hook methods. Hook methods are of the form
+ ``_if_%(addon_name)s``, and are called if the corresponding
+ addon is marked for installation. They take the arguments
+ cr, uid, ids and context, and return an iterable of addon
+ names
+ * additionals, additionals are setup through the ``_install_if``
+ class variable. ``_install_if`` is a dict of {iterable:iterable}
+ where key and value are iterables of addon names.
+
+ If all the addons in the key are selected for installation
+ (warning: addons added through hooks don't count), then the
+ addons in the value are added to the set of modules to install
+ * not already installed
+ """
base = set(module_name
for installer in self.read(cr, uid, ids, context=context)
for module_name, to_install in installer.iteritems()
if module_name != 'id'
if type(self._columns[module_name]) is fields.boolean
if to_install)
+
hooks_results = set()
for module in base:
hook = getattr(self, '_if_%s'%(module), None)
@@ -138,11 +337,40 @@ class res_config_installer(osv.osv_memory):
if base.issuperset(requirements)
for module in consequences)
- return base | hooks_results | additionals
+ return (base | hooks_results | additionals) - set(
+ map(attrgetter('name'), self._already_installed(cr, uid, context)))
+
+ def default_get(self, cr, uid, fields_list, context=None):
+ ''' If an addon is already installed, check it by default
+ '''
+ defaults = super(res_config_installer, self).default_get(
+ cr, uid, fields_list, context=context)
+
+ return dict(defaults,
+ **dict.fromkeys(
+ map(attrgetter('name'),
+ self._already_installed(cr, uid, context=context)),
+ True))
+
+ def fields_get(self, cr, uid, fields=None, context=None, read_access=True):
+ """ If an addon is already installed, set it to readonly as
+ res.config.installer doesn't handle uninstallations of already
+ installed addons
+ """
+ fields = super(res_config_installer, self).fields_get(
+ cr, uid, fields, context, read_access)
+
+ for module in self._already_installed(cr, uid, context=context):
+ fields[module.name].update(
+ readonly=True,
+ help=fields[module.name].get('help', '') +
+ _('\n\nThis addon is already installed on your system'))
+
+ return fields
def execute(self, cr, uid, ids, context=None):
modules = self.pool.get('ir.module.module')
- to_install = list(self._modules_to_install(
+ to_install = list(self.modules_to_install(
cr, uid, ids, context=context))
self.logger.notifyChannel(
'installer', netsvc.LOG_INFO,
diff --git a/bin/addons/base/res/res_config.xml b/bin/addons/base/res/res_config.xml
index 2b1d3b45d42..11d753018c2 100644
--- a/bin/addons/base/res/res_config.xml
+++ b/bin/addons/base/res/res_config.xml
@@ -47,5 +47,41 @@
+
+ Inheritable view for installer objects
+ res.config.installer
+ form
+
+
+
+
+
+
+
diff --git a/bin/addons/base/res/res_user.py b/bin/addons/base/res/res_user.py
index c459fbbe5a1..83b0827a98e 100644
--- a/bin/addons/base/res/res_user.py
+++ b/bin/addons/base/res/res_user.py
@@ -83,7 +83,6 @@ class groups(osv.osv):
groups()
-
class roles(osv.osv):
_name = "res.roles"
_columns = {
@@ -118,18 +117,66 @@ class users(osv.osv):
_uid_cache = {}
_name = "res.users"
+ WELCOME_MAIL_SUBJECT = u"Welcome to OpenERP"
+ WELCOME_MAIL_BODY = u"An OpenERP account has been created for you, "\
+ "\"%(name)s\".\n\nYour login is %(login)s, "\
+ "you should ask your supervisor or system administrator if you "\
+ "haven't been given your password yet.\n\n"\
+ "If you aren't %(name)s, this email reached you errorneously, "\
+ "please delete it."
+
+ def get_welcome_mail_subject(self, cr, uid, context=None):
+ """ Returns the subject of the mail new users receive (when
+ created via the res.config.users wizard), default implementation
+ is to return config_users.WELCOME_MAIL_SUBJECT
+ """
+ return self.WELCOME_MAIL_SUBJECT
+ def get_welcome_mail_body(self, cr, uid, context=None):
+ """ Returns the subject of the mail new users receive (when
+ created via the res.config.users wizard), default implementation
+ is to return config_users.WELCOME_MAIL_BODY
+ """
+ return self.WELCOME_MAIL_BODY
+
def get_current_company(self, cr, uid):
res=[]
cr.execute('select company_id, res_company.name from res_users left join res_company on res_company.id = company_id where res_users.id=%s' %uid)
res = cr.fetchall()
return res
+ def send_welcome_email(self, cr, uid, id, context=None):
+ logger= netsvc.Logger()
+ user = self.pool.get('res.users').read(cr, uid, id, context=context)
+ if not tools.config.get('smtp_server'):
+ logger.notifyChannel('mails', netsvc.LOG_WARNING,
+ _('"smtp_server" needs to be set to send mails to users'))
+ return False
+ if not tools.config.get('email_from'):
+ logger.notifyChannel("mails", netsvc.LOG_WARNING,
+ _('"email_from" needs to be set to send welcome mails '
+ 'to users'))
+ return False
+ if not user.get('email'):
+ return False
+
+ return tools.email_send(email_from=None, email_to=[user['email']],
+ subject=self.get_welcome_mail_subject(
+ cr, uid, context=context),
+ body=self.get_welcome_mail_body(
+ cr, uid, context=context) % user)
+
_columns = {
- 'name': fields.char('Name', size=64, required=True, select=True),
- 'login': fields.char('Login', size=64, required=True),
+ 'name': fields.char('Name', size=64, required=True, select=True,
+ help="The new user's real name, used for searching"
+ " and most listings"),
+ 'login': fields.char('Login', size=64, required=True,
+ help="Used to log into the system"),
'password': fields.char('Password', size=64, invisible=True, help="Keep empty if you don't want the user to be able to connect on the system."),
- 'email': fields.char('E-mail', size=64, help='If an email is provided'\
- ', the user will be sent a message welcoming him'),
+ 'email': fields.char('E-mail', size=64,
+ help='If an email is provided, the user will be sent a message '
+ 'welcoming him.\n\nWarning: if "email_from" and "smtp_server"'
+ " aren't configured, it won't be possible to email new "
+ "users."),
'signature': fields.text('Signature', size=64),
'address_id': fields.many2one('res.partner.address', 'Address'),
'active': fields.boolean('Active'),
@@ -138,10 +185,15 @@ class users(osv.osv):
'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
'roles_id': fields.many2many('res.roles', 'res_roles_users_rel', 'uid', 'rid', 'Roles'),
'rules_id': fields.many2many('ir.rule.group', 'user_rule_group_rel', 'user_id', 'rule_group_id', 'Rules'),
- 'company_id': fields.many2one('res.company', 'Company', help="The company this user is currently working on.", required=True),
+ 'company_id': fields.many2one('res.company', 'Company', required=True,
+ help="The company this user is currently working for."),
'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Accepted Companies'),
- 'context_lang': fields.selection(_lang_get, 'Language', required=True),
- 'context_tz': fields.selection(_tz_get, 'Timezone', size=64),
+ 'context_lang': fields.selection(_lang_get, 'Language', required=True,
+ help="Sets the language for the user's user interface, when UI "
+ "translations are available"),
+ 'context_tz': fields.selection(_tz_get, 'Timezone', size=64,
+ help="The user's timezone, used to perform timezone conversions "
+ "between the server and the client."),
}
def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
def override_password(o):
@@ -315,9 +367,14 @@ class config_users(osv.osv_memory):
}
def create_user(self, cr, uid, new_id, context=None):
- ''' create a new res.user instance from the data stored
- in the current res.config.users
- '''
+ """ create a new res.user instance from the data stored
+ in the current res.config.users.
+
+ If an email address was filled in for the user, sends a mail
+ composed of the return values of ``get_welcome_mail_subject``
+ and ``get_welcome_mail_body`` (which should be unicode values),
+ with the user's data %-formatted into the mail body
+ """
base_data = self.read(cr, uid, new_id, context=context)
partner_id = self.pool.get('res.partner').main_partner(cr, uid)
address = self.pool.get('res.partner.address').create(
@@ -331,10 +388,14 @@ class config_users(osv.osv_memory):
cr, base_data['name'], base_data['email'], context=context),
address_id=address,
)
- self.pool.get('res.users').create(
+ new_user = self.pool.get('res.users').create(
cr, uid, user_data, context)
-
+ self.send_welcome_email(cr, uid, new_user, context=context)
def execute(self, cr, uid, ids, context=None):
+ 'Do nothing on execution, just launch the next action/todo'
+ pass
+ def action_add(self, cr, uid, ids, context=None):
+ 'Create a user, and re-display the view'
self.create_user(cr, uid, ids[0], context=context)
return {
'view_type': 'form',
diff --git a/bin/addons/base/rng/view.rng b/bin/addons/base/rng/view.rng
index debf8764a4e..766b75bf9c3 100644
--- a/bin/addons/base/rng/view.rng
+++ b/bin/addons/base/rng/view.rng
@@ -97,6 +97,7 @@
+
diff --git a/bin/osv/orm.py b/bin/osv/orm.py
index 77b0362a5b1..95d0b5685f3 100644
--- a/bin/osv/orm.py
+++ b/bin/osv/orm.py
@@ -932,6 +932,17 @@ class orm_template(object):
self._invalids.clear()
def default_get(self, cr, uid, fields_list, context=None):
+ """ Set default values for the object's fields.
+
+ Returns a dict of {field_name:default_value}
+
+ Arguments:
+ `fields_list`: the fields for which the object doesn't have
+ any value yet, and default values need to be
+ provided. If fields outside this list are
+ returned, the user-provided values will be
+ overwritten.
+ """
return {}
def perm_read(self, cr, user, ids, context=None, details=True):
diff --git a/bin/tools/misc.py b/bin/tools/misc.py
index 2984fb09186..f68dda46b7a 100644
--- a/bin/tools/misc.py
+++ b/bin/tools/misc.py
@@ -395,13 +395,22 @@ def html2plaintext(html, body_id=None, encoding='utf-8'):
for i, url in enumerate(url_index):
if i == 0:
html += '\n\n'
- html += '[%s] %s\n' % (i+1, url)
+ html += '[%s] %s\n' % (i+1, url)
return html
def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'):
- """Send an email."""
+ """Send an email.
+
+ Arguments:
+
+ `email_from`: A string used to fill the `From` header, if falsy,
+ config['email_from'] is used instead. Also used for
+ the `Reply-To` header if `reply_to` is not provided
+
+ `email_to`: a sequence of addresses to send the mail to.
+ """
import smtplib
from email.MIMEText import MIMEText
from email.MIMEBase import MIMEBase
@@ -410,32 +419,30 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
from email.Utils import formatdate, COMMASPACE
from email.Utils import formatdate, COMMASPACE
from email import Encoders
- import netsvc
+ import netsvc
if x_headers is None:
x_headers = {}
- if not ssl:
- ssl = config.get('smtp_ssl', False)
+ if not ssl: ssl = config.get('smtp_ssl', False)
- if not email_from and not config['email_from']:
- raise Exception("No Email sender by default, see config file")
+ if not (email_from or config['email_from']):
+ raise ValueError("Sending an email requires either providing a sender "
+ "address or having configured one")
- if not email_from:
- email_from = config.get('email_from', False)
+ if not email_from: email_from = config.get('email_from', False)
- if not email_cc:
- email_cc = []
- if not email_bcc:
- email_bcc = []
+ if not email_cc: email_cc = []
+ if not email_bcc: email_bcc = []
+ if not body: body = u''
+ try: email_body = body.encode('utf-8')
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ email_body = body
- if not attach:
- try:
- msg = MIMEText(body.encode('utf8') or '',_subtype=subtype,_charset='utf-8')
- except:
- msg = MIMEText(body or '',_subtype=subtype,_charset='utf-8')
- else:
- msg = MIMEMultipart()
+ email_text = MIMEText(email_body, _subtype=subtype, _charset='utf-8')
+
+ if attach: msg = MIMEMultipart()
+ else: msg = email_text
msg['Subject'] = Header(ustr(subject), 'utf-8')
msg['From'] = email_from
@@ -455,21 +462,18 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
msg['X-Generated-By'] = 'OpenERP (http://www.openerp.com)'
msg['X-OpenERP-Server-Host'] = socket.gethostname()
msg['X-OpenERP-Server-Version'] = release.version
- if priority:
- msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
+
+ msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
# Add dynamic X Header
- for key, value in x_headers.items():
+ for key, value in x_headers.iteritems():
msg['X-OpenERP-%s' % key] = str(value)
if openobject_id:
msg['Message-Id'] = "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
if attach:
- try:
- msg.attach(MIMEText(body.encode('utf8') or '',_subtype=subtype,_charset='utf-8'))
- except:
- msg.attach(MIMEText(body or '', _charset='utf-8', _subtype=subtype) )
+ msg.attach(email_text)
for (fname,fcontent) in attach:
part = MIMEBase('application', "octet-stream")
part.set_payload( fcontent )