diff --git a/addons/import_sugarcrm/import_sugarcrm.py b/addons/import_sugarcrm/import_sugarcrm.py
index fa89ad1949e..42ba4382b27 100644
--- a/addons/import_sugarcrm/import_sugarcrm.py
+++ b/addons/import_sugarcrm/import_sugarcrm.py
@@ -949,7 +949,7 @@ class import_sugarcrm(osv.osv):
'user' : fields.boolean('User', help="Check this box to import sugarCRM Users into OpenERP users, warning if a user with the same login exist in OpenERP, user information will be erase by sugarCRM user information", readonly=True),
'opportunity': fields.boolean('Leads & Opp', help="Check this box to import sugarCRM Leads and Opportunities into OpenERP Leads and Opportunities"),
'contact': fields.boolean('Contacts', help="Check this box to import sugarCRM Contacts into OpenERP addresses"),
- 'account': fields.boolean('Accounts', help="Check this box to import sugarCRM Accounts into OpenERP partners"),
+ 'account': fields.boolean('Partner/Account', help="Check this box to import sugarCRM Accounts into OpenERP partners"),
'employee': fields.boolean('Employee', help="Check this box to import sugarCRM Employees into OpenERP employees"),
'meeting': fields.boolean('Meetings', help="Check this box to import sugarCRM Meetings and Tasks into OpenERP meetings"),
'call': fields.boolean('Calls', help="Check this box to import sugarCRM Calls into OpenERP calls"),
diff --git a/addons/import_sugarcrm/import_sugarcrm_view.xml b/addons/import_sugarcrm/import_sugarcrm_view.xml
index 8dfc984c6c0..7188108ba6c 100644
--- a/addons/import_sugarcrm/import_sugarcrm_view.xml
+++ b/addons/import_sugarcrm/import_sugarcrm_view.xml
@@ -9,76 +9,58 @@
form
diff --git a/addons/l10n_be_hr_payroll/l10n_be_hr_payroll_view.xml b/addons/l10n_be_hr_payroll/l10n_be_hr_payroll_view.xml
index 46caa9f6478..79c7c08ffca 100644
--- a/addons/l10n_be_hr_payroll/l10n_be_hr_payroll_view.xml
+++ b/addons/l10n_be_hr_payroll/l10n_be_hr_payroll_view.xml
@@ -9,23 +9,17 @@
-
+
+
+
-
-
-
-
-
-
-
-
-
+
diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py
index d9835f86fc5..805c27386f3 100644
--- a/addons/mail/__init__.py
+++ b/addons/mail/__init__.py
@@ -23,6 +23,7 @@ import mail_message
import mail_thread
import mail_group
import mail_subscription
+import ir_needaction
import res_users
import res_partner
import report
diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py
index 95ce7d5619d..f501cc58d2c 100644
--- a/addons/mail/__openerp__.py
+++ b/addons/mail/__openerp__.py
@@ -86,6 +86,7 @@ The main features are:
'static/src/css/mail_group.css',
],
'js': [
+ 'static/lib/jquery.expander/jquery.expander.js',
'static/src/js/mail.js',
],
'qweb': [
diff --git a/addons/mail/doc/index.rst.inc b/addons/mail/doc/index.rst.inc
index 822d855d5b4..c4b87cc18f9 100644
--- a/addons/mail/doc/index.rst.inc
+++ b/addons/mail/doc/index.rst.inc
@@ -10,3 +10,4 @@ Mail Module documentation topics
mail_openchatter_howto
mail_needaction_howto
mail_partner
+ mail_state
diff --git a/addons/mail/doc/mail_openchatter_howto.rst b/addons/mail/doc/mail_openchatter_howto.rst
index 7ca52348d99..4c8797e7f8b 100644
--- a/addons/mail/doc/mail_openchatter_howto.rst
+++ b/addons/mail/doc/mail_openchatter_howto.rst
@@ -38,7 +38,7 @@ Make your module inheriting from the ``mail.thread`` class.
# inherit from mail.thread allows the use of OpenChatter
_inherit = ['mail.thread']
-Use the thread viewer widget inside your form view by using the ThreadView widget on the message_ids field inherited from mail.thread.
+Use the thread viewer widget inside your form view by using the mail_thread widget on the message_ids field inherited from mail.thread.
::
@@ -50,7 +50,7 @@ Use the thread viewer widget inside your form view by using the ThreadView widge
[...]
-
+
@@ -131,7 +131,7 @@ The drawback of this method is that it is no longer possible to those subscriber
Messages display management
++++++++++++++++++++++++++++
-By default, the ThreadView widget shows all messages related to the current document beside the document, in the History and comments section. However, you may want to display other messages in the widget. For example, the OpenChatter on res.users model shows
+By default, the mail_thread widget shows all messages related to the current document beside the document, in the History and comments section. However, you may want to display other messages in the widget. For example, the OpenChatter on res.users model shows
- messages related to the user, as usual (messages with ``model = res.users, res_id = current_document_id``)
- messages directly pushed to this user (containing @login)
diff --git a/addons/mail/doc/mail_state.rst b/addons/mail/doc/mail_state.rst
new file mode 100644
index 00000000000..527a31ba835
--- /dev/null
+++ b/addons/mail/doc/mail_state.rst
@@ -0,0 +1,28 @@
+.. _mail_state:
+
+message_state
+=============
+
+``message_state`` is a boolean field that states whether the document
+has unread messages. In previous versions, some documents were going
+back to ``pending`` state when receiving an email through the mail
+gateway. Now the state related to messages differs from the state or
+stage of the document itself.
+
+message_state and need action mechanism
++++++++++++++++++++++++++++++++++++++++
+
+The ``mail`` module introduces a default behavior for the need_action
+mechanism [REF].
+
+::
+
+ def get_needaction_user_ids(self, cr, uid, ids, context=None):
+ """ Returns the user_ids that have to perform an action
+ :return: dict { record_id: [user_ids], }
+ """
+ result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context)
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.message_state == False and obj.user_id:
+ result[obj.id].append(obj.user_id.id)
+ return result
diff --git a/addons/mail/ir_needaction.py b/addons/mail/ir_needaction.py
new file mode 100644
index 00000000000..8193ac4f385
--- /dev/null
+++ b/addons/mail/ir_needaction.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2012-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# 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
+#
+##############################################################################
+
+from osv import osv
+
+class ir_needaction_mixin(osv.Model):
+ """ Update of ir.needaction_mixin class
+ - override the get_needaction_user_ids method to define the default
+ mail gateway need_action: when the object is unread, the object
+ responsible has an action to perform.
+ """
+ _name = 'ir.needaction_mixin'
+ _inherit = ['ir.needaction_mixin']
+
+ def get_needaction_user_ids(self, cr, uid, ids, context=None):
+ """ Returns the user_ids that have to perform an action. It the
+ document mail state is unread (False), return object.user_id.id
+ as need_action uid.
+ :return: dict { record_id: [user_ids], }
+ """
+ result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context)
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.message_state == False and obj.user_id:
+ result[obj.id].append(obj.user_id.id)
+ return result
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index 0a8677d7862..f691d75e0bc 100644
--- a/addons/mail/mail_group.py
+++ b/addons/mail/mail_group.py
@@ -87,7 +87,7 @@ class mail_group(osv.osv):
result = dict.fromkeys(ids)
for id in ids:
result[id] = {}
- result[id]['member_ids'] = self.message_get_subscribers_ids(cr, uid, [id], context=context)
+ result[id]['member_ids'] = self.message_get_subscribers(cr, uid, [id], context=context)
result[id]['member_count'] = len(result[id]['member_ids'])
result[id]['is_subscriber'] = uid in result[id]['member_ids']
return result
diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml
index 921c3b528c4..6ad1792b3b4 100644
--- a/addons/mail/mail_group_view.xml
+++ b/addons/mail/mail_group_view.xml
@@ -51,28 +51,30 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 1c1e980e3ed..301657ba4c2 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -96,11 +96,24 @@ class mail_message_common(osv.osv_memory):
continue
result[message.id] = self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1]
return result
-
+
+ def name_get(self, cr, uid, ids, context=None):
+ res = []
+ for message in self.browse(cr, uid, ids, context=context):
+ name = ''
+ if message.subject:
+ name = '%s: ' % (message.subject)
+ if message.body_text:
+ name = '%s%s ' % (name, message.body_text[0:20])
+ if message.date:
+ name = '%s(%s)' % (name, message.date)
+ res.append((message.id, name))
+ return res
+
_name = 'mail.message.common'
_rec_name = 'subject'
_columns = {
- 'subject': fields.char('Subject', size=512, required=True),
+ 'subject': fields.char('Subject', size=512),
'model': fields.char('Related Document Model', size=128, select=1),
'res_id': fields.integer('Related Document ID', select=1),
'record_name': fields.function(get_record_name, type='string', string='Message Record Name',
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index e0f2bfaf455..5668709af17 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -34,7 +34,7 @@ import xmlrpclib
_logger = logging.getLogger(__name__)
-class mail_thread(osv.osv):
+class mail_thread(osv.Model):
'''Mixin model, meant to be inherited by any model that needs to
act as a discussion topic on which messages can be attached.
Public methods are prefixed with ``message_`` in order to avoid
@@ -62,10 +62,15 @@ class mail_thread(osv.osv):
_name = 'mail.thread'
_description = 'Email Thread'
- def _get_message_ids(self, cr, uid, ids, name, arg, context=None):
+ def _get_message_ids(self, cr, uid, ids, name, args, context=None):
res = {}
for id in ids:
- res[id] = self.message_load_ids(cr, uid, [id], context=context)
+ message_ids = self.message_load_ids(cr, uid, [id], context=context)
+ subscriber_ids = self.message_get_subscribers(cr, uid, [id], context=context)
+ res[id] = {
+ 'message_ids': message_ids,
+ 'message_summary': "Msg: %d . Fol: %d" % (len(message_ids), len(subscriber_ids)),
+ }
return res
def _search_message_ids(self, cr, uid, obj, name, args, context=None):
@@ -73,10 +78,23 @@ class mail_thread(osv.osv):
msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context)
return [('id', 'in', msg_ids)]
- # OpenChatter: message_ids is a dummy field that should not be used
_columns = {
- 'message_ids': fields.function(_get_message_ids, method=True, fnct_search=_search_message_ids,
- type='one2many', obj='mail.message', string='Temp messages', _fields_id = 'res_id'),
+ 'message_ids': fields.function(_get_message_ids, method=True,
+ fnct_search=_search_message_ids,
+ type='one2many', obj='mail.message', _fields_id = 'res_id',
+ string='Temp messages', multi="_get_message_ids",
+ help="Functional field holding messages related to the current document."),
+ 'message_state': fields.boolean('Read',
+ help="When checked, new messages require your attention."),
+ 'message_summary': fields.function(_get_message_ids, method=True,
+ type='text', string='Summary', multi="_get_message_ids",
+ help="Holds the Chatter summary (number of messages, ...). "\
+ "This summary is directly in html format in order to "\
+ "be inserted in kanban views."),
+ }
+
+ _defaults = {
+ 'message_state': True,
}
#------------------------------------------------------
@@ -84,10 +102,11 @@ class mail_thread(osv.osv):
#------------------------------------------------------
def create(self, cr, uid, vals, context=None):
- """Automatically subscribe the creator"""
- thread_id = super(mail_thread, self).create(cr, uid, vals, context=context);
- self.message_subscribe(cr, uid, [thread_id], [uid], context=context)
- return thread_id;
+ """Automatically subscribe the creator """
+ thread_id = super(mail_thread, self).create(cr, uid, vals, context=context)
+ if thread_id:
+ self.message_subscribe(cr, uid, [thread_id], [uid], context=context)
+ return thread_id
def write(self, cr, uid, ids, vals, context=None):
"""Automatically subscribe the writer"""
@@ -106,8 +125,6 @@ class mail_thread(osv.osv):
a foreign key with a 'cascade' ondelete attribute.
Notifications will be deleted with messages
"""
- if context is None:
- context = {}
subscr_obj = self.pool.get('mail.subscription')
msg_obj = self.pool.get('mail.message')
# delete subscriptions
@@ -120,17 +137,18 @@ class mail_thread(osv.osv):
return super(mail_thread, self).unlink(cr, uid, ids, context=context)
#------------------------------------------------------
- # Generic message api
+ # mail.message wrappers and tools
#------------------------------------------------------
def message_create(self, cr, uid, thread_id, vals, context=None):
- """OpenSocial: wrapper of mail.message create method
+ """ OpenChatter: wrapper of mail.message create method
- creates the mail.message
- automatically subscribe the message writer
- push the message to subscribed users
"""
if context is None:
context = {}
+
message_obj = self.pool.get('mail.message')
subscription_obj = self.pool.get('mail.subscription')
notification_obj = self.pool.get('mail.notification')
@@ -167,7 +185,10 @@ class mail_thread(osv.osv):
# create message
msg_id = message_obj.create(cr, uid, vals, context=context)
-
+
+ # Set as unread if writer is not the document responsible
+ self.message_create_set_unread(cr, uid, [thread_id], context=context)
+
# special: if install mode, do not push demo data
if context.get('install_mode', False):
return True
@@ -186,7 +207,7 @@ class mail_thread(osv.osv):
body = new_msg_vals.get('body_html', '') if new_msg_vals.get('subtype', 'plain') == 'html' else new_msg_vals.get('body_text', '')
for thread_id in thread_ids:
# add subscribers
- notif_user_ids += [user['id'] for user in self.message_get_subscribers(cr, uid, [thread_id], context=context)]
+ notif_user_ids += self.message_get_subscribers(cr, uid, [thread_id], context=context)
# add users requested via parsing message (@login)
notif_user_ids += self.message_parse_users(cr, uid, [thread_id], body, context=context)
# add users requested to perform an action (need_action mechanism)
@@ -214,6 +235,10 @@ class mail_thread(osv.osv):
user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context)
return user_ids
+ #------------------------------------------------------
+ # Generic message api
+ #------------------------------------------------------
+
def message_capable_models(self, cr, uid, context=None):
ret_dict = {}
for model_name in self.pool.obj_list():
@@ -376,7 +401,6 @@ class mail_thread(osv.osv):
original = msg_dict.get('original'),
context = context)
- # Message loading
def _message_add_ancestor_ids(self, cr, uid, ids, child_ids, root_ids, context=None):
""" Given message child_ids
Find their ancestors until root ids"""
@@ -420,6 +444,9 @@ class mail_thread(osv.osv):
msg_ids = self.message_load_ids(cr, uid, ids, limit, offset, domain, ascent, root_ids, context=context)
msgs = self.pool.get('mail.message').read(cr, uid, msg_ids, [], context=context)
+ # Set as read
+ self.message_check_and_set_read(cr, uid, ids, context=context)
+
""" Retrieve all attachments names """
map_id_to_name = {}
@@ -474,7 +501,7 @@ class mail_thread(osv.osv):
return msgs
#------------------------------------------------------
- # Email specific
+ # Mail gateway
#------------------------------------------------------
# message_process will call either message_new or message_update.
@@ -549,7 +576,7 @@ class mail_thread(osv.osv):
if res_id:
res_id = res_id.group(1)
if res_id:
- res_id = int(res_id)
+ res_id = res_id
if model_pool.exists(cr, uid, res_id):
if hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
@@ -558,7 +585,7 @@ class mail_thread(osv.osv):
res_id = False
if not res_id:
res_id = create_record(msg)
- #To forward the email to other followers
+ # To forward the email to other followers
self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
return res_id
@@ -594,29 +621,34 @@ class mail_thread(osv.osv):
fields = model_pool.fields_get(cr, uid, context=context)
data = model_pool.default_get(cr, uid, fields, context=context)
if 'name' in fields and not data.get('name'):
- data['name'] = msg_dict.get('from','')
+ data['name'] = msg_dict.get('from', '')
if custom_values and isinstance(custom_values, dict):
data.update(custom_values)
res_id = model_pool.create(cr, uid, data, context=context)
self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
return res_id
- def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None):
- """Called by ``message_process`` when a new message is received
- for an existing thread. The default behavior is to create a
- new mail.message in the given thread (by calling
- ``message_append_dict``)
- Additional behavior may be implemented by overriding this
- method.
+ def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
+ """ Called by ``message_process`` when a new message is received
+ for an existing thread. The default behavior is to create a
+ new mail.message in the given thread (by calling
+ ``message_append_dict``)
+ Additional behavior may be implemented by overriding this
+ method.
- :param dict msg_dict: a map containing the email details and
+ :param dict msg_dict: a map containing the email details and
attachments. See ``message_process`` and
``mail.message.parse()`` for details.
- :param dict context: if a ``thread_model`` value is present
+ :param dict vals: a dict containing values to update records
+ given their ids; if the dict is None or is
+ void, no write operation is performed.
+ :param dict context: if a ``thread_model`` value is present
in the context, its value will be used
to determine the model of the thread to
update (instead of the current model).
"""
+ if update_vals:
+ self.write(cr, uid, ids, update_vals, context=context)
return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
def message_thread_followers(self, cr, uid, ids, context=None):
@@ -746,13 +778,8 @@ class mail_thread(osv.osv):
self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
- if subject is None:
- if type == 'notification':
- subject = _('System notification')
- elif type == 'comment' and not parent_id:
- subject = _('Comment')
- elif type == 'comment' and parent_id:
- subject = _('Reply')
+ if type in ['notification', 'reply']:
+ subject = None
if subtype == 'html':
body_html = body
body_text = body
@@ -765,51 +792,77 @@ class mail_thread(osv.osv):
# Subscription mechanism
#------------------------------------------------------
- def message_get_subscribers_ids(self, cr, uid, ids, context=None):
+ def message_get_subscribers(self, cr, uid, ids, context=None):
+ """ Returns the current document followers. Basically this method
+ checks in mail.subscription for entries with matching res_model,
+ res_id.
+
+ :param get_ids: if set to True, return the ids of users; if set
+ to False, returns the result of a read in res.users
+ """
subscr_obj = self.pool.get('mail.subscription')
subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
- subs = subscr_obj.read(cr, uid, subscr_ids, context=context)
- return [sub['user_id'][0] for sub in subs]
+ return [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)]
- def message_get_subscribers(self, cr, uid, ids, context=None):
- user_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
- users = self.pool.get('res.users').read(cr, uid, user_ids, fields=['id', 'name', 'avatar'], context=context)
- return users
+ def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'avatar'], context=None):
+ """ Returns the current document followers as a read result. Used
+ mainly for Chatter having only one method to call to have
+ details about users.
+ """
+ user_ids = self.message_get_subscribers(cr, uid, ids, context=context)
+ return self.pool.get('res.users').read(cr, uid, user_ids, fields=fields, context=context)
def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
- users = self.message_get_subscribers(cr, uid, ids, context=context)
+ """ Check if uid or user_id (if set) is a subscriber to the current
+ document.
+
+ :param user_id: if set, check is done on user_id; if not set
+ check is done on uid
+ """
sub_user_id = uid if user_id is None else user_id
- if sub_user_id in [user['id'] for user in users]:
+ if sub_user_id in self.message_get_subscribers(cr, uid, ids, context=context):
return True
return False
def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
+ """ Subscribe the user (or user_ids) to the current document.
+
+ :param user_ids: a list of user_ids; if not set, subscribe
+ uid instead
+ """
subscription_obj = self.pool.get('mail.subscription')
to_subscribe_uids = [uid] if user_ids is None else user_ids
create_ids = []
for id in ids:
+ already_subscribed_user_ids = self.message_get_subscribers(cr, uid, [id], context=context)
for user_id in to_subscribe_uids:
- if self.message_is_subscriber(cr, uid, [id], user_id=user_id, context=context): continue
+ if user_id in already_subscribed_user_ids: continue
create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
return create_ids
def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
- if not user_ids and not uid in self.message_get_subscribers_ids(cr, uid, ids, context=context):
+ """ Unsubscribe the user (or user_ids) from the current document.
+
+ :param user_ids: a list of user_ids; if not set, subscribe
+ uid instead
+ """
+ # Trying to unsubscribe somebody not in subscribers: returns False
+ # if special management is needed; allows to know that an automatically
+ # subscribed user tries to unsubscribe and allows to warn him
+ mail_thread_model = self.pool.get('mail.thread')
+ if not user_ids and not uid in mail_thread_model.message_get_subscribers(cr, uid, ids, context=context):
return False
subscription_obj = self.pool.get('mail.subscription')
to_unsubscribe_uids = [uid] if user_ids is None else user_ids
to_delete_sub_ids = subscription_obj.search(cr, uid,
['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
- subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
- return True
+ return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
#------------------------------------------------------
# Notification API
#------------------------------------------------------
def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
- if context is None:
- context = {}
notif_obj = self.pool.get('mail.notification')
msg_obj = self.pool.get('mail.message')
if remove_childs:
@@ -819,4 +872,37 @@ class mail_thread(osv.osv):
to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
+ #------------------------------------------------------
+ # Thread_state
+ #------------------------------------------------------
+
+ def message_create_set_unread(self, cr, uid, ids, context=None):
+ """ When creating a new message, set as unread if uid is not the
+ object responsible. """
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id != uid):
+ self.message_mark_as_unread(cr, uid, [obj.id], context=context)
+
+ def message_check_and_set_unread(self, cr, uid, ids, context=None):
+ """ Set unread if uid is the object responsible or if the object has
+ no responsible. """
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id == uid):
+ self.message_mark_as_unread(cr, uid, [obj.id], context=context)
+
+ def message_mark_as_unread(self, cr, uid, ids, context=None):
+ """ Set as unread. """
+ return self.write(cr, uid, ids, {'message_state': False}, context=context)
+
+ def message_check_and_set_read(self, cr, uid, ids, context=None):
+ """ Set read if uid is the object responsible. """
+ for obj in self.browse(cr, uid, ids, context=context):
+ if not obj.message_state and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.id == uid:
+ self.message_mark_as_read(cr, uid, [obj.id], context=context)
+
+ def message_mark_as_read(self, cr, uid, ids, context=None):
+ """ Set as read. """
+ return self.write(cr, uid, ids, {'message_state': True}, context=context)
+
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/res_partner_view.xml b/addons/mail/res_partner_view.xml
index 0a9860dc979..15f6618c8e5 100644
--- a/addons/mail/res_partner_view.xml
+++ b/addons/mail/res_partner_view.xml
@@ -10,7 +10,7 @@
diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml
index 428f53d092e..918f77e0ace 100644
--- a/addons/mail/res_users_view.xml
+++ b/addons/mail/res_users_view.xml
@@ -28,7 +28,7 @@
-
+
diff --git a/addons/mail/static/lib/jquery.expander/jquery.expander.js b/addons/mail/static/lib/jquery.expander/jquery.expander.js
new file mode 100644
index 00000000000..91cc03db918
--- /dev/null
+++ b/addons/mail/static/lib/jquery.expander/jquery.expander.js
@@ -0,0 +1,385 @@
+/*!
+ * jQuery Expander Plugin v1.4.2
+ *
+ * Date: Fri Mar 16 14:29:56 2012 EDT
+ * Requires: jQuery v1.3+
+ *
+ * Copyright 2011, Karl Swedberg
+ * Dual licensed under the MIT and GPL licenses (just like jQuery):
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ *
+ *
+ *
+*/
+
+(function($) {
+ $.expander = {
+ version: '1.4.2',
+ defaults: {
+ // the number of characters at which the contents will be sliced into two parts.
+ slicePoint: 100,
+
+ // whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false)
+ preserveWords: true,
+
+ // a threshold of sorts for whether to initially hide/collapse part of the element's contents.
+ // If after slicing the contents in two there are fewer words in the second part than
+ // the value set by widow, we won't bother hiding/collapsing anything.
+ widow: 4,
+
+ // text displayed in a link instead of the hidden part of the element.
+ // clicking this will expand/show the hidden/collapsed text
+ expandText: 'read more',
+ expandPrefix: '… ',
+
+ expandAfterSummary: false,
+
+ // class names for summary element and detail element
+ summaryClass: 'summary',
+ detailClass: 'details',
+
+ // class names for around "read-more" link and "read-less" link
+ moreClass: 'read-more',
+ lessClass: 'read-less',
+
+ // number of milliseconds after text has been expanded at which to collapse the text again.
+ // when 0, no auto-collapsing
+ collapseTimer: 0,
+
+ // effects for expanding and collapsing
+ expandEffect: 'fadeIn',
+ expandSpeed: 250,
+ collapseEffect: 'fadeOut',
+ collapseSpeed: 200,
+
+ // allow the user to re-collapse the expanded text.
+ userCollapse: true,
+
+ // text to use for the link to re-collapse the text
+ userCollapseText: 'read less',
+ userCollapsePrefix: ' ',
+
+
+ // all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called
+
+ onSlice: null, // function() {}
+ beforeExpand: null, // function() {},
+ afterExpand: null, // function() {},
+ onCollapse: null // function(byUser) {}
+ }
+ };
+
+ $.fn.expander = function(options) {
+ var meth = 'init';
+
+ if (typeof options == 'string') {
+ meth = options;
+ options = {};
+ }
+
+ var opts = $.extend({}, $.expander.defaults, options),
+ rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i,
+ rAmpWordEnd = opts.wordEnd || /(&(?:[^;]+;)?|[a-zA-Z\u00C0-\u0100]+)$/,
+ rOpenCloseTag = /<\/?(\w+)[^>]*>/g,
+ rOpenTag = /<(\w+)[^>]*>/g,
+ rCloseTag = /<\/(\w+)>/g,
+ rLastCloseTag = /(<\/[^>]+>)\s*$/,
+ rTagPlus = /^<[^>]+>.?/,
+ delayedCollapse;
+
+ var methods = {
+ init: function() {
+ this.each(function() {
+ var i, l, tmp, newChar, summTagless, summOpens, summCloses,
+ lastCloseTag, detailText, detailTagless,
+ $thisDetails, $readMore,
+ openTagsForDetails = [],
+ closeTagsForsummaryText = [],
+ defined = {},
+ thisEl = this,
+ $this = $(this),
+ $summEl = $([]),
+ o = $.meta ? $.extend({}, opts, $this.data()) : opts,
+ hasDetails = !!$this.find('.' + o.detailClass).length,
+ hasBlocks = !!$this.find('*').filter(function() {
+ var display = $(this).css('display');
+ return (/^block|table|list/).test(display);
+ }).length,
+ el = hasBlocks ? 'div' : 'span',
+ detailSelector = el + '.' + o.detailClass,
+ moreSelector = 'span.' + o.moreClass,
+ expandSpeed = o.expandSpeed || 0,
+ allHtml = $.trim( $this.html() ),
+ allText = $.trim( $this.text() ),
+ summaryText = allHtml.slice(0, o.slicePoint);
+
+ // bail out if we've already set up the expander on this element
+ if ( $.data(this, 'expander') ) {
+ return;
+ }
+
+ $.data(this, 'expander', true);
+
+ // determine which callback functions are defined
+ $.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) {
+ defined[val] = $.isFunction(o[val]);
+ });
+
+ // back up if we're in the middle of a tag or word
+ summaryText = backup(summaryText);
+
+ // summary text sans tags length
+ summTagless = summaryText.replace(rOpenCloseTag, '').length;
+
+ // add more characters to the summary, one for each character in the tags
+ while (summTagless < o.slicePoint) {
+ newChar = allHtml.charAt(summaryText.length);
+ if (newChar == '<') {
+ newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0];
+ }
+ summaryText += newChar;
+ summTagless++;
+ }
+
+ summaryText = backup(summaryText, o.preserveWords);
+
+ // separate open tags from close tags and clean up the lists
+ summOpens = summaryText.match(rOpenTag) || [];
+ summCloses = summaryText.match(rCloseTag) || [];
+
+ // filter out self-closing tags
+ tmp = [];
+ $.each(summOpens, function(index, val) {
+ if ( !rSelfClose.test(val) ) {
+ tmp.push(val);
+ }
+ });
+ summOpens = tmp;
+
+ // strip close tags to just the tag name
+ l = summCloses.length;
+ for (i = 0; i < l; i++) {
+ summCloses[i] = summCloses[i].replace(rCloseTag, '$1');
+ }
+
+ // tags that start in summary and end in detail need:
+ // a). close tag at end of summary
+ // b). open tag at beginning of detail
+ $.each(summOpens, function(index, val) {
+ var thisTagName = val.replace(rOpenTag, '$1');
+ var closePosition = $.inArray(thisTagName, summCloses);
+ if (closePosition === -1) {
+ openTagsForDetails.push(val);
+ closeTagsForsummaryText.push('' + thisTagName + '>');
+
+ } else {
+ summCloses.splice(closePosition, 1);
+ }
+ });
+
+ // reverse the order of the close tags for the summary so they line up right
+ closeTagsForsummaryText.reverse();
+
+ // create necessary summary and detail elements if they don't already exist
+ if ( !hasDetails ) {
+
+ // end script if there is no detail text or if detail has fewer words than widow option
+ detailText = allHtml.slice(summaryText.length);
+ detailTagless = $.trim( detailText.replace(rOpenCloseTag, '') );
+
+ if ( detailTagless === '' || detailTagless.split(/\s+/).length < o.widow ) {
+ return;
+ }
+ // otherwise, continue...
+ lastCloseTag = closeTagsForsummaryText.pop() || '';
+ summaryText += closeTagsForsummaryText.join('');
+ detailText = openTagsForDetails.join('') + detailText;
+
+ } else {
+ // assume that even if there are details, we still need readMore/readLess/summary elements
+ // (we already bailed out earlier when readMore el was found)
+ // but we need to create els differently
+
+ // remove the detail from the rest of the content
+ detailText = $this.find(detailSelector).remove().html();
+
+ // The summary is what's left
+ summaryText = $this.html();
+
+ // allHtml is the summary and detail combined (this is needed when content has block-level elements)
+ allHtml = summaryText + detailText;
+
+ lastCloseTag = '';
+ }
+ o.moreLabel = $this.find(moreSelector).length ? '' : buildMoreLabel(o);
+
+ if (hasBlocks) {
+ detailText = allHtml;
+ }
+ summaryText += lastCloseTag;
+
+ // onSlice callback
+ o.summary = summaryText;
+ o.details = detailText;
+ o.lastCloseTag = lastCloseTag;
+
+ if (defined.onSlice) {
+ // user can choose to return a modified options object
+ // one last chance for user to change the options. sneaky, huh?
+ // but could be tricky so use at your own risk.
+ tmp = o.onSlice.call(thisEl, o);
+
+ // so, if the returned value from the onSlice function is an object with a details property, we'll use that!
+ o = tmp && tmp.details ? tmp : o;
+ }
+
+ // build the html with summary and detail and use it to replace old contents
+ var html = buildHTML(o, hasBlocks);
+
+ $this.html( html );
+
+ // set up details and summary for expanding/collapsing
+ $thisDetails = $this.find(detailSelector);
+ $readMore = $this.find(moreSelector);
+ $thisDetails.hide();
+ $readMore.find('a').unbind('click.expander').bind('click.expander', expand);
+
+ $summEl = $this.find('div.' + o.summaryClass);
+
+ if ( o.userCollapse && !$this.find('span.' + o.lessClass).length ) {
+ $this
+ .find(detailSelector)
+ .append('' + o.userCollapsePrefix + '' + o.userCollapseText + '');
+ }
+
+ $this
+ .find('span.' + o.lessClass + ' a')
+ .unbind('click.expander')
+ .bind('click.expander', function(event) {
+ event.preventDefault();
+ clearTimeout(delayedCollapse);
+ var $detailsCollapsed = $(this).closest(detailSelector);
+ reCollapse(o, $detailsCollapsed);
+ if (defined.onCollapse) {
+ o.onCollapse.call(thisEl, true);
+ }
+ });
+
+ function expand(event) {
+ event.preventDefault();
+ $readMore.hide();
+ $summEl.hide();
+ if (defined.beforeExpand) {
+ o.beforeExpand.call(thisEl);
+ }
+
+ $thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function() {
+ $thisDetails.css({zoom: ''});
+ if (defined.afterExpand) {o.afterExpand.call(thisEl);}
+ delayCollapse(o, $thisDetails, thisEl);
+ });
+ }
+
+ }); // this.each
+ },
+ destroy: function() {
+ if ( !this.data('expander') ) {
+ return;
+ }
+ this.removeData('expander');
+ this.each(function() {
+ var $this = $(this),
+ o = $.meta ? $.extend({}, opts, $this.data()) : opts,
+ details = $this.find('.' + o.detailClass).contents();
+
+ $this.find('.' + o.moreClass).remove();
+ $this.find('.' + o.summaryClass).remove();
+ $this.find('.' + o.detailClass).after(details).remove();
+ $this.find('.' + o.lessClass).remove();
+
+ });
+ }
+ };
+
+ // run the methods (almost always "init")
+ if ( methods[meth] ) {
+ methods[ meth ].call(this);
+ }
+
+ // utility functions
+ function buildHTML(o, blocks) {
+ var el = 'span',
+ summary = o.summary;
+ if ( blocks ) {
+ el = 'div';
+ // if summary ends with a close tag, tuck the moreLabel inside it
+ if ( rLastCloseTag.test(summary) && !o.expandAfterSummary) {
+ summary = summary.replace(rLastCloseTag, o.moreLabel + '$1');
+ } else {
+ // otherwise (e.g. if ends with self-closing tag) just add moreLabel after summary
+ // fixes #19
+ summary += o.moreLabel;
+ }
+
+ // and wrap it in a div
+ summary = '
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index 99eb7192738..3da63fb1adc 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -12,18 +12,18 @@
or
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/addons/marketing_campaign/marketing_campaign.py b/addons/marketing_campaign/marketing_campaign.py
index a6294182301..417bd694624 100644
--- a/addons/marketing_campaign/marketing_campaign.py
+++ b/addons/marketing_campaign/marketing_campaign.py
@@ -439,7 +439,7 @@ class marketing_campaign_activity(osv.osv):
'revenue': fields.float('Revenue', help="Set an expected revenue if you consider that every campaign item that has reached this point has generated a certain revenue. You can get revenue statistics in the Reporting section", digits_compute=dp.get_precision('Sale Price')),
'signal': fields.char('Signal', size=128,
help='An activity with a signal can be called programmatically. Be careful, the workitem is always created when a signal is sent'),
- 'keep_if_condition_not_met': fields.boolean("Don't delete workitems",
+ 'keep_if_condition_not_met': fields.boolean("Don't Delete Workitems",
help="By activating this option, workitems that aren't executed because the condition is not met are marked as cancelled instead of being deleted.")
}
diff --git a/addons/marketing_campaign/marketing_campaign_view.xml b/addons/marketing_campaign/marketing_campaign_view.xml
index 41ed78da709..6a06cb33f1d 100644
--- a/addons/marketing_campaign/marketing_campaign_view.xml
+++ b/addons/marketing_campaign/marketing_campaign_view.xml
@@ -9,6 +9,22 @@
Marketing Campaign
======================
-->
+
+
+
+ marketing.campaign.form
@@ -24,20 +40,30 @@
+