From c02116b41f9c500828c54725c11541ddcd1c5dfd Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Tue, 14 Aug 2012 21:44:12 +0200 Subject: [PATCH 001/264] [IMP] cleaning of need action bzr revid: fp@openerp.com-20120814194412-2bcujivyd45buikx --- openerp/addons/base/ir/ir.xml | 24 --- openerp/addons/base/ir/ir_needaction.py | 182 +++--------------- openerp/addons/base/ir/ir_ui_menu.py | 18 +- .../addons/base/security/ir.model.access.csv | 1 - openerp/osv/orm.py | 33 +--- 5 files changed, 36 insertions(+), 222 deletions(-) diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index fac2946e204..c554a8896aa 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -556,30 +556,6 @@ - - - ir.needaction_users_rel.tree - ir.needaction_users_rel - 10 - - - - - - - - - - - Need action relationships - ir.needaction_users_rel - form - tree,form - - - - - ir.ui.view diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 0f28c33ded9..95fc6d81b09 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -19,180 +19,48 @@ # ############################################################################## -import openerp.pooler as pooler -from operator import itemgetter from osv import osv, fields -from tools.translate import _ - -class ir_needaction_users_rel(osv.Model): - ''' ir_needaction_users_rel holds data related to the needaction - mechanism inside OpenERP. A row in this model is characterized - by: - - res_model: model of the record requiring an action - - res_id: ID of the record requiring an action - - user_id: foreign key to the res.users table, to the user that has to - perform the action - This model can be seen as a many2many, linking (res_model, res_id) to users - (those whose attention is required on the record). ''' - - _name = 'ir.needaction_users_rel' - _description = 'Needaction relationship table' - _rec_name = 'id' - _order = 'id desc' - _columns = { - 'res_model': fields.char('Related Document Model', size=128, - select=1, required=True), - 'res_id': fields.integer('Related Document ID', - select=1, required=True), - 'user_id': fields.many2one('res.users', string='Related User', - ondelete='cascade', select=1, required=True), - } - - def _get_users(self, cr, uid, res_ids, res_model, context=None): - """Given res_ids of res_model, get user_ids present in table""" - rel_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context) - return list(set(map(itemgetter('user_id'), self.read(cr, uid, rel_ids, ['user_id'], context=context)))) - - def create_users(self, cr, uid, res_ids, res_model, user_ids, context=None): - """Given res_ids of res_model, add user_ids to the relationship table""" - for res_id in res_ids: - for user_id in user_ids: - self.create(cr, uid, {'res_model': res_model, 'res_id': res_id, 'user_id': user_id}, context=context) - return True - - def unlink_users(self, cr, uid, res_ids, res_model, context=None): - """Given res_ids of res_model, delete all entries in the relationship table""" - to_del_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context) - return self.unlink(cr, uid, to_del_ids, context=context) - - def update_users(self, cr, uid, res_ids, res_model, user_ids, context=None): - """Given res_ids of res_model, update their entries in the relationship table to user_ids""" - # read current records - cur_users = self._get_users(cr, uid, res_ids, res_model, context=context) - if len(cur_users) == len(user_ids) and all(cur_user in user_ids for cur_user in cur_users): - return True - # unlink old records - self.unlink_users(cr, uid, res_ids, res_model, context=context) - # link new records - self.create_users(cr, uid, res_ids, res_model, user_ids, context=context) - return True - class ir_needaction_mixin(osv.Model): '''Mixin class for objects using the need action feature. - - Need action feature can be used by objects having to be able to - signal that an action is required on a particular record. If in - the business logic an action must be performed by somebody, for - instance validation by a manager, this mechanism allows to set a + + Need action feature can be used by objects having to be able to + signal that an action is required on a particular record. If in + the business logic an action must be performed by somebody, for + instance validation by a manager, this mechanism allows to set a list of users asked to perform an action. - - This class wraps a class (ir.ir_needaction_users_rel) that - behaves like a many2many field. This class handles the low-level - considerations of updating relationships. Every change made on - the record calls a method that updates the relationships. - - Objects using the 'need_action' feature should override the - ``get_needaction_user_ids`` method. This methods returns a - dictionary whose keys are record ids, and values a list of user - ids, like in a many2many relationship. Therefore by defining - only one method, you can specify if an action is required by - defining the users that have to do it, in every possible - situation. - - This class also offers several global services: - - ``needaction_get_record_ids``: for the current model and uid, get - all record ids that ask this user to perform an action. This - mechanism is used for instance to display the number of pending - actions in menus, such as Leads (12) + + Objects using the 'need_action' feature should override the + ``needaction_domain_get`` method. This methods returns a + domain to filter records requiring an action for a specific user. + + This class also offers several global services: - ``needaction_get_action_count``: as ``needaction_get_record_ids`` - but returns only the number of action, not the ids (performs a + but returns only the number of action, not the ids (performs a search with count=True) + ''' - The ``ir_needaction_mixin`` class adds a calculated field - ``needaction_pending``. This function field allows to state - whether a given record has a needaction for the current user. - This is usefull if you want to customize views according to the - needaction feature. For example, you may want to set records in - bold in a list view if the current user has an action to perform - on the record. ''' - _name = 'ir.needaction_mixin' - _description = 'Need action mixin' - - def get_needaction_pending(self, cr, uid, ids, name, arg, context=None): - res = {} - needaction_user_ids = self.get_needaction_user_ids(cr, uid, ids, context=context) - for id in ids: - res[id] = uid in needaction_user_ids[id] - return res + _description = 'Need Action Mixin' + _needaction = True - def search_needaction_pending(self, cr, uid, self_again, field_name, criterion, context=None): - ids = self.needaction_get_record_ids( - cr, uid, uid, limit=1024, context=context) - return [('id', 'in', ids)] - - _columns = { - 'needaction_pending': fields.function( - get_needaction_pending, type='boolean', - fnct_search=search_needaction_pending, - string='Need action pending', - help="If True, this field states that users have to perform an " \ - "action This field comes from the ir.needaction_mixin class."), - } - #------------------------------------------------------ # Addon API #------------------------------------------------------ - - 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], } + + def _needaction_domain_get(self, cr, uid, context=None): + """ Returns the domain to filter records that require an action + :return: domain or False is no need action """ - return dict((id,list()) for id in ids) - - def create(self, cr, uid, values, context=None): - rel_obj = self.pool.get('ir.needaction_users_rel') - # perform create - obj_id = super(ir_needaction_mixin, self).create(cr, uid, values, context=context) - # link user_ids - needaction_user_ids = self.get_needaction_user_ids(cr, uid, [obj_id], context=context) - rel_obj.create_users(cr, uid, [obj_id], self._name, needaction_user_ids[obj_id], context=context) - return obj_id - - def write(self, cr, uid, ids, values, context=None): - rel_obj = self.pool.get('ir.needaction_users_rel') - # perform write - write_res = super(ir_needaction_mixin, self).write(cr, uid, ids, values, context=context) - # get and update user_ids - needaction_user_ids = self.get_needaction_user_ids(cr, uid, ids, context=context) - for id in ids: - rel_obj.update_users(cr, uid, [id], self._name, needaction_user_ids[id], context=context) - return write_res - - def unlink(self, cr, uid, ids, context=None): - # unlink user_ids - rel_obj = self.pool.get('ir.needaction_users_rel') - rel_obj.unlink_users(cr, uid, ids, self._name, context=context) - # perform unlink - return super(ir_needaction_mixin, self).unlink(cr, uid, ids, context=context) - + return False + #------------------------------------------------------ # "Need action" API #------------------------------------------------------ - - def needaction_get_record_ids(self, cr, uid, user_id, limit=80, context=None): - """Given the current model and a user_id - return the record ids that require the user to perform an - action""" - rel_obj = self.pool.get('ir.needaction_users_rel') - rel_ids = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context) - return map(itemgetter('res_id'), rel_obj.read(cr, uid, rel_ids, ['res_id'], context=context)) - - def needaction_get_action_count(self, cr, uid, user_id, limit=80, context=None): + + def _needaction_count(self, cr, uid, domain=[], context=None): """Given the current model and a user_id get the number of actions it has to perform""" - rel_obj = self.pool.get('ir.needaction_users_rel') - return rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, count=True, context=context) + dom = self._needaction_domain_get(cr, uid, context=context) + return self.search(cr, uid, domain+dom, context=context, count=True) -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index 77c0c4d738a..1bb517c570f 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -255,23 +255,23 @@ class ir_ui_menu(osv.osv): return res - def _get_needaction_info(self, cr, uid, id, domain=[], context={}): - return [False, 0] - def _get_needaction(self, cr, uid, ids, field_names, args, context=None): if context is None: context = {} res = {} for menu in self.browse(cr, uid, ids, context=context): + res[menu.id]['needaction_enabled'] = False + res[menu.id]['needaction_counter'] = False res[menu.id] = {} if menu.action and menu.action.type == 'ir.actions.act_window' and menu.action.res_model: - menu_needaction_res = self.pool.get(menu.action.res_model)._get_needaction_info(cr, uid, uid, domain=menu.action.domain, context=context) - else: - menu_needaction_res = [False, 0] - res[menu.id]['needaction_enabled'] = menu_needaction_res[0] - res[menu.id]['needaction_counter'] = menu_needaction_res[1] + obj = self.pool.get(menu.action.res_model) + if obj._needaction_active: + res[menu.id]['needaction_enabled'] = obj._needaction_active + # check domain and context: should we evaluate the domain ? + # and add context of the action ? + res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, menu.action.domain, context=context) return res - + _columns = { 'name': fields.char('Menu', size=64, required=True, translate=True), 'sequence': fields.integer('Sequence'), diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index d49aa18db8c..b0218e977bc 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -118,6 +118,5 @@ "access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0 "access_ir_mail_server_all","ir_mail_server","model_ir_mail_server",,1,0,0,0 "access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0 -"access_ir_needaction_users_rel","ir_needaction_users_rel","model_ir_needaction_users_rel",,1,1,1,1 "access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1 diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 7c8085ea8ed..368a37f455a 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -666,6 +666,7 @@ class BaseModel(object): _order = 'id' _sequence = None _description = None + _needaction = False # dict of {field:method}, with method returning the name_get of records # to include in the _read_group, if grouped on this field @@ -4960,37 +4961,7 @@ class BaseModel(object): # backwards compatibility get_xml_id = get_external_id _get_xml_ids = _get_external_ids - - def _get_needaction_info(self, cr, uid, user_id, limit=None, order=None, domain=False, context=None): - """Base method for needaction mechanism - - see ir.needaction for actual implementation - - if the model uses the need action mechanism - (hasattr(model_obj, 'needaction_get_record_ids')): - - get the record ids on which the user has actions to perform - - evaluate the menu domain - - compose a new domain: menu domain, limited to ids of - records requesting an action - - count the number of records maching that domain, that - is the number of actions the user has to perform - - this method returns default values - :param: model_name: the name of the model (ex: hr.holidays) - :param: user_id: the id of user - :return: [uses_needaction=True/False, needaction_uid_ctr=%d] - """ - if hasattr(self, 'needaction_get_record_ids'): - # Arbitrary limit, but still much lower thant infinity, to avoid - # getting too much data. - ids = self.needaction_get_record_ids(cr, uid, user_id, limit=8192, context=context) - if not ids: - return [True, 0] - if domain: - new_domain = eval(domain, locals_dict={'uid': user_id}) + [('id', 'in', ids)] - else: - new_domain = [('id', 'in', ids)] - return [True, self.search(cr, uid, new_domain, limit=limit, order=order, count=True, context=context)] - else: - return [False, 0] - + # Transience def is_transient(self): """ Return whether the model is transient. From 560e9e7fd259c4da0285d3730ec72c1f462b755a Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Tue, 14 Aug 2012 21:58:40 +0200 Subject: [PATCH 002/264] [IMP] put needaction_pending again, new implementation bzr revid: fp@openerp.com-20120814195840-yaha9r3qvj8o13zr --- openerp/addons/base/ir/ir_needaction.py | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 95fc6d81b09..5552b42052e 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -38,12 +38,39 @@ class ir_needaction_mixin(osv.Model): - ``needaction_get_action_count``: as ``needaction_get_record_ids`` but returns only the number of action, not the ids (performs a search with count=True) + + The ``ir_needaction_mixin`` class adds a calculated field + ``needaction_pending``. This function field allows to state + whether a given record has a needaction for the current user. + This is usefull if you want to customize views according to the + needaction feature. For example, you may want to set records in + bold in a list view if the current user has an action to perform + on the record. ''' _name = 'ir.needaction_mixin' _description = 'Need Action Mixin' _needaction = True + def get_needaction_pending(self, cr, uid, ids, name, arg, context=None): + dom = self._needaction_domain_get(cr, uid, context=context) + result = dict.fromkeys(ids, False) + ids2 = self.search(cr, uid, [('id','in',ids)+dom], context=context) + for idna in ids2: + result[idna] = True + return result + + def search_needaction_pending(self, cr, uid, field, field_name, criterion, context=None): + return self._needaction_domain_get(cr, uid, context=context) + + _columns = { + 'needaction_pending': fields.function( + get_needaction_pending, type='boolean', + fnct_search=search_needaction_pending, + string='Need an Action', + help="Do the current user requires to perform an action."), + } + #------------------------------------------------------ # Addon API #------------------------------------------------------ From 8b61ed737c1e5da85dc75091246cab89ca6b85b0 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 15 Aug 2012 12:29:19 +0200 Subject: [PATCH 003/264] [IMP] need action new implementation bzr revid: fp@openerp.com-20120815102919-w3iuspeoz5fmz97u --- addons/base_calendar/crm_meeting.py | 4 ++++ addons/crm/crm_phonecall.py | 2 +- addons/hr_holidays/hr_holidays.py | 28 +++++++++++++--------------- addons/purchase/purchase.py | 8 ++------ addons/sale/sale.py | 8 ++------ 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/addons/base_calendar/crm_meeting.py b/addons/base_calendar/crm_meeting.py index cb96ff2a4d0..a66449a8b5d 100644 --- a/addons/base_calendar/crm_meeting.py +++ b/addons/base_calendar/crm_meeting.py @@ -70,6 +70,10 @@ class crm_meeting(base_state, osv.Model): # OpenChatter # ---------------------------------------- + # shows events of the day for this user + def needaction_domain_get(self, cr, uid, domain=[], context={}): + return [('date','<=',time.strftime('%Y-%M-%D 23:59:59')), ('date_deadline','>=', time.strftime('%Y-%M-%D 00:00:00')), ('user_id','=',uid)] + def case_get_note_msg_prefix(self, cr, uid, id, context=None): return 'Meeting' diff --git a/addons/crm/crm_phonecall.py b/addons/crm/crm_phonecall.py index 8ffa8dd591e..7a544da5dc1 100644 --- a/addons/crm/crm_phonecall.py +++ b/addons/crm/crm_phonecall.py @@ -32,7 +32,7 @@ class crm_phonecall(base_state, osv.osv): _name = "crm.phonecall" _description = "Phonecall" _order = "id desc" - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread'] _columns = { # base_state required fields 'date_action_last': fields.datetime('Last Action', readonly=1), diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 7eedfa54ce5..0134fc79fdd 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -347,24 +347,22 @@ class hr_holidays(osv.osv): if leaves_rest < record.number_of_days_temp: raise osv.except_osv(_('Warning!'), _('There are not enough %s allocated for employee %s; please create an allocation request for this leave type.') % (record.holiday_status_id.name, record.employee_id.name)) return True - + # ----------------------------- # OpenChatter and notifications # ----------------------------- - - def get_needaction_user_ids(self, cr, uid, ids, context=None): - result = super(hr_holidays, self).get_needaction_user_ids(cr, uid, ids, context=context) - for obj in self.browse(cr, uid, ids, context=context): - if obj.state == 'confirm' and obj.employee_id.parent_id: - result[obj.id] = [obj.employee_id.parent_id.user_id.id] - elif obj.state == 'validate1': - # get group_hr_manager: everyone will be warned of second validation - res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_hr_manager') or False - obj_id = res and res[1] or False - if obj_id: - hr_manager_group = self.pool.get('res.groups').read(cr, uid, [obj_id], ['users'], context=context)[0] - result[obj.id] = hr_manager_group['users'] - return result + + def needaction_domain_get(self, cr, uid, ids, context=None): + # to be tested, otherwise convert into employee_id in ... + emp_obj = self.pool.get('hr.employee') + empids = emp_obj.search(cr, uid, [('parent_id.user_id','=',uid)], context=context) + dom = [ + '&', ('state','=','confirm'),('employee_id', 'in', empids) + ] + # if this user is a hr.manager, he should do second validations + if self.pool.get('res.users').has_group(cr, uid, 'base.group_hr_manager'): + dom = ['|'] + dom + [ ('state','=','validate1') ] + return dom def message_get_subscribers(self, cr, uid, ids, context=None): """ Override to add employee and its manager. """ diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index b02e959301f..705f55f5d5c 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -732,12 +732,8 @@ class purchase_order(osv.osv): # OpenChatter methods and notifications # -------------------------------------- - def get_needaction_user_ids(self, cr, uid, ids, context=None): - result = super(purchase_order, self).get_needaction_user_ids(cr, uid, ids, context=context) - for obj in self.browse(cr, uid, ids, context=context): - if obj.state == 'approved': - result[obj.id].append(obj.validator.id) - return result + def needaction_domain_get(self, cr, uid, ids, context=None): + return [('state','=','draft')] def create_send_note(self, cr, uid, ids, context=None): return self.message_append_note(cr, uid, ids, body=_("Request for quotation created."), context=context) diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 722b22ba602..2a9aa08d3ca 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -1022,12 +1022,8 @@ class sale_order(osv.osv): # OpenChatter methods and notifications # ------------------------------------------------ - def get_needaction_user_ids(self, cr, uid, ids, context=None): - result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context) - for obj in self.browse(cr, uid, ids, context=context): - if (obj.state == 'manual' or obj.state == 'progress'): - result[obj.id].append(obj.user_id.id) - return result + def needaction_domain_get(self, cr, uid, ids, context=None): + return [('state', '=', 'draft'), ('user_id','=',uid)] def create_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): From eb0669850d6f07d19db12291fe78a40eb3ca8e4e Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 15 Aug 2012 15:36:43 +0200 Subject: [PATCH 004/264] [IMP] removed needaction_pending, some cleaning in messages mail bzr revid: fp@openerp.com-20120815133643-loyzmyp6azs5lr47 --- addons/base_calendar/crm_meeting_view.xml | 25 +- addons/crm/crm_lead_view.xml | 16 +- addons/crm/crm_phonecall_view.xml | 8 +- addons/event/event_view.xml | 12 +- addons/hr_holidays/hr_holidays_view.xml | 10 +- addons/hr_recruitment/hr_recruitment_view.xml | 6 +- addons/mail/mail_message.py | 90 ++--- addons/mail/mail_subscription.py | 40 ++ addons/mail/mail_thread.py | 342 ++++++------------ addons/mail/res_users.py | 3 +- addons/mrp/mrp_view.xml | 4 +- addons/project/project_view.xml | 14 +- addons/project_issue/project_issue_view.xml | 10 +- addons/purchase/purchase_view.xml | 6 +- .../purchase_requisition_view.xml | 4 +- addons/sale/sale_view.xml | 6 +- 16 files changed, 246 insertions(+), 350 deletions(-) diff --git a/addons/base_calendar/crm_meeting_view.xml b/addons/base_calendar/crm_meeting_view.xml index c6826786379..b57d7dcb2c7 100644 --- a/addons/base_calendar/crm_meeting_view.xml +++ b/addons/base_calendar/crm_meeting_view.xml @@ -170,7 +170,7 @@ -
- -

+ +

    @@ -25,7 +24,7 @@ -->
  • - +
  • From d572beb36e8827bda4a4501ca62430b75c664a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 22 Aug 2012 12:13:33 +0200 Subject: [PATCH 112/264] [IMP] res_users in mail: use message_subscribe, as we already have a browse in create_welcome_message, that I moved to create. bzr revid: tde@openerp.com-20120822101333-clct0k4o0pu9r2ix --- addons/mail/res_users.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py index 8eacd0580d7..1e638251e40 100644 --- a/addons/mail/res_users.py +++ b/addons/mail/res_users.py @@ -100,15 +100,15 @@ class res_users(osv.Model): # create user that follows its related partner user_id = super(res_users, self).create(cr, uid, data, context=context) - self.pool.get('res.partner').message_subscribe_users(cr, uid, [user_id], [user_id], context=context) + user = self.browse(cr, uid, user_id, context=context) + self.pool.get('res.partner').message_subscribe(cr, uid, [user.partner_id.id], [user.partner_id.id], context=context) # alias mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context) # create a welcome message self._create_welcome_message(cr, uid, user_id, context=context) return user_id - def _create_welcome_message(self, cr, uid, user_id, context=None): - user = self.browse(cr, uid, user_id, context=context) + def _create_welcome_message(self, cr, uid, user, context=None): company_name = user.company_id.name if user.company_id else _('the company') body = '''%s has joined %s.''' % (user.name, company_name) # TODO change 1 into user.id but catch errors From e46c8e43ff588ba4a7b0d902527cdf3011efed9d Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 22 Aug 2012 12:37:25 +0200 Subject: [PATCH 113/264] [IMP] Removing a bad practice: - what happens if you overwrite check_read and modules calls check_access_rights ? - 5 methods to learn to understand the API insead of one bzr revid: fp@tinyerp.com-20120822103725-vdubhkzlake2jyvo --- openerp/osv/orm.py | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 368a37f455a..e8f59e50cbd 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2346,7 +2346,7 @@ class BaseModel(object): def read_string(self, cr, uid, id, langs, fields=None, context=None): res = {} res2 = {} - self.pool.get('ir.translation').check_read(cr, uid) + self.pool.get('ir.translation').check_access_rights(cr, uid, 'read') if not fields: fields = self._columns.keys() + self._inherit_fields.keys() #FIXME: collect all calls to _get_source into one SQL call. @@ -2370,7 +2370,7 @@ class BaseModel(object): return res def write_string(self, cr, uid, id, langs, vals, context=None): - self.pool.get('ir.translation').check_write(cr, uid) + self.pool.get('ir.translation').check_access_rights(cr, uid, 'write') #FIXME: try to only call the translation in one SQL for lang in langs: for field in vals: @@ -2518,7 +2518,7 @@ class BaseModel(object): """ context = context or {} - self.check_read(cr, uid) + self.check_access_rights(cr, uid, 'read') if not fields: fields = self._columns.keys() @@ -3399,8 +3399,7 @@ class BaseModel(object): if context is None: context = {} - write_access = self.check_write(cr, user, False) or \ - self.check_create(cr, user, False) + write_access = self.check_access_rights(cr, user, 'write') or self.check_access_rights(cr, user, 'create') res = {} @@ -3464,7 +3463,7 @@ class BaseModel(object): if not context: context = {} - self.check_read(cr, user) + self.check_access_rights(cr, user, 'read') if not fields: fields = list(set(self._columns.keys() + self._inherit_fields.keys())) if isinstance(ids, (int, long)): @@ -3738,18 +3737,6 @@ class BaseModel(object): according to the access rights.""" return self.pool.get('ir.model.access').check(cr, uid, self._name, operation, raise_exception) - def check_create(self, cr, uid, raise_exception=True): - return self.check_access_rights(cr, uid, 'create', raise_exception) - - def check_read(self, cr, uid, raise_exception=True): - return self.check_access_rights(cr, uid, 'read', raise_exception) - - def check_unlink(self, cr, uid, raise_exception=True): - return self.check_access_rights(cr, uid, 'unlink', raise_exception) - - def check_write(self, cr, uid, raise_exception=True): - return self.check_access_rights(cr, uid, 'write', raise_exception) - def check_access_rule(self, cr, uid, ids, operation, context=None): """Verifies that the operation given by ``operation`` is allowed for the user according to ir.rules. @@ -3813,7 +3800,7 @@ class BaseModel(object): self._check_concurrency(cr, ids, context) - self.check_unlink(cr, uid) + self.check_access_rights(cr, uid, 'unlink') ir_property = self.pool.get('ir.property') @@ -3949,7 +3936,7 @@ class BaseModel(object): ids = [ids] self._check_concurrency(cr, ids, context) - self.check_write(cr, user) + self.check_access_rights(cr, user, 'write') result = self._store_get_values(cr, user, ids, vals.keys(), context) or [] @@ -4169,7 +4156,7 @@ class BaseModel(object): if self.is_transient(): self._transient_vacuum(cr, user) - self.check_create(cr, user) + self.check_access_rights(cr, user, 'create') if self._log_access: for f in LOG_ACCESS_COLUMNS: @@ -4661,7 +4648,7 @@ class BaseModel(object): """ if context is None: context = {} - self.check_read(cr, access_rights_uid or user) + self.check_access_rights(cr, access_rights_uid or user, 'read') # For transient models, restrict acces to the current user, except for the super-user if self.is_transient() and self._log_access and user != SUPERUSER_ID: From 40949f8052bf448ce9d67f73159da4a66baa8b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 22 Aug 2012 13:00:57 +0200 Subject: [PATCH 114/264] [FIX] mail.js: fixed a wrong var name. bzr revid: tde@openerp.com-20120822110057-1dqo701k2lmpmoxo --- addons/mail/static/src/js/mail.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index d0c4ff55ee8..409ff41fcf5 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -639,7 +639,7 @@ openerp.mail = function(session) { set_value: function() { this._super.apply(this, arguments); if (! this.view.datarecord.id || session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { - this.$el.find('oe_mail_thread').hide(); + this.$element.find('oe_mail_thread').hide(); return; } // update context From 49596ebe91b11bfa759782eb6176c6a0a918279a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 22 Aug 2012 13:03:13 +0200 Subject: [PATCH 115/264] [IMP] mail_thread: message_is_follower field is now calculated with other functional fields (message_summary and message_read). Cleaned message_subscribe and message_unsubscribe. Added deletion of followers when unlinking a record. Propagated change in mail_group view. bzr revid: tde@openerp.com-20120822110313-is3p1h4s2e8c4hbm --- addons/mail/mail_group_view.xml | 10 +- addons/mail/mail_thread.py | 143 ++++++++++---------- addons/mail/static/src/js/mail_followers.js | 8 +- 3 files changed, 80 insertions(+), 81 deletions(-) diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml index 1aa1e1aa687..c303b1fd40b 100644 --- a/addons/mail/mail_group_view.xml +++ b/addons/mail/mail_group_view.xml @@ -14,8 +14,8 @@ - + @@ -32,8 +32,8 @@

    @@ -80,10 +80,10 @@
    + - +
    diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 80cb7794e54..3bc45d3af26 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -69,38 +69,23 @@ class mail_thread(osv.Model): _description = 'Email Thread' # TODO: may be we should make it _inherit ir.needaction - def _get_is_follower(self, cr, uid, ids, name, args, context=None): - subobj = self.pool.get('mail.followers') - subids = subobj.search(cr, uid, [ - ('res_model','=',self._name), - ('res_id', 'in', ids), - ('partner_id.user_ids','in',[uid])], context=context) - result = dict.fromkeys(ids, False) - for sub in subobj.browse(cr, uid, subids, context=context): - result[sub.res_id] = True - return result - def _get_message_data(self, cr, uid, ids, name, args, context=None): - res = {} - for id in ids: - res[id] = { - 'message_unread': False, - 'message_summary': '' - } - nobj = self.pool.get('mail.notification') - nids = nobj.search(cr, uid, [ - ('partner_id.user_ids','in',[uid]), - ('message_id.res_id','in', ids), - ('message_id.model','=', self._name), - ('read','=',False) + res = dict( (id, dict(message_unread=False, message_summary='')) for id in ids) + user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + + notif_obj = self.pool.get('mail.notification') + notif_ids = notif_obj.search(cr, uid, [ + ('partner_id.user_ids', 'in', [uid]), + ('message_id.res_id', 'in', ids), + ('message_id.model', '=', self._name), + ('read', '=', False) ], context=context) - for notif in nobj.browse(cr, uid, nids, context=context): + for notif in notif_obj.browse(cr, uid, notif_ids, context=context): res[notif.message_id.res_id]['message_unread'] = True for thread in self.browse(cr, uid, ids, context=context): - message_ids = thread.message_ids - follower_ids = thread.message_follower_ids - res[id]['message_summary'] = "9 %d + %d" % (len(message_ids), len(follower_ids)), + res[thread.id]['message_summary'] = "9 %d + %d" % (len(thread.message_ids), len(thread.message_follower_ids)) + res[thread.id]['message_is_follower'] = user.partner_id.id in [follower.id for follower in thread.message_follower_ids] return res # FP Note: todo @@ -108,10 +93,10 @@ class mail_thread(osv.Model): return [] _columns = { - 'message_is_follower': fields.function(_get_is_follower, - type='boolean', string='Is a Follower'), + 'message_is_follower': fields.function(_get_message_data, + type='boolean', string='Is a Follower', multi='_get_message_data'), 'message_follower_ids': fields.many2many('res.partner', 'mail_followers', 'res_id', 'partner_id', - # FP Note: implement this domain=lambda self: [('res_model','=',self._name)], + domain=lambda self: [('res_model','=',self._name)], string='Followers'), 'message_ids': fields.one2many('mail.message', 'res_id', domain=lambda self: [('model','=',self._name)], @@ -129,26 +114,26 @@ class mail_thread(osv.Model): } #------------------------------------------------------ - # Automatic subscription when creating/reading + # Automatic subscription when creating #------------------------------------------------------ def create(self, cr, uid, vals, context=None): - """ Override of create to subscribe the current user - """ + """ Override of create to subscribe the current user. """ thread_id = super(mail_thread, self).create(cr, uid, vals, context=context) self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context) return thread_id def unlink(self, cr, uid, ids, context=None): - """Override unlink, to automatically delete messages - that are linked with res_model and res_id, not through - a foreign key with a 'cascade' ondelete attribute. - Notifications will be deleted with messages - """ + """ Override unlink to delete messages and followers. This cannot be + cascaded, because link is done through (res_model, res_id). """ msg_obj = self.pool.get('mail.message') + fol_obj = self.pool.get('mail.followers') # delete messages and notifications - msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context) - msg_obj.unlink(cr, uid, msg_to_del_ids, context=context) + msg_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context) + msg_obj.unlink(cr, uid, msg_ids, context=context) + # delete followers + fol_ids = fol_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context) + fol_obj.unlink(cr, uid, fol_ids, context=context) return super(mail_thread, self).unlink(cr, uid, ids, context=context) #------------------------------------------------------ @@ -557,54 +542,66 @@ class mail_thread(osv.Model): #------------------------------------------------------ - # Subscription mechanism + # Followers API #------------------------------------------------------ def message_subscribe_users(self, cr, uid, ids, user_ids=None, context=None): - if not user_ids: user_ids = [uid] - partners = {} - for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context): - partners[user.partner_id.id] = True - return self.message_subscribe(cr, uid, ids, partners.keys(), context=context) + """ Wrapper on message_subscribe, using users. If user_ids is not + provided, subscribe uid instead. """ + # isinstance: because using message_subscribe_users called in a view set the context as user_ids + if not user_ids or isinstance(user_ids, dict): user_ids = [uid] + partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)] + return self.message_subscribe(cr, uid, ids, partner_ids, context=context) def message_subscribe(self, cr, uid, ids, partner_ids, context=None): - """ - :param partner_ids: a list of user_ids; if not set, subscribe - uid instead + """ Add partners to the records followers. This implementation cannot + directly use [(4, partner_id)] because of the res_model column of + mail.followers. We therefore check access rights and access rules + before directly creating a follower record. This way we simulate + a write in message_follower_ids, enabling a correct check of access + rights. + + :param partner_ids: a list of partner_ids to subscribe :param return: new value of followers, for Chatter """ - obj = self.pool.get('mail.followers') - objids = obj.search(cr, uid, [ - ('res_id', 'in', ids), - ('res_model', '=', self._name), - ('partner_id', 'in', partner_ids), + self.check_access_rights(cr, uid, 'write', raise_exception=True) + self.check_access_rule(cr, uid, ids, 'write', context=context) + + fol_obj = self.pool.get('mail.followers') + fol_ids = fol_obj.search(cr, uid, [ + ('res_id', 'in', ids), ('res_model', '=', self._name), ('partner_id', 'in', partner_ids) ], context=context) followers = {} - for follow in obj.browse(cr, uid, objids, context=context): - followers.setdefault(follow.partner_id.id, {})[follow.res_id] = True - create_ids = [] + for fol in fol_obj.browse(cr, uid, fol_ids, context=context): + followers.setdefault(fol.partner_id.id, []).append(fol.res_id) + for res_id in ids: for partner_id in partner_ids: - if followers.get(partner_id, {}).get(res_id, False): - continue - create_ids.append(obj.create(cr, uid, { - 'res_model': self._name, - 'res_id': res_id, 'partner_id': partner_id - }, context=context)) - return create_ids + if not res_id in followers.get(partner_id, []): + fol_obj.create(cr, uid, { + 'res_id': res_id, 'partner_id': partner_id, 'res_model': self._name + }, context=context) + # TDE: temp, must check followers widget + return [] - def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None): - """ Unsubscribe the user (or user_ids) from the current document. + def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None): + """ Wrapper on message_subscribe, using users. If user_ids is not + provided, unsubscribe uid instead. """ + # isinstance: because using message_subscribe_users called in a view set the context as user_ids + if not user_ids or isinstance(user_ids, dict): user_ids = [uid] + partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)] + return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context) - :param user_ids: a list of user_ids; if not set, subscribe - uid instead + def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None): + """ Remove partners from the records followers. + + :param partner_ids: a list of partner_ids to unsubscribe :param return: new value of followers, for Chatter """ - partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id - self.write(cr, uid, ids, [(3, partner_id)], context=context) - - # FP Note: do we need this ? - return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids] + self.write(cr, uid, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context) + # return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids] + # TDE: temp, must check followers widget + return [] #------------------------------------------------------ # Notification API diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index d233235cd00..7b6723489ed 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -66,10 +66,12 @@ openerp_mail_followers = function(session, mail) { return; } if (this.getParent().fields.message_is_follower === undefined) { - // TDE: raise a warning + // TDE: TMP, need to change all form views + this.message_is_follower = false; + } + else { + this.message_is_follower = this.getParent().fields.message_is_follower.get_value(); } - this.message_is_follower = this.getParent().fields.message_is_follower.get_value(); - console.log(this.message_is_follower); return this.fetch_followers(value_); }, From 9871b2dd370c61052844ed79bc3de20a12c03adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 22 Aug 2012 13:16:13 +0200 Subject: [PATCH 116/264] [FIX] Fixed last commit, about removing check_read/write/unlink/create: those methods were used, propagated the changes :) . bzr revid: tde@openerp.com-20120822111613-305j77rs4fphkjts --- openerp/addons/base/ir/ir_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_sequence.py b/openerp/addons/base/ir/ir_sequence.py index e3136b92b6e..8b03cd3f3b3 100644 --- a/openerp/addons/base/ir/ir_sequence.py +++ b/openerp/addons/base/ir/ir_sequence.py @@ -210,7 +210,7 @@ class ir_sequence(openerp.osv.osv.osv): def next_by_id(self, cr, uid, sequence_id, context=None): """ Draw an interpolated string using the specified sequence.""" - self.check_read(cr, uid) + self.check_access_rights(cr, uid, 'read') company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False] ids = self.search(cr, uid, ['&',('id','=', sequence_id),('company_id','in',company_ids)]) return self._next(cr, uid, ids, context) @@ -227,7 +227,7 @@ class ir_sequence(openerp.osv.osv.osv): sequence selection. A matching sequence for that specific company will get higher priority. """ - self.check_read(cr, uid) + self.check_access_rights(cr, uid, 'read') company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False] ids = self.search(cr, uid, ['&',('code','=', sequence_code),('company_id','in',company_ids)]) return self._next(cr, uid, ids, context) From 406e58399fd1444cd5b83862d6493f59fcb08790 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 22 Aug 2012 13:34:39 +0200 Subject: [PATCH 117/264] [FIX] mail: unify message_post calls, fix typos, various lint cleanup bzr revid: odo@openerp.com-20120822113439-aoy7v4njs6o34tu5 --- addons/account/account_bank_statement.py | 2 +- addons/account/account_invoice.py | 17 +++--- addons/base_calendar/crm_meeting.py | 4 +- addons/base_status/base_stage.py | 2 +- addons/base_status/base_state.py | 8 +-- addons/crm/crm_action_rule.py | 4 +- addons/crm/crm_lead.py | 3 +- addons/crm/crm_meeting.py | 2 +- addons/event/event.py | 14 ++--- addons/hr/hr.py | 2 +- addons/hr_holidays/hr_holidays.py | 4 +- addons/mail/mail_mail.py | 1 - addons/mail/mail_thread.py | 67 +++++++++++++--------- addons/mail/res_users.py | 6 +- addons/mail/wizard/mail_compose_message.py | 5 +- addons/plugin/plugin_handler.py | 6 +- addons/procurement/procurement.py | 2 +- addons/project_issue/project_issue.py | 12 ++-- addons/sale/sale.py | 4 +- 19 files changed, 87 insertions(+), 78 deletions(-) diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py index 911086a83e4..095d461ff3b 100644 --- a/addons/account/account_bank_statement.py +++ b/addons/account/account_bank_statement.py @@ -430,7 +430,7 @@ class account_bank_statement(osv.osv): 'name': st_number, 'balance_end_real': st.balance_end }, context=context) - self.message_post(cr, uid, [st.id], body=_('Statement %s is confirmed, journal items are created.') % (st_number,), context=context) + self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st_number,), context=context) return self.write(cr, uid, ids, {'state':'confirm'}, context=context) def button_cancel(self, cr, uid, ids, context=None): diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index bca1530abae..8b86429ea1b 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -1288,24 +1288,25 @@ class account_invoice(osv.osv): def _get_document_type(self, type): type_dict = { - 'out_invoice': 'Customer invoice', - 'in_invoice': 'Supplier invoice', - 'out_refund': 'Customer Refund', - 'in_refund': 'Supplier Refund', + # Translation markers will have no effect at runtime, only used to properly flag export + 'out_invoice': _('Customer invoice'), + 'in_invoice': _('Supplier invoice'), + 'out_refund': _('Customer Refund'), + 'in_refund': _('Supplier Refund'), } return type_dict.get(type, 'Invoice') def create_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id],body=_("%s created.") % (self._get_document_type(obj.type)), context=context) + self.message_post(cr, uid, [obj.id], body=_("%s created.") % (_(self._get_document_type(obj.type))), context=context) def confirm_paid_send_note(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("%s paid.") % (self._get_document_type(obj.type)), context=context) + for obj in self.browse(cr, uid, ids, context=context): + self.message_post(cr, uid, [obj.id], body=_("%s paid.") % (_(self._get_document_type(obj.type))), context=context) def invoice_cancel_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("%s cancelled.") % (self._get_document_type(obj.type)), context=context) + self.message_post(cr, uid, [obj.id], body=_("%s cancelled.") % (_(self._get_document_type(obj.type))), context=context) account_invoice() diff --git a/addons/base_calendar/crm_meeting.py b/addons/base_calendar/crm_meeting.py index 4bf922a2797..5f58877a8a6 100644 --- a/addons/base_calendar/crm_meeting.py +++ b/addons/base_calendar/crm_meeting.py @@ -78,9 +78,9 @@ class crm_meeting(base_state, osv.Model): return 'Meeting' def case_open_send_note(self, cr, uid, ids, context=None): - return self.message_post(cr, uid, ids, body=_("Meeting has been confirmed."), context=context) + return self.message_post(cr, uid, ids, body=_("Meeting confirmed."), context=context) def case_close_send_note(self, cr, uid, ids, context=None): - return self.message_post(cr, uid, ids, body=_("Meeting has been done."), context=context) + return self.message_post(cr, uid, ids, body=_("Meeting completed."), context=context) diff --git a/addons/base_status/base_stage.py b/addons/base_status/base_stage.py index 6b834399dd9..26afe548217 100644 --- a/addons/base_status/base_stage.py +++ b/addons/base_status/base_stage.py @@ -428,5 +428,5 @@ class base_stage(object): msg = '%s has been escalated to %s.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context), new_section.name) else: msg = '%s has been escalated.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) - self.message_post(cr, uid, [id], 'System Notification', msg, context=context) + self.message_post(cr, uid, [id], body=msg, context=context) return True diff --git a/addons/base_status/base_state.py b/addons/base_status/base_state.py index 098e09210f9..cebf5537485 100644 --- a/addons/base_status/base_state.py +++ b/addons/base_status/base_state.py @@ -179,9 +179,9 @@ class base_state(object): # Notifications # ****************************** - def case_get_note_msg_prefix(self, cr, uid, id, context=None): - return '' - + def case_get_note_msg_prefix(self, cr, uid, id, context=None): + return '' + def case_open_send_note(self, cr, uid, ids, context=None): for id in ids: msg = _('%s has been opened.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) @@ -194,7 +194,7 @@ class base_state(object): msg = '%s has been escalated to %s.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context), new_section.name) else: msg = '%s has been escalated.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) - self.message_post(cr, uid, [id], 'System Notification', msg, context=context) + self.message_post(cr, uid, [id], body=msg, context=context) return True def case_close_send_note(self, cr, uid, ids, context=None): diff --git a/addons/crm/crm_action_rule.py b/addons/crm/crm_action_rule.py index 6753484dae0..36700b5b6c2 100644 --- a/addons/crm/crm_action_rule.py +++ b/addons/crm/crm_action_rule.py @@ -105,8 +105,8 @@ class base_action_rule(osv.osv): write['email_cc'] = obj.act_email_cc # Put state change by rule in communication history - if hasattr(obj, 'state') and hasattr(obj, 'message_append') and action.act_state: - model_obj.message_post(cr, uid, [obj], _(action.act_state)) + if hasattr(obj, 'state') and hasattr(obj, 'message_post') and action.act_state: + model_obj.message_post(cr, uid, [obj], _(action.act_state), context=context) model_obj.write(cr, uid, [obj.id], write, context) super(base_action_rule, self).do_action(cr, uid, action, model_obj, obj, context=context) diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index b1335fd91d4..39ad56e007d 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -19,7 +19,6 @@ # ############################################################################## -import binascii from base_status.base_stage import base_stage import crm from datetime import datetime @@ -476,7 +475,7 @@ class crm_lead(base_stage, osv.osv): subject = subject[0] + ", ".join(subject[1:]) details = "\n\n".join(details) - return self.message_post(cr, uid, [opportunity_id], subject=subject, body=details) + return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context) def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None): message = self.pool.get('mail.message') diff --git a/addons/crm/crm_meeting.py b/addons/crm/crm_meeting.py index de54d8e40ac..62fc53364ee 100644 --- a/addons/crm/crm_meeting.py +++ b/addons/crm/crm_meeting.py @@ -53,7 +53,7 @@ class crm_meeting(osv.Model): if meeting.opportunity_id: # meeting can be create from phonecalls or opportunities, therefore checking for the parent lead = meeting.opportunity_id message = _("Meeting linked to the opportunity %s has been created and scheduled on %s.") % (lead.name, meeting_date_tz) - lead.message_post(_('System Notification'), message) + lead.message_post(body=message) elif meeting.phonecall_id: phonecall = meeting.phonecall_id message = _("Meeting linked to the phonecall %s has been created and scheduled on %s.") % (phonecall.name, meeting_date_tz) diff --git a/addons/event/event.py b/addons/event/event.py index a057fe80ac4..d347c1547d8 100644 --- a/addons/event/event.py +++ b/addons/event/event.py @@ -19,10 +19,8 @@ # ############################################################################## -import time from osv import fields, osv from tools.translate import _ -import decimal_precision as dp from openerp import SUPERUSER_ID class event_type(osv.osv): @@ -53,7 +51,7 @@ class event_event(osv.osv): def name_get(self, cr, uid, ids, context=None): if not ids: - return [] + return [] res = [] for record in self.browse(cr, uid, ids, context=context): date = record.date_begin.split(" ")[0] @@ -99,7 +97,6 @@ class event_event(osv.osv): return self.write(cr, uid, ids, {'state': 'done'}, context=context) def check_registration_limits(self, cr, uid, ids, context=None): - register_pool = self.pool.get('event.registration') for self.event in self.browse(cr, uid, ids, context=context): total_confirmed = self.event.register_current if total_confirmed < self.event.register_min or total_confirmed > self.event.register_max and self.event.register_max!=0: @@ -109,7 +106,7 @@ class event_event(osv.osv): for event in self.browse(cr, uid, ids, context=context): available_seats = event.register_avail if available_seats and no_of_registration > available_seats: - raise osv.except_osv(_('Warning!'),_("Only %d Seats are Available!") % (available_seats)) + raise osv.except_osv(_('Warning!'),_("Only %d Seats are Available!") % (available_seats)) elif available_seats == 0: raise osv.except_osv(_('Warning!'),_("No Tickets Available!")) @@ -139,7 +136,6 @@ class event_event(osv.osv): @param context: A standard dictionary for contextual values @return: Dictionary of function fields value. """ - register_pool = self.pool.get('event.registration') res = {} for event in self.browse(cr, uid, ids, context=context): res[event.id] = {} @@ -334,7 +330,7 @@ class event_registration(osv.osv): return self.write(cr, uid, ids, {'state': 'draft'}, context=context) def confirm_registration(self, cr, uid, ids, context=None): - self.message_post(cr, uid, ids, body=_('State set to open')) + self.message_post(cr, uid, ids, body=_('State set to open'), context=context) return self.write(cr, uid, ids, {'state': 'open'}, context=context) def create(self, cr, uid, vals, context=None): @@ -364,13 +360,13 @@ class event_registration(osv.osv): if today >= registration.event_id.date_begin: values = {'state': 'done', 'date_closed': today} self.write(cr, uid, ids, values) - self.message_post(cr, uid, ids, body=_('State set to Done')) + self.message_post(cr, uid, ids, body=_('State set to Done'), context=context) else: raise osv.except_osv(_('Error!'),_("You must wait for the starting day of the event to do this action.") ) return True def button_reg_cancel(self, cr, uid, ids, context=None, *args): - self.message_post(cr, uid, ids,body = _('State set to Cancel')) + self.message_post(cr, uid, ids, body=_('State set to Cancel'), context=context) return self.write(cr, uid, ids, {'state': 'cancel'}) def mail_user(self, cr, uid, ids, context=None): diff --git a/addons/hr/hr.py b/addons/hr/hr.py index 54560863dd0..3c307b9fee8 100644 --- a/addons/hr/hr.py +++ b/addons/hr/hr.py @@ -212,7 +212,7 @@ class hr_employee(osv.osv): try: (model, mail_group_id) = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'group_all_employees') employee = self.browse(cr, uid, employee_id, context=context) - self.pool.get('mail.group').message_post(cr, uid, [mail_group_id], body='Welcome to %s! Please help him make its first steps in OpenERP!' % (employee.name), context=context) + self.pool.get('mail.group').message_post(cr, uid, [mail_group_id], body='Welcome to %s! Please help them take the first steps with OpenERP!' % (employee.name), context=context) except: pass # group deleted: do not push a message return employee_id diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 5748da37c1e..2e26e72bcdc 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -367,14 +367,14 @@ class hr_holidays(osv.osv): def create_notificate(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): self.message_post(cr, uid, ids, - _("The %s request has been created and is waiting confirmation.") + _("The %s request has been created and is waiting for confirmation.") % ('leave' if obj.type == 'remove' else 'allocation',), context=context) return True def holidays_confirm_notificate(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids): self.message_post(cr, uid, [obj.id], _("The %s request has been confirmed and is waiting for validation by the manager.") - % ('leave' if obj.type == 'remove' else 'allocation',)) + % ('leave' if obj.type == 'remove' else 'allocation',), context=context) def holidays_validate_notificate(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids): diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index a016586347f..bd0bfbf33c0 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -45,7 +45,6 @@ class mail_mail(osv.Model): _columns = { 'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade'), 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1), - 'subject': fields.char('Subject', size=128), 'state': fields.selection([ ('outgoing', 'Outgoing'), ('sent', 'Sent'), diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 3bc45d3af26..5795fa8b824 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -19,7 +19,6 @@ # ############################################################################## -import base64 import dateutil import email import logging @@ -27,7 +26,6 @@ from email.utils import parsedate from email.message import Message from osv import osv, fields from mail_message import decode -import re import time import tools from tools.translate import _ @@ -313,7 +311,7 @@ class mail_thread(osv.Model): model_pool.message_update(cr, user_id, [thread_id], msg, context=context) else: thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context) - self.message_post(cr, uid, thread_id, context=context, **msg) + self.message_post(cr, uid, [thread_id], msg_txt['body'], context=context, **msg) return True def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): @@ -421,7 +419,7 @@ class mail_thread(osv.Model): if save_original: msg_original = message.as_string() if isinstance(message, Message) \ else message - attachments.append(('email.eml', msg_original)) + attachments.append(('original_email.eml', msg_original)) if not message_id: # Very unusual situation, be we should be fault-tolerant here @@ -451,11 +449,11 @@ class mail_thread(osv.Model): msg['partner_ids'] = partner_ids if 'Date' in msg_fields: - date_hdr = decode(msg_txt.get('Date')) - # convert from email timezone to server timezone - date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone())) - date_server_datetime_str = date_server_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) - msg['date'] = date_server_datetime_str + date_hdr = decode(msg_txt.get('Date')) + # convert from email timezone to server timezone + date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone())) + date_server_datetime_str = date_server_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) + msg['date'] = date_server_datetime_str #if 'Content-Transfer-Encoding' in msg_fields: # msg['encoding'] = msg_txt.get('Content-Transfer-Encoding') @@ -507,38 +505,53 @@ class mail_thread(osv.Model): module instead of by the res.log mechanism. Please \ use the mail.thread OpenChatter API instead of the \ now deprecated res.log.") - self.message_post(cr, uid, id, message, context=context) + self.message_post(cr, uid, [id], message, context=context) - def message_post(self, cr, uid, res_id, body, subject=False, - mtype='notification', parent_id=False, attachments=None, context=None, **kwargs): + def message_post(self, cr, uid, thread_id, body, subject=False, + msg_type='notification', parent_id=False, attachments=None, context=None, **kwargs): + """Post a new message in an existing message thread, returning the new mail.message ID. + Extra keyword arguments will be used as default column values for the new + mail.message record. + + :param int thread_id: thread ID to post into, or list with one ID + :param str body: body of the message, usually raw HTML + :param str subject: optional subject + :param str msg_type: message type, out of the possible values for mail_message.type, + currently one of ``email, 'comment', 'notification'``. + :param int parent_id: optional ID of parent message in this thread + :param tuple(str,str) attachments: list of attachment tuples in the form ``(name,content)`` + :return: ID of newly created mail.message + """ context = context or {} attachments = attachments or {} - if type(res_id) in (list, tuple): - res_id = res_id and res_id[0] or False + assert (not thread_id) or isinstance(thread_id, (int,long)) or \ + (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id" + if isinstance(thread_id, (list, tuple)): + thread_id = thread_id and thread_id[0] to_attach = [] - for fname, fcontent in attachments: - if isinstance(fcontent, unicode): - fcontent = fcontent.encode('utf-8') + for name, content in attachments: + if isinstance(content, unicode): + content = content.encode('utf-8') data_attach = { - 'name': fname, - 'datas': fcontent, - 'datas_fname': fname, - 'description': _('email attachment'), + 'name': name, + 'datas': content, + 'datas_fname': name, + 'description': name, } to_attach.append((0,0, data_attach)) - value = kwargs - value.update( { - 'model': res_id and self._name or False, - 'res_id': res_id, + values = kwargs + values.update( { + 'model': thread_id and self._name or False, + 'thread_id': thread_id or False, 'body': body, 'subject': subject, - 'type': mtype, + 'type': msg_type, 'parent_id': parent_id, 'attachment_ids': to_attach }) - return self.pool.get('mail.message').create(cr, uid, value, context=context) + return self.pool.get('mail.message').create(cr, uid, values, context=context) #------------------------------------------------------ diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py index 1e638251e40..d863719db77 100644 --- a/addons/mail/res_users.py +++ b/addons/mail/res_users.py @@ -110,9 +110,9 @@ class res_users(osv.Model): def _create_welcome_message(self, cr, uid, user, context=None): company_name = user.company_id.name if user.company_id else _('the company') - body = '''%s has joined %s.''' % (user.name, company_name) - # TODO change 1 into user.id but catch errors - return self.pool.get('res.partner').message_post(cr, 1, [user.partner_id.id], + body = _('%s has joined %s.') % (user.name, company_name) + # TODO change SUPERUSER_ID into user.id but catch errors + return self.pool.get('res.partner').message_post(cr, SUPERUSER_ID, [user.partner_id.id], body=body, context=context) def write(self, cr, uid, ids, vals, context=None): diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 50ffd6e440e..19faaad9578 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -254,9 +254,9 @@ class mail_compose_message(osv.TransientModel): context = {} formatting = context.get('formatting') + # FIXME TODO: mass_mail_mode unused? mass_mail_mode = context.get('mail.compose.message.mode') == 'mass_mail' - mail_message_obj = self.pool.get('mail.message') for mail_wiz in self.browse(cr, uid, ids, context=context): attachment = {} for attach in mail_wiz.attachment_ids: @@ -264,6 +264,7 @@ class mail_compose_message(osv.TransientModel): # default values, according to the wizard options subject = mail_wiz.subject if formatting else False + # FIXME TODO: partner_ids not used?? partner_ids = [partner.id for partner in mail_wiz.dest_partner_ids] body = mail_wiz.body_html if mail_wiz.content_subtype == 'html' else mail_wiz.body @@ -274,7 +275,7 @@ class mail_compose_message(osv.TransientModel): active_model_pool = self.pool.get(active_model) subject = self.render_template(cr, uid, subject, active_model, active_id) body = self.render_template(cr, uid, mail_wiz.body_html, active_model, active_id) - active_model_pool.message_post(cr, uid, active_id, body, subject, 'comment', + active_model_pool.message_post(cr, uid, [active_id], body=body, subject=subject, msg_type='comment', attachments=attachment, context=context) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/plugin/plugin_handler.py b/addons/plugin/plugin_handler.py index 6d6262ddd22..9c582a8e3bf 100644 --- a/addons/plugin/plugin_handler.py +++ b/addons/plugin/plugin_handler.py @@ -101,15 +101,15 @@ class plugin_handler(osv.osv_memory): notify = "Email already pushed" elif res_id == 0: if model == 'res.partner': - notify = 'User the button Partner to create a new partner' + notify = 'User the Partner button to create a new partner' else: res_id = model_obj.message_new(cr, uid, msg) - notify = "Mail succefully pushed, a new %s has been created " % model + notify = "Mail succesfully pushed, a new %s has been created " % model else: if model == 'res.partner': model_obj = self.pool.get('mail.thread') model_obj.message_post(cr, uid, [res_id], body=msg) - notify = "Mail succefully pushed" + notify = "Mail succesfully pushed" url = self._make_url(cr, uid, res_id, model) return (model, res_id, url, notify) diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index 8efd09a1efc..db50ebd94fd 100644 --- a/addons/procurement/procurement.py +++ b/addons/procurement/procurement.py @@ -389,7 +389,7 @@ class procurement_order(osv.osv): ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id]) order_point_id = self.pool.get('stock.warehouse.orderpoint').search(cr, uid, [('product_id', '=', procurement.product_id.id)], context=context) if not order_point_id and not ok: - message = _("Not enough stock and no minimum orderpoint rule defined.") + message = _("Not enough stock and no minimum orderpoint rule defined.") elif not order_point_id: message = _("No minimum orderpoint rule defined.") elif not ok: diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 3b0311d8f45..93040b20af5 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -507,21 +507,21 @@ class project_issue(base_stage, osv.osv): return 'Project issue' def convert_to_task_send_note(self, cr, uid, ids, context=None): - message = _("Project issue has been converted into task.") + message = _("Project issue converted to task.") return self.message_post(cr, uid, ids, body=message, context=context) def create_send_note(self, cr, uid, ids, context=None): - message = _("Project issue has been created.") + message = _("Project issue created.") return self.message_post(cr, uid, ids, body=message, context=context) def case_escalate_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): if obj.project_id: - message = _("has been escalated to '%s'.") % (obj.project_id.name) - obj.message_post(body=message, context=context) + message = _("escalated to '%s'.") % (obj.project_id.name) + obj.message_post(body=message) else: - message = _("has been escalated.") - obj.message_post(body=message, context=context) + message = _("escalated.") + obj.message_post(body=message) return True project_issue() diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 476ee6458f0..521d7dc504f 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -1027,7 +1027,7 @@ class sale_order(osv.osv): def create_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("Quotation for %s has been created.") % (obj.partner_id.name), context=context) + self.message_post(cr, uid, [obj.id], body=_("Quotation for %s created.") % (obj.partner_id.name), context=context) def confirm_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): @@ -1058,7 +1058,7 @@ class sale_order(osv.osv): self.message_post(cr, uid, [order.id], body=_("Draft Invoice of %s %s waiting for validation.") % (invoice.amount_total, invoice.currency_id.symbol), context=context) def action_cancel_draft_send_note(self, cr, uid, ids, context=None): - return self.message_post(cr, uid, ids, body='Sale order has been set in draft.', context=context) + return self.message_post(cr, uid, ids, body=_('Sale order set to draft.'), context=context) sale_order() From 8b7812721078b0331c90081c77fdcd6f7d4d187e Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 22 Aug 2012 14:38:58 +0200 Subject: [PATCH 118/264] [FIX] mail: typo bzr revid: odo@openerp.com-20120822123858-e3alagahniwbdpgz --- addons/mail/mail_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 5795fa8b824..61c8f611b18 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -311,7 +311,7 @@ class mail_thread(osv.Model): model_pool.message_update(cr, user_id, [thread_id], msg, context=context) else: thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context) - self.message_post(cr, uid, [thread_id], msg_txt['body'], context=context, **msg) + self.message_post(cr, uid, [thread_id], context=context, **msg) return True def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): From b873d36b9098ad2e06b6b14db691871ddc8c1371 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 22 Aug 2012 14:43:07 +0200 Subject: [PATCH 119/264] [FIX] mail: more typos bzr revid: odo@openerp.com-20120822124307-qnwx6nk66msz01jx --- addons/mail/res_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py index d863719db77..86fd048479c 100644 --- a/addons/mail/res_users.py +++ b/addons/mail/res_users.py @@ -105,7 +105,7 @@ class res_users(osv.Model): # alias mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context) # create a welcome message - self._create_welcome_message(cr, uid, user_id, context=context) + self._create_welcome_message(cr, uid, user, context=context) return user_id def _create_welcome_message(self, cr, uid, user, context=None): From a001d892431b8efabef6118e67e828aa1849c2da Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 22 Aug 2012 14:49:43 +0200 Subject: [PATCH 120/264] [FIX] mail: more typos bzr revid: odo@openerp.com-20120822124943-2ixd2vbbug38tfb2 --- addons/mail/mail_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 61c8f611b18..c8e5ba3ffa1 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -544,7 +544,7 @@ class mail_thread(osv.Model): values = kwargs values.update( { 'model': thread_id and self._name or False, - 'thread_id': thread_id or False, + 'res_id': thread_id or False, 'body': body, 'subject': subject, 'type': msg_type, From 786419565ab19523233856a0fcce26de58db3b3c Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 22 Aug 2012 15:30:28 +0200 Subject: [PATCH 121/264] [FIX] mail: make basic mail tests pass again, remove useless/dead test code bzr revid: odo@openerp.com-20120822133028-hms3xfhibhn1j38l --- addons/mail/mail_thread.py | 3 +- addons/mail/tests/test_mail.py | 15 ++++--- addons/mail/tests/write_test.py | 79 --------------------------------- 3 files changed, 9 insertions(+), 88 deletions(-) delete mode 100644 addons/mail/tests/write_test.py diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index c8e5ba3ffa1..1be8dfdbf0c 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -297,7 +297,6 @@ class mail_thread(osv.Model): thread_id, custom_values, context=context) msg = self.parse_message(cr, uid, msg_txt, save_original=save_original, context=context) - msg['state'] = 'received' if strip_attachments and 'attachments' in msg: del msg['attachments'] for model, thread_id, custom_values, user_id in routes: @@ -543,7 +542,7 @@ class mail_thread(osv.Model): values = kwargs values.update( { - 'model': thread_id and self._name or False, + 'model': thread_id and context.get('thread_model', self._name) or False, 'res_id': thread_id or False, 'body': body, 'subject': subject, diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py index 0940d72c490..a9da19e90fa 100644 --- a/addons/mail/tests/test_mail.py +++ b/addons/mail/tests/test_mail.py @@ -82,26 +82,27 @@ class test_mail(common.TransactionCase): self.group_tech_id = self.mail_group.create(self.cr, self.uid, {'name': 'tech'}) def test_message_process(self): + cr, uid = self.cr, self.uid # Incoming mail creates a new mail_group "frogs" - self.assertEqual(self.mail_group.search(self.cr, self.uid, [('name','=','frogs')]), []) + self.assertEqual(self.mail_group.search(cr, uid, [('name','=','frogs')]), []) mail_frogs = MAIL_TEMPLATE.format(to='groups@example.com, other@gmail.com', subject='frogs', extra='') - self.mail_thread.message_process(self.cr, self.uid, None, mail_frogs) - frog_groups = self.mail_group.search(self.cr, self.uid, [('name','=','frogs')]) + self.mail_thread.message_process(cr, uid, None, mail_frogs) + frog_groups = self.mail_group.search(cr, uid, [('name','=','frogs')]) self.assertTrue(len(frog_groups) == 1) # Previously-created group can be emailed now - it should have an implicit alias group+frogs@... - frog_group = self.mail_group.browse(self.cr, self.uid, frog_groups[0]) + frog_group = self.mail_group.browse(cr, uid, frog_groups[0]) group_messages = frog_group.message_ids self.assertTrue(len(group_messages) == 1, 'New group should only have the original message') mail_frog_news = MAIL_TEMPLATE.format(to='Friendly Frogs ', subject='news', extra='') - self.mail_thread.message_process(self.cr, self.uid, None, mail_frog_news) + self.mail_thread.message_process(cr, uid, None, mail_frog_news) frog_group.refresh() self.assertTrue(len(frog_group.message_ids) == 2, 'Group should contain 2 messages now') # Even with a wrong destination, a reply should end up in the correct thread mail_reply = MAIL_TEMPLATE.format(to='erroneous@example.com>', subject='Re: news', extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n'%frog_group.id) - self.mail_thread.message_process(self.cr, self.uid, None, mail_reply) + self.mail_thread.message_process(cr, uid, None, mail_reply) frog_group.refresh() self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now') @@ -109,4 +110,4 @@ class test_mail(common.TransactionCase): mail_spam = MAIL_TEMPLATE.format(to='noone@example.com', subject='spam', extra='') self.assertRaises(Exception, self.mail_thread.message_process, - self.cr, self.uid, None, mail_spam) + cr, uid, None, mail_spam) diff --git a/addons/mail/tests/write_test.py b/addons/mail/tests/write_test.py deleted file mode 100644 index 920fbb92dbe..00000000000 --- a/addons/mail/tests/write_test.py +++ /dev/null @@ -1,79 +0,0 @@ -############################################################################## -# -# Copyright (c) 2004 TINY SPRL. (http://tiny.be) All Rights Reserved. -# Fabien Pinckaers -# -# WARNING: This program as such is intended to be used by professional -# programmers who take the whole responsability of assessing all potential -# consequences resulting from its eventual inadequacies and bugs -# End users who are looking for a ready-to-use solution with commercial -# garantees and support are strongly adviced to contract a Free Software -# Service Company -# -# This program is Free Software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -############################################################################## - -# -# This module test all RPC methods -# - -import xmlrpclib - -uid = 1 -passwd='admin' -server = 'localhost' -db = 'trunk' - -sock = xmlrpclib.ServerProxy('http://'+server+':8069/xmlrpc/object') - -def _print_data(data, level=0): - for d in data: - print ' '*level, d['id'] - _print_data(d['child_ids'], level+1) - -print 'With Domain', [('model','=','mail.group')], 'thread_level' -data = sock.execute(db, uid, passwd, 'mail.message', 'message_read', False, [('model','=','mail.group')], 1) -_print_data(data) - -print 'With Domain', [('model','=','mail.group')], 'no thread_level' -data = sock.execute(db, uid, passwd, 'mail.message', 'message_read', False, [('model','=','mail.group')], 0) -_print_data(data) - - -print 'With Domain', [('model','=','mail.group'), ('parent_id','=',False)], 'thread_level=0' -data = sock.execute(db, uid, passwd, 'mail.message', 'message_read', False, [('model','=','mail.group'), ('parent_id','=',False)], 0) -_print_data(data) - -print 'With Domain', [('model','=','mail.group'), ('parent_id','=',False)], 'thread_level=2' -data = sock.execute(db, uid, passwd, 'mail.message', 'message_read', False, [('model','=','mail.group'), ('parent_id','=',False)], 2) -_print_data(data) - -print 'Fixed IDS', [2,3,41,43], 'thread_level' -data = sock.execute(db, uid, passwd, 'mail.message', 'message_read', [2,3,41,43], [], 1) -_print_data(data) - -print 'Fixed IDS', [2,43], 'no thread_level' -data = sock.execute(db, uid, passwd, 'mail.message', 'message_read', [2,43], [], 0) -_print_data(data) - -print 'Fixed IDS', [2,43], 'thread_level' -data = sock.execute(db, uid, passwd, 'mail.message', 'message_read', [2,43], [], 1) -_print_data(data) - -print 'domain [id in 3,41]', 'thread_level' -data = sock.execute(db, uid, passwd, 'mail.message', 'message_read', False, [('id','in',[3,41])], 1) -_print_data(data) - From 0f63b4a1fb8a41971d1fd6a9810a75a7af5de759 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 22 Aug 2012 15:34:06 +0200 Subject: [PATCH 122/264] [IMP] need action bzr revid: fp@tinyerp.com-20120822133406-9ayg228mxfeg16dg --- openerp/addons/base/ir/ir_actions.py | 4 +++- openerp/addons/base/ir/ir_ui_menu.py | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index 28a309f427a..b70a9d89c06 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -43,7 +43,6 @@ class actions(osv.osv): _table = 'ir_actions' _order = 'name' _columns = { - 'name': fields.char('Action Name', required=True, size=64), 'type': fields.char('Action Type', required=True, size=32,readonly=True), 'usage': fields.char('Action Usage', size=32), } @@ -872,10 +871,13 @@ class act_client(osv.osv): self.write(cr, uid, id, {'params_store': field_value}, context=context) _columns = { + 'name': fields.char('Action Name', required=True, size=64, translate=True), 'tag': fields.char('Client action tag', size=64, required=True, help="An arbitrary string, interpreted by the client" " according to its own needs and wishes. There " "is no central tag repository across clients."), + 'res_model': fields.char('Destination Model', size=64, + help="Optional model, mostly used for needactions."), 'params': fields.function(_get_params, fnct_inv=_set_params, type='binary', string="Supplementary arguments", diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index f5e4ee2ba05..bc743d34fc0 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -264,12 +264,10 @@ class ir_ui_menu(osv.osv): res[menu.id]['needaction_enabled'] = False res[menu.id]['needaction_counter'] = False res[menu.id] = {} - if menu.action and menu.action.type == 'ir.actions.act_window' and menu.action.res_model: + if menu.action and menu.action.type in ('ir.actions.act_window','ir.actions.client') and menu.action.res_model: obj = self.pool.get(menu.action.res_model) if obj._needaction: res[menu.id]['needaction_enabled'] = obj._needaction - # check domain and context: should we evaluate the domain ? - # and add context of the action ? res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, menu.action.domain, context=context) return res From 4fdb70d12ab10728034e8077e9bba12d4b8a3a8a Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 22 Aug 2012 15:37:23 +0200 Subject: [PATCH 123/264] fix bzr revid: fp@tinyerp.com-20120822133723-k5r76cty7dej5kjh --- addons/mail/mail_group.py | 3 +-- addons/mail/mail_message.py | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index faa4bb1c9b4..1824f59e0b9 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -132,8 +132,7 @@ class mail_group(osv.Model): params = { 'search_view_id': search_ref and search_ref[1] or False, 'domain': [('model','=','mail.group'),('res_id','=',mail_group_id)], - 'res_model': 'mail.group', - 'res_id': mail_group_id, + 'res_model': 'mail.message', 'thread_level': 2 } cobj = self.pool.get('ir.actions.client') diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index b74df2f8594..b9cb0723a4f 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -109,6 +109,13 @@ class mail_message(osv.Model): 'body': fields.html('Content'), } + def _needaction_domain_get(self, cr, uid, context={}): + if self._needaction: + partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id + return [('notification_ids.partner_id','=',partner_id),('notification_ids.read','=',False)] + return [] + + def _get_default_author(self, cr, uid, context={}): return self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id From 9d6ee746eb4e29ea7dca76ef8c836d71374d6186 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 22 Aug 2012 15:41:09 +0200 Subject: [PATCH 124/264] fix bzr revid: fp@tinyerp.com-20120822134109-97lbhcllylbe15ce --- openerp/addons/base/ir/ir_actions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index b70a9d89c06..cf67e6fcf88 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -43,6 +43,7 @@ class actions(osv.osv): _table = 'ir_actions' _order = 'name' _columns = { + 'name': fields.char('Name', size=64, required=True), 'type': fields.char('Action Type', required=True, size=32,readonly=True), 'usage': fields.char('Action Usage', size=32), } From b531d9cff1054165bf8cc5cfe37304187d0924e6 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 22 Aug 2012 17:31:45 +0200 Subject: [PATCH 125/264] [IMP] needaction after mail.thread bzr revid: fp@tinyerp.com-20120822153145-g7xopqq7oje7y60c --- addons/base_calendar/crm_meeting.py | 2 +- addons/crm/crm_lead.py | 2 +- addons/event/event.py | 2 +- addons/hr_holidays/hr_holidays.py | 2 +- addons/hr_recruitment/hr_recruitment.py | 2 +- addons/mrp/mrp.py | 2 +- addons/project/project.py | 4 ++-- addons/project_issue/project_issue.py | 2 +- addons/purchase/purchase.py | 2 +- addons/purchase_requisition/purchase_requisition.py | 2 +- addons/sale/sale.py | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/addons/base_calendar/crm_meeting.py b/addons/base_calendar/crm_meeting.py index 5f58877a8a6..e1c66148ae7 100644 --- a/addons/base_calendar/crm_meeting.py +++ b/addons/base_calendar/crm_meeting.py @@ -43,7 +43,7 @@ class crm_meeting(base_state, osv.Model): _name = 'crm.meeting' _description = "Meeting" _order = "id desc" - _inherit = ["calendar.event", 'ir.needaction_mixin', "mail.thread"] + _inherit = ["calendar.event", "mail.thread", 'ir.needaction_mixin'] _columns = { # base_state required fields 'create_date': fields.datetime('Creation Date', readonly=True), diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 39ad56e007d..83ffb379950 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -38,7 +38,7 @@ class crm_lead(base_stage, osv.osv): _name = "crm.lead" _description = "Lead/Opportunity" _order = "priority,date_action,id desc" - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread','ir.needaction_mixin'] def _get_default_section_id(self, cr, uid, context=None): """ Gives default section by checking if present in the context """ diff --git a/addons/event/event.py b/addons/event/event.py index d347c1547d8..763aa423378 100644 --- a/addons/event/event.py +++ b/addons/event/event.py @@ -47,7 +47,7 @@ class event_event(osv.osv): _name = 'event.event' _description = __doc__ _order = 'date_begin' - _inherit = ['ir.needaction_mixin','mail.thread'] + _inherit = ['mail.thread','ir.needaction_mixin'] def name_get(self, cr, uid, ids, context=None): if not ids: diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 2e26e72bcdc..4bc9dabba71 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -94,7 +94,7 @@ class hr_holidays(osv.osv): _name = "hr.holidays" _description = "Leave" _order = "type desc, date_from asc" - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = [ 'mail.thread','ir.needaction_mixin'] def _employee_get(self, cr, uid, context=None): ids = self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context) diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py index 910c4b89dd2..db5fd57b0a9 100644 --- a/addons/hr_recruitment/hr_recruitment.py +++ b/addons/hr_recruitment/hr_recruitment.py @@ -95,7 +95,7 @@ class hr_applicant(base_stage, osv.Model): _name = "hr.applicant" _description = "Applicant" _order = "id desc" - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread', 'ir.needaction_mixin'] def _get_default_department_id(self, cr, uid, context=None): """ Gives default department by checking if present in the context """ diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index 76f7c7b9f34..c667daadbec 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -407,7 +407,7 @@ class mrp_production(osv.osv): _name = 'mrp.production' _description = 'Manufacturing Order' _date_name = 'date_planned' - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread', 'ir.needaction_mixin'] def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None): """ Calculates total hours and total no. of cycles for a production order. diff --git a/addons/project/project.py b/addons/project/project.py index 5fe7f31fa57..c998cd368d0 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -66,7 +66,7 @@ class project(osv.osv): _description = "Project" _inherits = {'account.analytic.account': "analytic_account_id", "mail.alias": "alias_id"} - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread', 'ir.needaction_mixin'] def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False): if user == 1: @@ -544,7 +544,7 @@ class task(base_stage, osv.osv): _name = "project.task" _description = "Task" _date_name = "date_start" - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread', 'ir.needaction_mixin'] def _get_default_project_id(self, cr, uid, context=None): """ Gives default section by checking if present in the context """ diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 93040b20af5..49d62ae9ba7 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -46,7 +46,7 @@ class project_issue(base_stage, osv.osv): _name = "project.issue" _description = "Project Issue" _order = "priority, create_date desc" - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread', 'ir.needaction_mixin'] def _get_default_project_id(self, cr, uid, context=None): """ Gives default project by checking if present in the context """ diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 0697ccb339a..b1a5631e59e 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -220,7 +220,7 @@ class purchase_order(osv.osv): ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'), ] _name = "purchase.order" - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Purchase Order" _order = "name desc" diff --git a/addons/purchase_requisition/purchase_requisition.py b/addons/purchase_requisition/purchase_requisition.py index bbd011a53a8..f8d7741822e 100644 --- a/addons/purchase_requisition/purchase_requisition.py +++ b/addons/purchase_requisition/purchase_requisition.py @@ -31,7 +31,7 @@ import decimal_precision as dp class purchase_requisition(osv.osv): _name = "purchase.requisition" _description="Purchase Requisition" - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread', 'ir.needaction_mixin'] _columns = { 'name': fields.char('Requisition Reference', size=32,required=True), 'origin': fields.char('Source', size=32), diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 521d7dc504f..6a6678d6163 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -48,7 +48,7 @@ sale_shop() class sale_order(osv.osv): _name = "sale.order" - _inherit = ['ir.needaction_mixin', 'mail.thread'] + _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Sales Order" From f6b6fb4debcd02918c8a4b172227e6331d38fdc5 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 22 Aug 2012 17:59:54 +0200 Subject: [PATCH 126/264] [FIX] needaction ok for crm bzr revid: fp@tinyerp.com-20120822155954-8ix4op6hj5e5hzi5 --- addons/mail/mail_thread.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 1be8dfdbf0c..f7078348f08 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -87,8 +87,18 @@ class mail_thread(osv.Model): return res # FP Note: todo - def _search_unread(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None): - return [] + def _search_unread(self, cr, uid, obj=None, name=None, domain=None, context=None): + partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id + res = {} + notif_obj = self.pool.get('mail.notification') + notif_ids = notif_obj.search(cr, uid, [ + ('partner_id', '=', partner_id), + ('message_id.model', '=', self._name), + ('read', '=', False) + ], context=context) + for notif in notif_obj.browse(cr, uid, notif_ids, context=context): + res[notif.message_id.res_id] = True + return [('id','in',res.keys())] _columns = { 'message_is_follower': fields.function(_get_message_data, From 9c3e45579958d2dcfd02990d6cc6858c46f1bc1b Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 22 Aug 2012 18:13:15 +0200 Subject: [PATCH 127/264] [FIX] bzr revid: fp@tinyerp.com-20120822161315-n1uwcxevdo18elkb --- addons/mail/mail_thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index f7078348f08..6867c0e08b1 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -86,7 +86,6 @@ class mail_thread(osv.Model): res[thread.id]['message_is_follower'] = user.partner_id.id in [follower.id for follower in thread.message_follower_ids] return res - # FP Note: todo def _search_unread(self, cr, uid, obj=None, name=None, domain=None, context=None): partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id res = {} @@ -103,8 +102,8 @@ class mail_thread(osv.Model): _columns = { 'message_is_follower': fields.function(_get_message_data, type='boolean', string='Is a Follower', multi='_get_message_data'), + # missing domain on model 'message_follower_ids': fields.many2many('res.partner', 'mail_followers', 'res_id', 'partner_id', - domain=lambda self: [('res_model','=',self._name)], string='Followers'), 'message_ids': fields.one2many('mail.message', 'res_id', domain=lambda self: [('model','=',self._name)], @@ -112,6 +111,7 @@ class mail_thread(osv.Model): help="All messages related to the current document."), 'message_unread': fields.function(_get_message_data, fnct_search=_search_unread, string='Has Unread Messages', + type='boolean', help="When checked, new messages require your attention.", multi="_get_message_data"), 'message_summary': fields.function(_get_message_data, method=True, From 552705494c11c53d81671766e608f830e773df96 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Wed, 22 Aug 2012 18:14:07 +0200 Subject: [PATCH 128/264] [IMP] needaction bzr revid: fp@tinyerp.com-20120822161407-aucl1m2jsa3pe6sv --- openerp/addons/base/ir/ir_needaction.py | 3 ++- openerp/addons/base/ir/ir_ui_menu.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index caf099c18cd..1e59a6fbb8e 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -63,5 +63,6 @@ class ir_needaction_mixin(osv.Model): dom = self._needaction_domain_get(cr, uid, context=context) if dom is False: return 0 - return self.search(cr, uid, (domain or []) +dom, context=context, count=True) + result = self.search(cr, uid, (domain or []) +dom, context=context, count=True) + return result diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index bc743d34fc0..eefa8ea4a50 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -24,6 +24,8 @@ import base64 import re import threading +from tools.safe_eval import safe_eval as eval + import tools import openerp.modules from osv import fields, osv @@ -268,7 +270,7 @@ class ir_ui_menu(osv.osv): obj = self.pool.get(menu.action.res_model) if obj._needaction: res[menu.id]['needaction_enabled'] = obj._needaction - res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, menu.action.domain, context=context) + res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, menu.action.domain and eval(menu.action.domain), context=context) return res _columns = { From 4e6e86fab535e217bbe548ee932bdcca1346e9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 22 Aug 2012 18:21:51 +0200 Subject: [PATCH 129/264] [IMP] [CLEAN] [WIP] composer: cleaning of the server-side code. It now relies less on context keys and values; some fields have been added to manage the composition mode, model, res_id and message_id as a classic form. Updated form view, with invisible fields, to have those value accessible and modifiable through onchange or through JS. Updated JS-part of the composer to handle the new composer; less options, less logic client-side ! Still not finished, short in time today (have to check mass_mail, templates). bzr revid: tde@openerp.com-20120822162151-n9o23ik0v45h7v6j --- addons/mail/static/src/js/mail.js | 157 +++++----- addons/mail/static/src/xml/mail.xml | 8 +- addons/mail/wizard/mail_compose_message.py | 275 ++++++++---------- .../mail/wizard/mail_compose_message_view.xml | 11 +- 4 files changed, 201 insertions(+), 250 deletions(-) diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 409ff41fcf5..fb788e305cd 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -104,87 +104,60 @@ openerp.mail = function(session) { * @param {Object} [options] * @param {String} [options.res_model] res_model of document [REQUIRED] * @param {Number} [options.res_id] res_id of record [REQUIRED] - * @param {Number} [options.formatting] true/false, tells whether - * we are in advance formatting mode - * @param {String} [options.model] mail.compose.message.mode (see - * composition wizard) + * @param {String} [options.composition_mode] mail.compose.message.mode + * (see composition wizard) * @param {Number} [options.msg_id] id of a message in case we are in * reply mode */ - init: function(parent, options) { + init: function (parent, options) { var self = this; this._super(parent); // options this.options = options || {}; - this.options.context = options.context || {}; - this.options.formatting = options.formatting || false; - this.options.mode = options.mode || 'comment'; + this.options.composition_mode = options.composition_mode || 'comment'; this.options.form_xml_id = options.form_xml_id || 'email_compose_message_wizard_form_chatter'; - this.options.form_view_id = false; - if (this.options.mode == 'reply') { - this.options.active_id = this.options.msg_id; - } else { - this.options.active_id = this.options.res_id; - } - this.formatting = false; + this.options.form_view_id = options.form_view_id || false; + this.options.context = options.context || {}; // debug console.groupCollapsed('New ComposeMessage: model', this.options.res_model, ', id', this.options.res_id); console.log('context:', this.options.context); console.groupEnd(); }, - /** - * Reinitialize the widget field values to the default values. The - * purpose is to avoid to destroy and re-build a form view. Default - * values are therefore given as for an onchange. */ - reinit: function() { - var self = this; - if (! this.form_view) return; - var call_defer = this.ds_compose.call('default_get', [['subject', 'body_text', 'body', 'attachment_ids', 'dest_partner_ids'], this.ds_compose.get_context()]).then( - function (result) { - self.form_view.on_processed_onchange({'value': result}, []); - }); - return call_defer; - }, - - /** - * Override-hack of do_action: clean the form */ - do_action: function(action, on_close) { - // this.init_comments(); - return this._super(action, on_close); - }, - - /** - * Widget start function - * - builds and initializes the form view */ - start: function() { - var self = this; + start: function () { this._super.apply(this, arguments); // customize display: add avatar, clean previous content var user_avatar = mail.ChatterUtils.get_image(this.session.prefix, this.session.session_id, 'res.users', 'image_small', this.session.uid); this.$element.find('img.oe_mail_icon').attr('src', user_avatar); this.$element.find('div.oe_mail_msg_content').empty(); - // create a context for the default_get of the compose form - var widget_context = { - 'active_model': this.options.res_model, - 'active_id': this.options.active_id, - 'mail.compose.message.mode': this.options.mode, - }; - var context = _.extend({}, this.options.context, widget_context); + // create a context for the dataset and default_get of the wizard + var context = this._update_context({}); + console.log(context); + // debugger this.ds_compose = new session.web.DataSetSearch(this, 'mail.compose.message', context); // find the id of the view to display in the chatter form var data_ds = new session.web.DataSetSearch(this, 'ir.model.data'); - var deferred_form_id = data_ds.call('get_object_reference', ['mail', this.options.form_xml_id]).then( function (result) { - if (result) { - self.options.form_view_id = result[1]; - } - }).pipe(this.proxy('create_form_view')); - return deferred_form_id; + return data_ds.call('get_object_reference', ['mail', this.options.form_xml_id]).pipe(this.proxy('create_form_view')); }, - /** - * Create a FormView, then append it to the to widget DOM. */ - create_form_view: function () { + /** Update the context of the compose wizard */ + _update_context: function (dest_context) { + _.extend(dest_context, this.options.context, { + 'default_model': this.options.res_model, + 'mail.compose.message.mode': this.options.composition_mode + }); + if (this.options.composition_mode == 'comment') { + _.extend(dest_context, {'default_res_id': this.options.res_id}); + } + else if (this.options.composition_mode == 'reply') { + _.extend(dest_context, {'active_id': this.options.msg_id}); + } + return dest_context + }, + + /** Create a FormView, then append it to the to widget DOM. */ + create_form_view: function (form_view_id) { + this.options.form_view_id = form_view_id[1] || false; var self = this; // destroy previous form_view if any if (this.form_view) { this.form_view.destroy(); } @@ -200,12 +173,31 @@ openerp.mail = function(session) { return $.when(this.form_view.appendTo(msg_node)).pipe(function() { self.bind_events(); self.form_view.do_show(); - if (self.options.formatting) { self.toggle_formatting_mode(); } }); }, - destroy: function() { - this._super.apply(this, arguments); + /** + * Reinitialize the widget field values to the default values. The + * purpose is to avoid to destroy and re-build a form view. Default + * values are therefore given as for an on_change. */ + refresh: function (options_update_values) { + var self = this; + // debugger + this.options = _.extend(this.options, options_update_values); + if (! this.form_view) return; + this.ds_compose.context = this._update_context(this.ds_compose.context); + return this.ds_compose.call('default_get', [ + ['subject', 'body_text', 'body', 'attachment_ids', 'partner_ids', 'composition_mode', + 'res_model', 'res_id', 'parent_id', 'content_subtype'], + this.ds_compose.get_context(), + ]).then( function (result) { self.form_view.on_processed_onchange({'value': result}, []); }); + }, + + /** + * Override-hack of do_action: clean the form */ + do_action: function(action, on_close) { + console.log('compose_message do_action', action, on_close); + return this._super(action, on_close); }, /** @@ -213,38 +205,23 @@ openerp.mail = function(session) { * in the function. */ bind_events: function() { var self = this; - this.$element.find('button.oe_form_button').click(function (event) { - event.preventDefault(); - }); + // this.$element.find('button.oe_form_button').click(function (event) { + // event.preventDefault(); + // event.stopPropagation(); + // }); // event: click on 'Formatting' icon-link that toggles the advanced // formatting options for writing a message (subject, body_html) - this.$element.on('click', 'button.oe_mail_compose_message_formatting', function (event) { - event.preventDefault(); - event.stopPropagation(); - self.toggle_formatting_mode(event); - }); + // this.$element.on('click', 'button.oe_mail_compose_message_formatting', function (event) { + // event.preventDefault(); + // event.stopPropagation(); + // self.toggle_formatting_mode(event); + // }); // event: click on 'Attachment' icon-link that opens the dialog to // add an attachment. this.$element.on('click', 'button.oe_mail_compose_message_attachment', function (event) { event.stopImmediatePropagation(); }); }, - - /** - * Toggle the formatting mode. */ - toggle_formatting_mode: function(event) { - this.formatting = ! this.formatting; - // update context of datasetsearch - this.ds_compose.context.formatting = this.formatting; - }, - - /** - * Update the values of the composition form; with possible different - * values for body and body_html. */ - set_body_value: function(body, body_html) { - this.form_view.fields.body.set_value(body); - this.form_view.fields.body_html.set_value(body_html); - }, }), /** @@ -398,12 +375,15 @@ openerp.mail = function(session) { }); // event: click on "Reply by email" in msg side menu (email style) this.$element.on('click', 'a.oe_mail_msg_reply_by_email', function (event) { + console.log('cacaprout'); event.preventDefault(); event.stopPropagation(); var msg_id = event.srcElement.dataset.msg_id; var formatting = (event.srcElement.dataset.formatting == 'html'); if (! msg_id) return false; - self.instantiate_composition_form('reply', formatting, msg_id); + // self.instantiate_composition_form('reply', formatting, msg_id); + console.log('cacaprout2'); + self.compose_message_widget.refresh({'composition_mode': 'reply', 'msg_id': parseInt(msg_id)}); }); }, @@ -420,15 +400,14 @@ openerp.mail = function(session) { return this._super(action, on_close); }, - /** Instantiate the composition form, with paramteres coming from thread parameters */ + /** Instantiate the composition form, with parameters coming from thread parameters */ instantiate_composition_form: function(mode, formatting, msg_id, context) { if (this.compose_message_widget) { this.compose_message_widget.destroy(); } this.compose_message_widget = new mail.ComposeMessage(this, { - 'extended_mode': false, 'uid': this.options.uid, 'res_model': this.options.context.res_model, - 'res_id': this.options.context.res_id, 'mode': mode || 'comment', 'msg_id': msg_id, - 'formatting': formatting || false, 'context': context || false } ); + 'res_model': this.options.context.res_model, 'res_id': this.options.context.res_id, + 'composition_mode': mode || 'comment', 'msg_id': msg_id, 'context': context || false } ); var composition_node = this.$element.find('div.oe_mail_thread_action'); composition_node.empty(); var compose_done = this.compose_message_widget.appendTo(composition_node); diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 9a6d8b92a3c..d0ae83316e7 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -102,15 +102,15 @@ diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 19faaad9578..140773981e5 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -32,81 +32,75 @@ from tools.translate import _ EXPRESSION_PATTERN = re.compile('(\$\{.+?\})') class mail_compose_message(osv.TransientModel): - """ Generic Email composition wizard. This wizard is meant to be inherited - at model and view levels to provide specific wizard features. + """ Generic message composition wizard. You may inherit from this wizard + at model and view levels to provide specific features. - The behavior of the wizard can be modified through the use of context - parameters, among which are: - - mail.compose.message.mode: - - if set to 'reply', the wizard is a reply to a previous message. - It is pre-populated with the original quote - - if set to 'comment', it means you are writing a new message to - be attached to a document. It is pre-populated with values - coming from ``get_value``, related to the document, and that - can be overridden to add specific model-related behavior. - - if set to 'mass_mail', the wizard is in mass mailing mode where - the mail details can contain template placeholders that will be - merged with actual data before being sent to each recipient. - - active_model: model name of the document to which the mail being - composed is related - - active_id: id of the document to which the mail being composed is - related, or id of the message to which user is replying, in case - ``mail.compose.message.mode == 'reply'`` - - active_ids: ids of the documents to which the mail being composed is - related, in case ``mail.compose.message.mode == 'mass_mail'``. + The behavior of the wizard can be modified through the context key + mail.compose.message.mode: + - 'reply': reply to a previous message. The wizard is pre-populated + via ``get_message_data``. + - 'comment': new post on a record. The wizard is pre-populated via + ``get_record_data`` + - 'mass_mail': wizard in mass mailing mode where the mail details can + contain template placeholders that will be merged with actual data + before being sent to each recipient. """ _name = 'mail.compose.message' _inherit = 'mail.message' _description = 'Email composition wizard' def default_get(self, cr, uid, fields, context=None): - """ Overridden to provide specific defaults depending on the context - parameters. - - Composition mode - - comment: default mode; active_model, active_id = model and ID of a - document we are commenting, - - mass_mailing mode: active_model, active_id = model and ID of a - document we are commenting, - - reply: active_id = ID of a mail.message to which we are replying. - From this message we can find the related model and res_id, - - :param dict context: several context values will modify the behavior - of the wizard, cfr. the class description. + """ Handle composition mode. Some details about context keys: + - comment: default mode, model and ID of a record the user comments + - default_model or active_model + - default_res_id or active_id + - reply: active_id of a message the user replies to + - active_id: ID of a mail.message to which we are replying + - message.res_model or default_model + - message.res_id or default_res_id + - mass_mailing mode: model and IDs of records the user mass-mails + - active_ids: record IDs + - default_model or active_model """ + # get some important values from context if context is None: context = {} - compose_mode = context.get('mail.compose.message.mode', 'comment') - active_model = context.get('active_model') - active_id = context.get('active_id') result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context) + # get some important values from context + composition_mode = context.get('mail.compose.message.mode') + model = context.get('default_model', context.get('active_model')) + res_id = context.get('default_res_id', context.get('active_id')) + active_id = context.get('active_id') + active_ids = context.get('active_ids') + # get default values according to the composition mode - vals = {} - if compose_mode in ['reply']: - vals = self.get_message_data(cr, uid, int(context['active_id']), context=context) - elif compose_mode in ['comment', 'mass_mail'] and active_model and active_id: - vals = self.get_value(cr, uid, active_model, active_id, context) + if composition_mode in ['reply']: + vals = self.get_message_data(cr, uid, active_id, context=context) + elif composition_mode in ['comment', 'mass_mail'] and model and res_id: + vals = self.get_record_data(cr, uid, model, res_id, context=context) + else: + vals = {'model': model, 'res_id': res_id} + if composition_mode: + vals['composition_mode'] = composition_mode + for field in vals: if field in fields: result[field] = vals[field] - - # link to model and record if not done yet - if not result.get('model') and active_model: - result['model'] = active_model - if not result.get('res_id') and active_id: - result['res_id'] = active_id return result _columns = { - 'dest_partner_ids': fields.many2many('res.partner', + 'composition_mode': fields.selection([ + ('comment', 'Comment a document'), + ('reply', 'Reply to a message'), + ('mass_mail', 'Mass mailing') + ], string='Composition mode'), + 'partner_ids': fields.many2many('res.partner', 'mail_compose_message_res_partner_rel', - 'wizard_id', 'partner_id', 'Destination partners', - help="When sending emails through the social network composition wizard"\ - "you may choose to send a copy of the mail to partners."), - 'attachment_ids': fields.many2many('ir.attachment','mail_compose_message_ir_attachments_rel', + 'wizard_id', 'partner_id', 'Additional contacts'), + 'attachment_ids': fields.many2many('ir.attachment', + 'mail_compose_message_ir_attachments_rel', 'wizard_id', 'attachment_id', 'Attachments'), - 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"), 'filter_id': fields.many2one('ir.filters', 'Filters'), 'body_text': fields.text('Plain-text editor body'), 'content_subtype': fields.char('Message content subtype', size=32, readonly=1, @@ -115,46 +109,75 @@ class mail_compose_message(osv.TransientModel): } _defaults = { + 'composition_mode': 'comment', 'content_subtype': lambda self,cr, uid, context={}: 'plain', - 'body_text': lambda self,cr, uid, context={}: '', + 'body_text': lambda self,cr, uid, context={}: False, 'body': lambda self,cr, uid, context={}: '', + 'subject': lambda self,cr, uid, context={}: False, + 'partner_ids': [], } - def get_value(self, cr, uid, model, res_id, context=None): + def get_record_data(self, cr, uid, model, res_id, context=None): """ Returns a defaults-like dict with initial values for the composition wizard when sending an email related to the document record identified by ``model`` and ``res_id``. - The default implementation returns an empty dictionary, and is meant - to be overridden by subclasses. - :param str model: model name of the document record this mail is related to. - :param int res_id: id of the document record this mail is related to. - :param dict context: several context values will modify the behavior - of the wizard, cfr. the class description. + :param int res_id: id of the document record this mail is related to """ + return {'model': model, 'res_id': res_id} + + def get_message_data(self, cr, uid, message_id, context=None): + """ Returns a defaults-like dict with initial values for the composition + wizard when replying to the given message (e.g. including the quote + of the initial message, and the correct recipients). + + :param int message_id: id of the mail.message to which the user + is replying. + """ + if context is None: + context = {} result = {} - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + if not message_id: + return result + + current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context=context) + + # create subject + re_prefix = _("Re:") + reply_subject = tools.ustr(message_data.subject or '') + if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)): + reply_subject = "%s %s" % (re_prefix, reply_subject) + # create the reply in the body + reply_header = _('On %(date)s, %(sender_name)s wrote:') % { + 'date': message_data.date if message_data.date else '', + 'sender_name': message_data.author_id.name } + reply_body = '
    %s
    %s
    %s' % (reply_header, message_data.body, current_user.signature) + # get partner_ids from original message + partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else [] + + # update the result result.update({ - 'model': model, - 'res_id': res_id, - 'email_from': user.email or tools.config.get('email_from', False), - 'body': False, - 'subject': False, - 'dest_partner_ids': [], + 'model': message_data.model, + 'res_id': message_data.res_id, + 'parent_id': message_data.id, + 'body': reply_body, + 'subject': reply_subject, + 'partner_ids': partner_ids, + 'content_subtype': 'html', }) return result - def toggle_formatting(self, cr, uid, ids, context=None): + def toggle_content_subtype(self, cr, uid, ids, context=None): """ hit toggle formatting mode button: calls onchange_formatting to emulate an on_change, then writes the value to update the form. """ for record in self.browse(cr, uid, ids, context=context): content_st_new_value = 'plain' if record.content_subtype == 'html' else 'html' onchange_res = self.onchange_content_subtype(cr, uid, ids, content_st_new_value, record.model, record.res_id, context=context) self.write(cr, uid, [record.id], onchange_res['value'], context=context) - return False - + return True def onchange_content_subtype(self, cr, uid, ids, value, model, res_id, context=None): """ onchange_content_subtype (values: 'plain' or 'html'). This onchange @@ -164,12 +187,10 @@ class mail_compose_message(osv.TransientModel): This method can be overridden for models that want to have their specific behavior. """ - if value == 'plain': - return {'value': {'subject': False, 'content_subtype': value}} return {'value': {'content_subtype': value}} - def onchange_dest_partner_ids(self, cr, uid, ids, value, context=None): - """ onchange_dest_partner_ids (value format: [[6, False, [3, 4]]]). The + def onchange_partner_ids(self, cr, uid, ids, value, context=None): + """ onchange_partner_ids (value format: [[6, False, [3, 4]]]). The basic purpose of this method is to check that destination partners effectively have email addresses. Otherwise a warning is thrown. """ @@ -189,94 +210,40 @@ class mail_compose_message(osv.TransientModel): } return {'warning': warning, 'value': {}} - def get_message_data(self, cr, uid, message_id, context=None): - """ Returns a defaults-like dict with initial values for the composition - wizard when replying to the given message (e.g. including the quote - of the initial message, and the correct recipient). It should not be - called unless ``context['mail.compose.message.mode'] == 'reply'``. - - :param int message_id: id of the mail.message to which the user - is replying. - :param dict context: several context values will modify the behavior - of the wizard, cfr. the class description. - """ - if context is None: - context = {} - result = {} - if not message_id: - return result - - current_user = self.pool.get('res.users').browse(cr, uid, uid, context) - message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context) - # Form the subject - re_prefix = _("Re:") - reply_subject = tools.ustr(message_data.subject or '') - if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)): - reply_subject = "%s %s" % (re_prefix, reply_subject) - - # Form the bodies (text and html). We use the plain text version of the - # original mail, by default, as it is easier to quote than the HTML - # version. TODO: make it possible to switch to HTML on the fly - - sent_date = _('On %(date)s, ') % {'date': message_data.date} if message_data.date else '' - sender = _('%(sender_name)s wrote:') % {'sender_name': tools.ustr(message_data.email_from or _('You'))} - - body = message_data.body or '' - quoted_body = '
    %s
    ' % (tools.ustr(body)), - reply_body = '

    %s%s
    %s
    %s' % (sent_date, sender, quoted_body, current_user.signature) - - # form dest_partner_ids - dest_partner_ids = [partner.id for partner in message_data.partner_ids] - - # update the result - result.update({ - 'body': reply_body, - 'subject': reply_subject, - 'dest_partner_ids': dest_partner_ids, - 'model': message_data.model or False, - 'res_id': message_data.res_id or False, - }) - return result - def send_mail(self, cr, uid, ids, context=None): - '''Process the wizard contents and proceed with sending the corresponding - email(s), rendering any template patterns on the fly if needed. - If the wizard is in mass-mail mode (context['mail.compose.message.mode'] is - set to ``'mass_mail'``), the resulting email(s) are scheduled for being - sent the next time the mail.message scheduler runs, or the next time - ``mail.message.process_email_queue`` is called. - Otherwise the new message is sent immediately. - - :param dict context: several context values will modify the behavior - of the wizard, cfr. the class description. - ''' + """ Process the wizard content and proceed with sending the related + email(s), rendering any template patterns on the fly if needed. """ if context is None: context = {} - formatting = context.get('formatting') - # FIXME TODO: mass_mail_mode unused? - mass_mail_mode = context.get('mail.compose.message.mode') == 'mass_mail' + for wizard in self.browse(cr, uid, ids, context=context): + mass_mail_mode = wizard.composition_mode == 'mass_mail' - for mail_wiz in self.browse(cr, uid, ids, context=context): attachment = {} - for attach in mail_wiz.attachment_ids: + for attach in wizard.attachment_ids: attachment[attach.datas_fname] = attach.datas and attach.datas or False # default values, according to the wizard options - subject = mail_wiz.subject if formatting else False - # FIXME TODO: partner_ids not used?? - partner_ids = [partner.id for partner in mail_wiz.dest_partner_ids] - body = mail_wiz.body_html if mail_wiz.content_subtype == 'html' else mail_wiz.body + subject = wizard.subject if wizard.content_subtype == 'html' else False + partner_ids = [partner.id for partner in wizard.partner_ids] + body = wizard.body if wizard.content_subtype == 'html' else wizard.body_text - active_model_pool = self.pool.get('mail.thread') - active_id = context.get('default_res_id', False) + active_model_pool = self.pool.get(wizard.model if wizard.model else 'mail.thread') + + #TODO: TDE: WIP: have to check for mass mail and templates - no time anymore today if context.get('mail.compose.message.mode') == 'mass_mail' and context.get('default_model', False) and context.get('default_res_id', False): active_model = context.get('default_model', False) active_model_pool = self.pool.get(active_model) subject = self.render_template(cr, uid, subject, active_model, active_id) - body = self.render_template(cr, uid, mail_wiz.body_html, active_model, active_id) - active_model_pool.message_post(cr, uid, [active_id], body=body, subject=subject, msg_type='comment', - attachments=attachment, context=context) + body = self.render_template(cr, uid, wizard.body_html, active_model, active_id) + + # determine the ids we are commenting + if mass_mail_mode: + res_ids = context.get('active_ids', []) + else: + res_ids = [wizard.res_id] + active_model_pool.message_post(cr, uid, res_ids, body=body, subject=subject, msg_type='comment', + attachments=attachment, context=context, partner_ids=partner_ids) return {'type': 'ir.actions.act_window_close'} @@ -310,7 +277,7 @@ class mail_compose_message(osv.TransientModel): return template and EXPRESSION_PATTERN.sub(merge, template) def dummy(self, cr, uid, ids, context=None): - return False - - -#FIXME: check for models defining '_mail_compose_message' + """ TDE: defined to have buttons that do basically nothing. It is + currently impossible to have buttons that do nothing special + in views (if type not specified, considered as 'object'). """ + return True diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml index a10dc6ccc86..8bf8df24f59 100644 --- a/addons/mail/wizard/mail_compose_message_view.xml +++ b/addons/mail/wizard/mail_compose_message_view.xml @@ -38,8 +38,13 @@
    + + + + + @@ -49,9 +54,9 @@ -