bzr revid: chs@tinyerp.com-20100205151012-yew32sugmd4vj48r
This commit is contained in:
Christophe Simonis
2010-02-05 16:10:12 +01:00
8 changed files with 418 additions and 71 deletions
+25 -19
View File
@@ -226,22 +226,35 @@
<field name="context_tz"/>
<separator string="Group" colspan="4"/>
<label align="0.0" colspan="4" string="
Groups are used to define access rights on objects and
the visibility of screens and menus"/>
<label align="0.0" colspan="4" string="Groups are used to define access rights on objects and the visibility of screens and menus"/>
<field colspan="4" nolabel="1" name="groups_id"/>
</group>
<xpath expr='//button[@name="action_skip"]'
position='attributes'>
<attribute name='string'>Next</attribute>
<attribute name='icon'>gtk-go-forward</attribute>
</xpath>
<xpath expr='//button[@name="action_next"]'
position='attributes'>
<attribute name="name">action_add</attribute>
<attribute name='string'>Add User</attribute>
<attribute name='icon'>gtk-add</attribute>
</xpath>
<xpath expr='//button[@name="action_skip"]'
position='attributes'>
<!-- if this one is performed first, it transforms
the action_skip into action_next, and the
transformation of action_next to action_add,
since it uses first-match, transforms the same
button. And we end up with [add] [next] instead
of [next] [add]
Would probably be simpler to just replace both
by nothing and create a pair of brand new
buttons... but we'd have to handle the groups
around the buttons... oh well...
-->
<attribute name="name">action_next</attribute>
<attribute name='string'>Done</attribute>
<attribute name='icon'>gtk-go-forward</attribute>
</xpath>
</data>
</field>
</record>
@@ -268,18 +281,11 @@
</form>
<group string="res_config_contents" position="replace">
<label colspan="4" align="0.0" string='
Choose between the simplified interface and the extended
one.'/>
<label colspan="4" align="0.0" string="Choose between the simplified interface and the extended one."/>
<newline/>
<label colspan="4" align="0.0" string='
If you are testing OpenERP or using it for the first time,
we suggest you use the simplified interface. It has less
options and fields but is easier to understand.'/>
<label colspan="4" align="0.0" string="If you are testing OpenERP or using it for the first time, we suggest you use the simplified interface. It has less options and fields but is easier to understand."/>
<newline/>
<label colspan="4" align="0.0" string='
You will be able to switch to the extended interface later.
'/>
<label colspan="4" align="0.0" string="You will be able to switch to the extended interface later."/>
<separator string="Choose Your Interface" colspan="4"/>
<field colspan="2" name="view" nolabel="1"/>
</group>
@@ -314,7 +320,7 @@
<record id="config_wizard_simple_view" model="ir.actions.todo">
<field name="action_id" ref="action_config_simple_view_form"/>
<field name="sequence">5</field>
<field name="sequence">1</field>
</record>
</data>
+1 -1
View File
@@ -1342,7 +1342,7 @@
<field name="sequence"/>
<field name="action_id" select="1"/>
<field name="state"/>
<field name="active" select="2"/>
<field name="active" select="2" invisible="True"/>
</tree>
</field>
</record>
+239 -11
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
#
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
#
@@ -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 <http://www.gnu.org/licenses/>.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
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,
+36
View File
@@ -47,5 +47,41 @@
</field>
</record>
<record id="res_config_installer" model="ir.ui.view">
<field name="name">Inheritable view for installer objects</field>
<field name="model">res.config.installer</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form>
<group colspan="4">
<separator string="title" colspan="4"/>
<group colspan="4" col="10">
<group colspan="2">
<image name="gtk-dialog-info"/>
<newline/>
<label align="0.0" string="description" width="200"/>
</group>
<group colspan="8"/>
</group>
</group>
<separator string="" colspan="4"/>
<group colspan="4" col="8">
<field name="progress" widget="progressbar"/>
<label string="" colspan="3"/>
<group colspan="1" col="2">
<button name="action_skip" icon="gtk-goto-last" special="cancel"
type="object" string="Skip"/>
</group>
<group colspan="1" col="2">
<button name="action_next" icon="gtk-go-forward"
type="object" string="Install Modules"/>
</group>
</group>
</form>
</field>
</record>
</data>
</openerp>
+74 -13
View File
@@ -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',
+1
View File
@@ -97,6 +97,7 @@
<rng:optional><rng:attribute name="position"/></rng:optional>
<rng:optional><rng:attribute name="fill"/></rng:optional>
<rng:optional><rng:attribute name="help"/></rng:optional>
<rng:optional><rng:attribute name="width"/></rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="notebook"/>
+11
View File
@@ -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):
+31 -27
View File
@@ -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 )