diff --git a/addons/account/account.py b/addons/account/account.py
index 278f41a8df8..0b70ea1a826 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -111,7 +111,7 @@ class account_payment_term_line(osv.osv):
'days': fields.integer('Number of Days', required=True, help="Number of days to add before computation of the day of month." \
"If Date=15/01, Number of Days=22, Day of Month=-1, then the due date is 28/02."),
'days2': fields.integer('Day of the Month', required=True, help="Day of the month, set -1 for the last day of the current month. If it's positive, it gives the day of the next month. Set 0 for net days (otherwise it's based on the beginning of the month)."),
- 'payment_id': fields.many2one('account.payment.term', 'Payment Term', required=True, select=True),
+ 'payment_id': fields.many2one('account.payment.term', 'Payment Term', required=True, select=True, ondelete='cascade'),
}
_defaults = {
'value': 'balance',
@@ -1377,7 +1377,7 @@ class account_move(osv.osv):
balance = 0.0
for line in line_ids:
if line[2]:
- balance += (line[2]['debit'] or 0.00)- (line[2]['credit'] or 0.00)
+ balance += (line[2].get('debit',0.00)- (line[2].get('credit',0.00)))
return {'value': {'balance': balance}}
def write(self, cr, uid, ids, vals, context=None):
diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py
index 8df291a98d1..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_append_note(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 7bc9ceaaf61..15027b7a8f6 100644
--- a/addons/account/account_invoice.py
+++ b/addons/account/account_invoice.py
@@ -1045,7 +1045,7 @@ class account_invoice(osv.osv):
if obj_inv.type in ('out_invoice', 'out_refund'):
ctx = self.get_log_context(cr, uid, context=ctx)
message = _("Invoice '%s' is validated.") % name
- self.message_append_note(cr, uid, [inv_id], body=message, context=context)
+ self.message_post(cr, uid, [inv_id], body=message, context=context)
return True
def action_cancel(self, cr, uid, ids, *args):
@@ -1275,7 +1275,7 @@ class account_invoice(osv.osv):
# TODO: use currency's formatting function
msg = _("Invoice '%s' is paid partially: %s%s of %s%s (%s%s remaining).") % \
(name, pay_amount, code, invoice.amount_total, code, total, code)
- self.message_append_note(cr, uid, [inv_id], body=msg, context=context)
+ self.message_post(cr, uid, [inv_id], body=msg, context=context)
self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
# Update the stored value (fields.function), so we write to trigger recompute
@@ -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_append_note(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_append_note(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_append_note(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()
@@ -1361,10 +1362,16 @@ class account_invoice_line(osv.osv):
'company_id': fields.related('invoice_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
'partner_id': fields.related('invoice_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True)
}
+
+ def _default_account_id(self, cr, uid, ids, context=None):
+ prop = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context)
+ return prop and prop.id or False
+
_defaults = {
'quantity': 1,
'discount': 0.0,
'price_unit': _price_unit_default,
+ 'account_id': _default_account_id,
}
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
@@ -1473,10 +1480,11 @@ class account_invoice_line(osv.osv):
prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
prod_uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
if prod.uom_id.category_id.id != prod_uom.category_id.id:
- warning = {
+ warning = {
'title': _('Warning!'),
'message': _('The selected unit of measure is not compatible with the unit of measure of the product.')
- }
+ }
+ res['value'].update({'uos_id': prod.uom_id.id})
return {'value': res['value'], 'warning': warning}
return res
diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml
index 143c5514141..610485ec3a0 100644
--- a/addons/account/account_invoice_view.xml
+++ b/addons/account/account_invoice_view.xml
@@ -191,17 +191,23 @@
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
@@ -342,17 +348,22 @@
-
-
+
+
+
+
diff --git a/addons/account/board_account_view.xml b/addons/account/board_account_view.xml
index 630c551c5ca..563c3f5fc6c 100644
--- a/addons/account/board_account_view.xml
+++ b/addons/account/board_account_view.xml
@@ -29,7 +29,6 @@
-You may change your password following this link:
+
You may change your password following this link,
+or by copy-pasting the following URL in your browser: ${object._auth_reset_password_link()}
-${object._auth_reset_password_link()}
-
-If you don't have asked for password reset, you can safely ignore this email.
-
-]]>
+
Note: If you did not ask for a password reset, you can safely ignore this email.
]]>
diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py
index b6eacda63c0..7e9bbacab13 100644
--- a/addons/base_action_rule/base_action_rule.py
+++ b/addons/base_action_rule/base_action_rule.py
@@ -302,33 +302,27 @@ the rule to mark CC(mail to any other person defined in actions)."),
return self.format_body(body % data)
def email_send(self, cr, uid, obj, emails, body, emailfrom=None, context=None):
- """ send email
- @param self: The object pointer
- @param cr: the current row, from the database cursor,
- @param uid: the current user’s ID for security checks,
- @param email: pass the emails
- @param emailfrom: Pass name the email From else False
- @param context: A standard dictionary for contextual values """
-
if not emailfrom:
- emailfrom = tools.config.get('email_from', False)
-
- if context is None:
- context = {}
-
- mail_message = self.pool.get('mail.message')
+ emailfrom = tools.config.get('email_from')
body = self.format_mail(obj, body)
- if not emailfrom:
- if hasattr(obj, 'user_id') and obj.user_id and obj.user_id.email:
- emailfrom = obj.user_id.email
-
- name = '[%d] %s' % (obj.id, tools.ustr(obj.name))
+ if not emailfrom and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.email:
+ emailfrom = obj.user_id.email
emailfrom = tools.ustr(emailfrom)
reply_to = emailfrom
if not emailfrom:
raise osv.except_osv(_('Error!'),
- _("No email ID found for your company address."))
- return mail_message.schedule_with_attach(cr, uid, emailfrom, emails, name, body, model='base.action.rule', reply_to=reply_to, res_id=obj.id)
+ _("Missing default email address or missing email on responsible user"))
+ return self.pool.get('mail.mail').create(cr, uid,
+ { 'email_from': emailfrom,
+ 'email_to': emails.join(','),
+ 'reply_to': reply_to,
+ 'state': 'outgoing',
+ 'subject': '[%d] %s' % (obj.id, tools.ustr(obj.name)),
+ 'body_html': '
%s
' % body,
+ 'res_id': obj.id,
+ 'model': obj._table_name,
+ 'auto_delete': True
+ }, context=context)
def do_check(self, cr, uid, action, obj, context=None):
@@ -438,11 +432,8 @@ the rule to mark CC(mail to any other person defined in actions)."),
if len(emails) and action.act_mail_body:
emails = list(set(emails))
email_from = safe_eval(action.act_email_from, {}, locals_for_emails)
-
- def to_email(text):
- return re.findall(r'([^ ,<@]+@[^> ,]+)', text or '')
- emails = to_email(','.join(filter(None, emails)))
- email_froms = to_email(email_from)
+ emails = tools.email_split(','.join(filter(None, emails)))
+ email_froms = tools.email_split(email_from)
if email_froms:
self.email_send(cr, uid, obj, emails, action.act_mail_body, emailfrom=email_froms[0])
return True
diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py
index be3244f8a5b..a6d5f6859af 100644
--- a/addons/base_calendar/base_calendar.py
+++ b/addons/base_calendar/base_calendar.py
@@ -471,18 +471,10 @@ property or property parameter."),
def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context=None):
"""
Send mail for event invitation to event attendees.
- @param cr: the current row, from the database cursor,
- @param uid: the current user’s ID for security checks,
- @param ids: List of attendee’s IDs.
@param email_from: Email address for user sending the mail
- @param context: A standard dictionary for contextual values
@return: True
"""
- if context is None:
- context = {}
-
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
- mail_message = self.pool.get('mail.message')
for att in self.browse(cr, uid, ids, context=context):
sign = att.sent_by_uid and att.sent_by_uid.signature or ''
sign = ' '.join(sign and sign.split('\n') or [])
@@ -508,17 +500,18 @@ property or property parameter."),
}
body = html_invitation % body_vals
if mail_to and email_from:
- attach = self.get_ics_file(cr, uid, res_obj, context=context)
- mail_message.schedule_with_attach(cr, uid,
- email_from,
- mail_to,
- sub,
- body,
- attachments=attach and {'invitation.ics': attach} or None,
- content_subtype='html',
- reply_to=email_from,
- context=context
- )
+ ics_file = self.get_ics_file(cr, uid, res_obj, context=context)
+ vals = {'email_from': email_from,
+ 'email_to': mail_to,
+ 'state': 'outgoing',
+ 'subject': sub,
+ 'body_html': body,
+ 'auto_delete': True}
+ if ics_file:
+ vals['attachment_ids'] = [(0,0,{'name': 'invitation.ics',
+ 'datas_fname': 'invitation.ics',
+ 'datas': str(ics_file).encode('base64')})]
+ self.pool.get('mail.mail').create(cr, uid, vals, context=context)
return True
def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
@@ -812,7 +805,6 @@ class calendar_alarm(osv.osv):
"""
if context is None:
context = {}
- mail_message = self.pool.get('mail.message')
current_datetime = datetime.now()
alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
@@ -849,36 +841,10 @@ class calendar_alarm(osv.osv):
else:
re_dates = [alarm.trigger_date]
- for r_date in re_dates:
- ref = alarm.model_id.model + ',' + str(alarm.res_id)
-
- # search for alreay sent requests
- #if request_obj.search(cr, uid, [('trigger_date', '=', r_date), ('ref_doc1', '=', ref)], context=context):
- #continue
-
- # Deactivated because of the removing of res.request
- # TODO: when cleaning calendar module, re-add this in a new mechanism
- #if alarm.action == 'display':
- #value = {
- #'name': alarm.name,
- #'act_from': alarm.user_id.id,
- #'act_to': alarm.user_id.id,
- #'body': alarm.description,
- #'trigger_date': r_date,
- #'ref_doc1': ref
- #}
- #request_id = request_obj.create(cr, uid, value)
- #request_ids = [request_id]
- #for attendee in res_obj.attendee_ids:
- #if attendee.user_id:
- #value['act_to'] = attendee.user_id.id
- #request_id = request_obj.create(cr, uid, value)
- #request_ids.append(request_id)
- #request_obj.request_send(cr, uid, request_ids)
-
+ if re_dates:
if alarm.action == 'email':
- sub = '[Openobject Reminder] %s' % (alarm.name)
- body = """
+ sub = '[OpenERP Reminder] %s' % (alarm.name)
+ body = """
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/addons/hr_recruitment/report/hr_recruitment_report_view.xml b/addons/hr_recruitment/report/hr_recruitment_report_view.xml
index 30a20a25eb8..1b0b8bfa7c5 100644
--- a/addons/hr_recruitment/report/hr_recruitment_report_view.xml
+++ b/addons/hr_recruitment/report/hr_recruitment_report_view.xml
@@ -71,7 +71,6 @@
-
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
index 4cba4a18680..a1562d53a61 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
@@ -95,25 +95,25 @@ class account_analytic_account(osv.osv):
def set_close(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'close'}, context=context)
message = _("Contract has been closed.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def set_cancel(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
message = _("Contract has been cancelled.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def set_open(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'open'}, context=context)
message = _("Contract has been opened.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def set_pending(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'pending'}, context=context)
message = _("Contract has been set as pending.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
account_analytic_account()
diff --git a/addons/hr_timesheet_sheet/__openerp__.py b/addons/hr_timesheet_sheet/__openerp__.py
index 34fbb602c9a..b1f03e7d6e0 100644
--- a/addons/hr_timesheet_sheet/__openerp__.py
+++ b/addons/hr_timesheet_sheet/__openerp__.py
@@ -27,8 +27,8 @@
'sequence': 16,
'summary': 'Timesheets, Attendances, Activities',
'description': """
-This module helps you to easily encode and validate timesheet and attendances within the same view.
-===================================================================================================
+This module helps you to easily record and validate timesheets and attendances within the same view.
+====================================================================================================
* It will maintain attendances and track (sign in/sign out) events.
* Track the timesheet lines.
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
index 39a0d9aed0c..05e70a19acb 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
@@ -442,10 +442,10 @@ class hr_timesheet_sheet(osv.osv):
return True
def name_get(self, cr, uid, ids, context=None):
- if isinstance(ids, (int, long)):
- ids = [ids]
- if not len(ids):
+ if not ids:
return []
+ if isinstance(ids, (long, int)):
+ ids = [ids]
return [(r['id'], r['date_from'] + ' - ' + r['date_to']) \
for r in self.read(cr, uid, ids, ['date_from', 'date_to'],
context=context, load='_classic_write')]
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml
index 5458f397e36..87edf949972 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml
@@ -1,24 +1,23 @@
-
-
-
-
-
- Module Timesheets Validation has been installed
- From the top menu "Human Resources", encode and validate timesheets and attendances.
-
+
+
+ mail.group
+
+ notification
+ Timesheet Validation application installed!
+ From the top menu "Human Resources", enter and validate timesheets and attendances.
+
-
-
-
+
+ code
+ ir.actions.server
-
-
-
-
+ action = pool.get('hr.timesheet.current.open').open_timesheet(cr, uid, None, context)
+ True
+ My Timesheet
diff --git a/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml b/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml
index f4f9326890f..352861263b8 100644
--- a/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml
+++ b/addons/hr_timesheet_sheet/test/test_hr_timesheet_sheet.yml
@@ -69,7 +69,7 @@
- account_id: account.analytic_agrolait
date: !eval time.strftime('%Y-%m-%d')
name: 'Develop yaml for hr module'
- user_id: base.user_qdp
+ user_id: base.user_demo
unit_amount: 6.00
amount: -90.00
product_id: product.product_product_consultant
@@ -97,7 +97,7 @@
date: !eval time.strftime('%Y-%m-%d')
name: 'Develop yaml for hr module'
unit_amount: 2.00
- user_id: base.user_qdp
+ user_id: base.user_demo
amount: -90.00
product_id: product.product_product_consultant
general_account_id: account.a_expense
diff --git a/addons/idea/idea.py b/addons/idea/idea.py
index 3be4dd2e28d..e2e5ec50ddf 100644
--- a/addons/idea/idea.py
+++ b/addons/idea/idea.py
@@ -67,21 +67,21 @@ class idea_idea(osv.osv):
def idea_cancel(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, { 'state': 'cancel' })
- self.message_append_note(cr, uid, ids, body=_('Idea canceled.'), context=context)
+ self.message_post(cr, uid, ids, body=_('Idea canceled.'), context=context)
return True
def idea_open(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, { 'state': 'open'})
- self.message_append_note(cr, uid, ids, body=_('Idea accepted.'), context=context)
+ self.message_post(cr, uid, ids, body=_('Idea accepted.'), context=context)
return True
def idea_close(self, cr, uid, ids, context={}):
- self.message_append_note(cr, uid, ids, body=_('Idea done.'), context=context)
+ self.message_post(cr, uid, ids, body=_('Idea done.'), context=context)
self.write(cr, uid, ids, { 'state': 'close' })
return True
def idea_draft(self, cr, uid, ids, context={}):
- self.message_append_note(cr, uid, ids, body=_('Idea reset to draft.'), context=context)
+ self.message_post(cr, uid, ids, body=_('Idea reset to draft.'), context=context)
self.write(cr, uid, ids, { 'state': 'draft' })
return True
idea_idea()
diff --git a/addons/import_base/import_framework.py b/addons/import_base/import_framework.py
index 74d2185b57e..7e0a055d95b 100644
--- a/addons/import_base/import_framework.py
+++ b/addons/import_base/import_framework.py
@@ -425,7 +425,7 @@ class import_framework(Thread):
email_id = email_obj.create(self.cr, self.uid, {
'email_from' : 'import@module.openerp',
'email_to' : self.email,
- 'body_text' : self.get_email_body(result, error),
+ 'body' : self.get_email_body(result, error),
'subject' : self.get_email_subject(result, error),
'auto_delete' : True})
email_obj.send(self.cr, self.uid, [email_id])
diff --git a/addons/import_sugarcrm/import_sugarcrm.py b/addons/import_sugarcrm/import_sugarcrm.py
index 440a7b2b6f6..1646fb7ff2a 100644
--- a/addons/import_sugarcrm/import_sugarcrm.py
+++ b/addons/import_sugarcrm/import_sugarcrm.py
@@ -226,7 +226,7 @@ class sugar_import(import_framework):
'model': 'model',
'partner_id/.id': 'partner_id/.id',
'user_id/id': ref(self.TABLE_USER, 'assigned_user_id'),
- 'body_text': 'description',
+ 'body': 'description',
'body_html' : 'description_html',
}
diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py
index 289b9080fe7..da517ee17ad 100644
--- a/addons/mail/__init__.py
+++ b/addons/mail/__init__.py
@@ -20,11 +20,11 @@
##############################################################################
import mail_alias
-import mail_message
import mail_followers
+import mail_message
+import mail_mail
import mail_thread
import mail_group
-import ir_needaction
import res_partner
import res_users
import report
diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py
index daec8d03183..99dd6bda6ac 100644
--- a/addons/mail/__openerp__.py
+++ b/addons/mail/__openerp__.py
@@ -64,6 +64,7 @@ The main features of the module are:
'wizard/mail_compose_message_view.xml',
'res_config_view.xml',
'mail_message_view.xml',
+ 'mail_mail_view.xml',
'mail_followers_view.xml',
'mail_thread_view.xml',
'mail_group_view.xml',
diff --git a/addons/mail/data/mail_demo.xml b/addons/mail/data/mail_demo.xml
index e3d7c2ab1e1..1350fd33547 100644
--- a/addons/mail/data/mail_demo.xml
+++ b/addons/mail/data/mail_demo.xml
@@ -3,49 +3,43 @@
- Internal company announcemail.group
-
- html
-
+
+ Your monthly meal vouchers arrived. You can get them at Christine's office.
+This month you also get 250 EUR of eco-vouchers if you have been in the company for more than a year.comment
- mail.group
- html
-
+ comment
- mail.group
- html
-
+ Thanks, but where is Christine's office, if I may ask? (I'm new here)comment
- mail.group
- html
-
+ Great news, I need to buy a new fridge, I think I can pay it with the eco-vouchers!comment
-
+
+
+
+ mail.group
+
+ Building B3, second floor on the right :-)
+
+ comment
diff --git a/addons/mail/data/mail_group_data.xml b/addons/mail/data/mail_group_data.xml
index 2152706d056..53720926a5c 100644
--- a/addons/mail/data/mail_group_data.xml
+++ b/addons/mail/data/mail_group_data.xml
@@ -13,16 +13,16 @@
-
-
-
- Welcome to OpenERP!
- Your homepage is a summary of messages you received and key information about documents you follow.
+
+ mail.group
+
+ notification
+ Welcome to OpenERP!
+ Your homepage is a summary of messages you received and key information about documents you follow.
The top menu bar contains all applications you installed. You can use this <i>Settings</i> menu to install more applications, activate others features or give access to new users.
-To setup your preferences (name, email signature, avatar), click on the top right corner.
-
-
+To setup your preferences (name, email signature, avatar), click on the top right corner.
+
diff --git a/addons/mail/doc/mail_openchatter_howto.rst b/addons/mail/doc/mail_openchatter_howto.rst
index 789acdd87eb..e4c86add7bc 100644
--- a/addons/mail/doc/mail_openchatter_howto.rst
+++ b/addons/mail/doc/mail_openchatter_howto.rst
@@ -57,7 +57,7 @@ Use the thread viewer widget inside your form view by using the mail_thread widg
Send notifications
+++++++++++++++++++
-When sending a notification is required in your workflow or business logic, use the ``message_append_note`` method. This method is a shortcut to the ``message_append`` method that takes all ``mail.message`` fields as arguments. This latter method calls ``message_create`` that
+When sending a notification is required in your workflow or business logic, use the ``message_post`` method. This method is a shortcut to the ``message_append`` method that takes all ``mail.message`` fields as arguments. This latter method calls ``message_create`` that
- creates the message
- parses the body to find users you want to push the message to (finding and parsing ``@login`` in the message body)
@@ -74,7 +74,7 @@ You should therefore not worry about subscriptions or anything else than sending
return res
def do_something_send_note(self, cr, uid, ids, context=None):
- self.message_append_note(cr, uid, ids, _('My subject'),
+ self.message_post(cr, uid, ids, _('My subject'),
_("has received a notification and is happy for it."), context=context)
Notifications guidelines
@@ -147,7 +147,7 @@ The best way to direct the messages that will be displayed in the OpenChatter wi
# add: search in the current task project messages
'&', '&', ('res_id', '=', my_task.project_id.id), ('model', '=', 'project.project'),
# ... containing the task name
- '|', ('body_text', 'like', '%s' % (my_task.name)), ('body_html', 'like', '%s' % (my_task.name))
+ '|', ('body', 'like', '%s' % (my_task.name)), ('body_html', 'like', '%s' % (my_task.name))
] + domain, limit=limit, offset=offset, context=context)
# if asked: add ancestor ids to have complete threads
if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
diff --git a/addons/mail/doc/mail_state.rst b/addons/mail/doc/mail_state.rst
index 527a31ba835..fc2772bf094 100644
--- a/addons/mail/doc/mail_state.rst
+++ b/addons/mail/doc/mail_state.rst
@@ -1,15 +1,15 @@
.. _mail_state:
-message_state
+message_unread
=============
-``message_state`` is a boolean field that states whether the document
+``message_unread`` is a boolean field that states whether the document
has unread messages. In previous versions, some documents were going
back to ``pending`` state when receiving an email through the mail
gateway. Now the state related to messages differs from the state or
stage of the document itself.
-message_state and need action mechanism
+message_unread and need action mechanism
+++++++++++++++++++++++++++++++++++++++
The ``mail`` module introduces a default behavior for the need_action
@@ -23,6 +23,6 @@ mechanism [REF].
"""
result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context)
for obj in self.browse(cr, uid, ids, context=context):
- if obj.message_state == False and obj.user_id:
+ if obj.message_unread == False and obj.user_id:
result[obj.id].append(obj.user_id.id)
return result
diff --git a/addons/mail/ir_needaction.py b/addons/mail/ir_needaction.py
deleted file mode 100644
index 8193ac4f385..00000000000
--- a/addons/mail/ir_needaction.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2012-today OpenERP SA ()
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see
-#
-##############################################################################
-
-from osv import osv
-
-class ir_needaction_mixin(osv.Model):
- """ Update of ir.needaction_mixin class
- - override the get_needaction_user_ids method to define the default
- mail gateway need_action: when the object is unread, the object
- responsible has an action to perform.
- """
- _name = 'ir.needaction_mixin'
- _inherit = ['ir.needaction_mixin']
-
- def get_needaction_user_ids(self, cr, uid, ids, context=None):
- """ Returns the user_ids that have to perform an action. It the
- document mail state is unread (False), return object.user_id.id
- as need_action uid.
- :return: dict { record_id: [user_ids], }
- """
- result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context)
- for obj in self.browse(cr, uid, ids, context=context):
- if obj.message_state == False and obj.user_id:
- result[obj.id].append(obj.user_id.id)
- return result
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index fcdc78d5f0a..2aba685c4fa 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -21,43 +21,113 @@
from osv import osv
from osv import fields
+import tools
class mail_followers(osv.Model):
""" mail_followers holds the data related to the follow mechanism inside
- OpenERP. Users can choose to follow documents (records) of any kind that
- inherits from mail.thread. Following documents allow to receive
- notifications for new messages.
- A subscription is characterized by:
- :param: res_model: model of the followed objects
- :param: res_id: ID of resource (may be 0 for every objects)
- :param: user_id: user_id of the follower
+ OpenERP. Partners can choose to follow documents (records) of any kind
+ that inherits from mail.thread. Following documents allow to receive
+ notifications for new messages.
+ A subscription is characterized by:
+ :param: res_model: model of the followed objects
+ :param: res_id: ID of resource (may be 0 for every objects)
"""
_name = 'mail.followers'
- _rec_name = 'id'
+ _rec_name = 'partner_id'
_log_access = False
- _order = 'res_model asc'
- _description = 'Mail Document Followers'
+ _description = 'Document Followers'
_columns = {
'res_model': fields.char('Related Document Model', size=128,
required=True, select=1,
help='Model of the followed resource'),
'res_id': fields.integer('Related Document ID', select=1,
help='Id of the followed resource'),
- 'user_id': fields.many2one('res.users', string='Related User',
+ 'partner_id': fields.many2one('res.partner', string='Related Partner',
ondelete='cascade', required=True, select=1),
}
+
class mail_notification(osv.Model):
- """ mail_notification is a relational table modeling messages pushed to users.
- """
+ """ Class holding notifications pushed to partners. Followers and partners
+ added in 'contacts to notify' receive notifications. """
_name = 'mail.notification'
- _rec_name = 'id'
+ _rec_name = 'partner_id'
_log_access = False
- _order = 'message_id desc'
- _description = 'Mail notification'
+ _description = 'Notifications'
+
_columns = {
- 'user_id': fields.many2one('res.users', string='User',
- ondelete='cascade', required=True, select=1),
+ 'partner_id': fields.many2one('res.partner', string='Contact',
+ ondelete='cascade', required=True),
+ 'read': fields.boolean('Read'),
'message_id': fields.many2one('mail.message', string='Message',
- ondelete='cascade', required=True, select=1),
+ ondelete='cascade', required=True),
}
+
+ _defaults = {
+ 'read': False,
+ }
+
+ def init(self, cr):
+ cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_partner_id_read_message_id',))
+ if not cr.fetchone():
+ cr.execute('CREATE INDEX mail_notification_partner_id_read_message_id ON mail_notification (partner_id, read, message_id)')
+
+ def create(self, cr, uid, vals, context=None):
+ """ Override of create to check that we can not create a notification
+ for a message the user can not read. """
+ if self.pool.get('mail.message').check_access_rights(cr, uid, 'read'):
+ return super(mail_notification, self).create(cr, uid, vals, context=context)
+ return False
+
+ def set_message_read(self, cr, uid, msg_id, context=None):
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+ notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
+ return self.write(cr, uid, notif_ids, {'read': True}, context=context)
+
+ def notify(self, cr, uid, partner_ids, msg_id, context=None):
+ """ Send by email the notification depending on the user preferences """
+ context = context or {}
+ # mail_noemail (do not send email) or no partner_ids: do not send, return
+ if context.get('mail_noemail') or not partner_ids:
+ return True
+
+ mail_mail = self.pool.get('mail.mail')
+ msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context)
+
+ # add signature
+ body_html = msg.body
+ signature = msg.author_id and msg.author_id.user_ids[0].signature or ''
+ if signature:
+ body_html = tools.append_content_to_html(body_html, signature)
+
+ mail_values = {
+ 'mail_message_id': msg.id,
+ 'email_to': [],
+ 'auto_delete': True,
+ 'body_html': body_html,
+ 'state': 'outgoing',
+ }
+
+ for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
+ # Do not send an email to the writer
+ if partner.user_ids and partner.user_ids[0].id == uid:
+ continue
+ # Do not send to partners without email address defined
+ if not partner.email:
+ continue
+ # Partner does not want to receive any emails
+ if partner.notification_email_send == 'none':
+ continue
+ # Partner wants to receive only emails and comments
+ if partner.notification_email_send == 'comment' and msg.type not in ('email', 'comment'):
+ continue
+ # Partner wants to receive only emails
+ if partner.notification_email_send == 'email' and msg.type != 'email':
+ continue
+ if partner.email not in mail_values['email_to']:
+ mail_values['email_to'].append(partner.email)
+ if mail_values['email_to']:
+ mail_values['email_to'] = ', '.join(mail_values['email_to'])
+ email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
+ mail_mail.send(cr, uid, [email_notif_id], context=context)
+ return True
diff --git a/addons/mail/mail_followers_view.xml b/addons/mail/mail_followers_view.xml
index 8b19d51ca2a..36f08b3814a 100644
--- a/addons/mail/mail_followers_view.xml
+++ b/addons/mail/mail_followers_view.xml
@@ -11,7 +11,7 @@
-
+
@@ -23,7 +23,7 @@
10
-
+
@@ -37,7 +37,7 @@
- Pushed notif
+ Notificationsmail.notificationformtree,form
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index 39a1707bc8a..fdfeafd4afe 100644
--- a/addons/mail/mail_group.py
+++ b/addons/mail/mail_group.py
@@ -28,13 +28,8 @@ from osv import fields
from tools.translate import _
class mail_group(osv.Model):
- """
- A mail_group is a collection of users sharing messages in a discussion
- group. Group users are users that follow the mail group, using the
- subscription/follow mechanism of OpenSocial. A mail group has nothing
- in common with res.users.group.
- """
-
+ """ A mail_group is a collection of users sharing messages in a discussion
+ group. The group mechanics are based on the followers. """
_description = 'Discussion group'
_name = 'mail.group'
_inherit = ['mail.thread']
@@ -45,31 +40,16 @@ class mail_group(osv.Model):
for obj in self.browse(cr, uid, ids, context=context):
result[obj.id] = tools.image_get_resized_images(obj.image)
return result
-
+
def _set_image(self, cr, uid, id, name, value, args, context=None):
return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
-
- def _get_last_month_msg_nbr(self, cr, uid, ids, name, args, context=None):
- result = {}
- for id in ids:
- lower_date = (DT.datetime.now() - DT.timedelta(days=30)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
- result[id] = self.message_search(cr, uid, [id], limit=None, domain=[('date', '>=', lower_date)], count=True, context=context)
- return result
-
- def _get_default_image(self, cr, uid, context=None):
- image_path = openerp.modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
- return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
-
+
_columns = {
'description': fields.text('Description'),
'menu_id': fields.many2one('ir.ui.menu', string='Related Menu', required=True, ondelete="cascade"),
- 'responsible_id': fields.many2one('res.users', string='Responsible',
- ondelete='set null', required=True, select=1,
- help="Responsible of the group that has all rights on the record."),
- 'public': fields.selection([('public', 'Public'), ('private', 'Private'), ('groups', 'Selected Group Only')],
- string='Privacy', required=True,
- help='This group is visible by non members. '\
- 'Invisible groups can add members through the invite button.'),
+ 'public': fields.selection([('public','Public'),('private','Private'),('groups','Selected Group Only')], 'Privacy', required=True,
+ help='This group is visible by non members. \
+ Invisible groups can add members through the invite button.'),
'group_public_id': fields.many2one('res.groups', string='Authorized Group'),
'group_ids': fields.many2many('res.groups', rel='mail_group_res_group_rel',
id1='mail_group_id', id2='groups_id', string='Auto Subscription',
@@ -79,7 +59,7 @@ class mail_group(osv.Model):
'image': fields.binary("Photo",
help="This field holds the image used as photo for the "\
"user. The image is base64 encoded, and PIL-supported. "\
- "It is limited to a 12024x1024 px image."),
+ "It is limited to a 1024x1024 px image."),
'image_medium': fields.function(_get_image, fnct_inv=_set_image,
string="Medium-sized photo", type="binary", multi="_get_image",
store = {
@@ -96,17 +76,19 @@ class mail_group(osv.Model):
help="Small-sized photo of the group. It is automatically "\
"resized as a 50x50px image, with aspect ratio preserved. "\
"Use this field anywhere a small image is required."),
- 'last_month_msg_nbr': fields.function(_get_last_month_msg_nbr, type='integer',
- string='Messages count for last month'),
- 'alias_id': fields.many2one('mail.alias', 'Alias', required=True, ondelete="cascade",
- help="The email address associated with this group. New emails received will automatically "
- "create new topics."),
+ 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", required=True,
+ help="The email address associated with this group. New emails received will automatically "
+ "create new topics."),
}
def _get_default_employee_group(self, cr, uid, context=None):
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user')
return ref and ref[1] or False
+ def _get_default_image(self, cr, uid, context=None):
+ image_path = openerp.modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
+ return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
+
def _get_menu_parent(self, cr, uid, context=None):
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'mail_group_root')
return ref and ref[1] or False
@@ -114,29 +96,23 @@ class mail_group(osv.Model):
_defaults = {
'public': 'groups',
'group_public_id': _get_default_employee_group,
- 'responsible_id': (lambda s, cr, uid, ctx: uid),
'image': _get_default_image,
'parent_id': _get_menu_parent,
- 'alias_domain': False, # always hide alias during creation
+ 'alias_domain': False, # always hide alias during creation
}
- def _subscribe_user_with_group_m2m_command(self, cr, uid, ids, group_ids_command, context=None):
- # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
- user_group_ids = [command[1] for command in group_ids_command if command[0] == 4]
- user_group_ids += [id for command in group_ids_command if command[0] == 6 for id in command[2]]
- # retrieve the user member of those groups
- user_ids = []
- res_groups_obj = self.pool.get('res.groups')
- for group in res_groups_obj.browse(cr, uid, user_group_ids, context=context):
- user_ids += [user.id for user in group.users]
- # subscribe the users
- return self.message_subscribe(cr, uid, ids, user_ids, context=context)
+ def _subscribe_users(self, cr, uid, ids, context=None):
+ for mail_group in self.browse(cr, uid, ids, context=context):
+ partner_ids = []
+ for group in mail_group.group_ids:
+ partner_ids += [user.partner_id.id for user in group.users]
+ self.message_subscribe(cr, uid, ids, partner_ids, context=context)
def create(self, cr, uid, vals, context=None):
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
vals.pop('alias_name', None) # prevent errors during copy()
- alias_id = mail_alias.create_unique_alias(cr, uid,
+ alias_id = mail_alias.create_unique_alias(cr, uid,
# Using '+' allows using subaddressing for those who don't
# have a catchall domain setup.
{'alias_name': "group+"+vals['name']},
@@ -148,23 +124,22 @@ class mail_group(osv.Model):
# Create client action for this group and link the menu to it
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'action_mail_group_feeds')
if ref:
- search_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'view_message_search_wall')
+ search_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'view_message_search')
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,
- 'thread_level': 2
+ 'domain': [('model','=','mail.group'), ('res_id','=',mail_group_id)],
+ 'context': {'default_model': 'mail.group', 'default_res_id': mail_group_id},
+ 'res_model': 'mail.message',
+ 'thread_level': 1,
}
cobj = self.pool.get('ir.actions.client')
newref = cobj.copy(cr, uid, ref[1], default={'params': str(params), 'name': vals['name']}, context=context)
self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,'+str(newref), 'mail_group_id': mail_group_id}, context=context)
mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context)
-
- if vals.get('group_ids'):
- self._subscribe_user_with_group_m2m_command(cr, uid, [mail_group_id], vals.get('group_ids'), context=context)
+ if vals.get('group_ids'):
+ self._subscribe_users(cr, uid, [mail_group_id], context=context)
return mail_group_id
def unlink(self, cr, uid, ids, context=None):
@@ -176,21 +151,17 @@ class mail_group(osv.Model):
return res
def write(self, cr, uid, ids, vals, context=None):
+ result = super(mail_group, self).write(cr, uid, ids, vals, context=context)
if vals.get('group_ids'):
- self._subscribe_user_with_group_m2m_command(cr, uid, ids, vals.get('group_ids'), context=context)
- return super(mail_group, self).write(cr, uid, ids, vals, context=context)
+ self._subscribe_users(cr, uid, ids, vals.get('group_ids'), context=context)
+ return result
- def action_group_join(self, cr, uid, ids, context=None):
- return self.message_subscribe(cr, uid, ids, context=context)
+ def action_follow(self, cr, uid, ids, context=None):
+ """ Wrapper because message_subscribe_users take a user_ids=None
+ that receive the context without the wrapper. """
+ return self.message_subscribe_users(cr, uid, ids, context=context)
- def action_group_leave(self, cr, uid, ids, context=None):
- return self.message_unsubscribe(cr, uid, ids, context=context)
-
- # ----------------------------------------
- # OpenChatter methods and notifications
- # ----------------------------------------
-
- def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
- """ Add 'responsible_id' to the monitored fields """
- res = super(mail_group, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
- return res + ['responsible_id']
+ def action_unfollow(self, cr, uid, ids, context=None):
+ """ Wrapper because message_unsubscribe_users take a user_ids=None
+ that receive the context without the wrapper. """
+ return self.message_unsubscribe_users(cr, uid, ids, context=context)
diff --git a/addons/mail/mail_group_menu.py b/addons/mail/mail_group_menu.py
index 8625285d640..25b85bb7b22 100644
--- a/addons/mail/mail_group_menu.py
+++ b/addons/mail/mail_group_menu.py
@@ -40,11 +40,12 @@ class ir_ui_menu(osv.osv):
""" Override to take off menu entries (mail.group) the user is not
following. """
ids = super(ir_ui_menu, self).search(cr, uid, args, offset=0, limit=None, order=order, context=context, count=False)
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
follower_obj = self.pool.get('mail.followers')
for menu in self.browse(cr, uid, ids, context=context):
if menu.mail_group_id:
sub_ids = follower_obj.search(cr, uid, [
- ('user_id', '=', uid), ('res_model', '=', 'mail.group'),
+ ('partner_id', '=', partner_id), ('res_model', '=', 'mail.group'),
('res_id', '=', menu.mail_group_id.id)
], context=context)
if not sub_ids:
diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml
index d83b32d7afb..31beba80ecc 100644
--- a/addons/mail/mail_group_view.xml
+++ b/addons/mail/mail_group_view.xml
@@ -1,10 +1,11 @@
+
Discussion Groupmail.wall
-
+ mail.message
@@ -14,8 +15,9 @@
-
+
+
@@ -103,7 +97,6 @@
-
@@ -116,7 +109,6 @@
-
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
new file mode 100644
index 00000000000..7b593c5b998
--- /dev/null
+++ b/addons/mail/mail_mail.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010-today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+# import ast
+import base64
+import logging
+import tools
+
+from osv import osv
+from osv import fields
+from tools.translate import _
+
+_logger = logging.getLogger(__name__)
+
+class mail_mail(osv.Model):
+ """ Model holding RFC2822 email messages to send. This model also provides
+ facilities to queue and send new email messages. """
+ _name = 'mail.mail'
+ _description = 'Outgoing Mails'
+ _inherits = {'mail.message': 'mail_message_id'}
+ _order = 'id desc'
+
+ _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),
+ 'state': fields.selection([
+ ('outgoing', 'Outgoing'),
+ ('sent', 'Sent'),
+ ('received', 'Received'),
+ ('exception', 'Delivery Failed'),
+ ('cancel', 'Cancelled'),
+ ], 'Status', readonly=True),
+ 'auto_delete': fields.boolean('Auto Delete',
+ help="Permanently delete this email after sending it, to save space"),
+ 'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
+ 'email_from': fields.char('From', help='Message sender, taken from user preferences.'),
+ 'email_to': fields.text('To', help='Message recipients'),
+ 'email_cc': fields.char('Cc', help='Carbon copy message recipients'),
+ 'reply_to': fields.char('Reply-To', help='Preferred response address for the message'),
+ 'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"),
+
+ # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
+ # and during unlink() we will cascade delete the parent and its attachments
+ 'notification': fields.boolean('Is Notification')
+ }
+
+ def _get_default_from(self, cr, uid, context=None):
+ cur = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ if not cur.alias_domain:
+ raise osv.except_osv(_('Invalid Action!'), _('Unable to send email, set an alias domain in your server settings.'))
+ return cur.alias_name + '@' + cur.alias_domain
+
+ _defaults = {
+ 'state': 'outgoing',
+ 'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
+ }
+
+ def create(self, cr, uid, values, context=None):
+ if 'notification' not in values and values.get('mail_message_id'):
+ values['notification'] = True
+ return super(mail_mail,self).create(cr, uid, values, context=context)
+
+ def unlink(self, cr, uid, ids, context=None):
+ # cascade-delete the parent message for all mails that are not created for a notification
+ ids_to_cascade = self.search(cr, uid, [('notification','=',False),('id','in',ids)])
+ parent_msg_ids = [m.mail_message_id.id for m in self.browse(cr, uid, ids_to_cascade, context=context)]
+ res = super(mail_mail,self).unlink(cr, uid, ids, context=context)
+ self.pool.get('mail.message').unlink(cr, uid, parent_msg_ids, context=context)
+ return res
+
+ def mark_outgoing(self, cr, uid, ids, context=None):
+ return self.write(cr, uid, ids, {'state':'outgoing'}, context=context)
+
+ def cancel(self, cr, uid, ids, context=None):
+ return self.write(cr, uid, ids, {'state':'cancel'}, context=context)
+
+ def process_email_queue(self, cr, uid, ids=None, context=None):
+ """Send immediately queued messages, committing after each
+ message is sent - this is not transactional and should
+ not be called during another transaction!
+
+ :param list ids: optional list of emails ids to send. If passed
+ no search is performed, and these ids are used
+ instead.
+ :param dict context: if a 'filters' key is present in context,
+ this value will be used as an additional
+ filter to further restrict the outgoing
+ messages to send (by default all 'outgoing'
+ messages are sent).
+ """
+ if context is None:
+ context = {}
+ if not ids:
+ filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')]
+ if 'filters' in context:
+ filters.extend(context['filters'])
+ ids = self.search(cr, uid, filters, context=context)
+ res = None
+ try:
+ # Force auto-commit - this is meant to be called by
+ # the scheduler, and we can't allow rolling back the status
+ # of previously sent emails!
+ res = self.send(cr, uid, ids, auto_commit=True, context=context)
+ except Exception:
+ _logger.exception("Failed processing mail queue")
+ return res
+
+ def _postprocess_sent_message(self, cr, uid, mail, context=None):
+ """Perform any post-processing necessary after sending ``mail``
+ successfully, including deleting it completely along with its
+ attachment if the ``auto_delete`` flag of the mail was set.
+ Overridden by subclasses for extra post-processing behaviors.
+
+ :param browse_record mail: the mail that was just sent
+ :return: True
+ """
+ if mail.auto_delete:
+ mail.unlink()
+ return True
+
+ def _send_get_mail_subject(self, cr, uid, mail, force=False, context=None):
+ """ if void and related document: ' posted on '
+ :param mail: mail.mail browse_record """
+ if force or (not mail.subject and mail.model and mail.res_id):
+ return '%s posted on %s' % (mail.author_id.name, mail.record_name)
+ return mail.subject
+
+ def send(self, cr, uid, ids, auto_commit=False, context=None):
+ """ Sends the selected emails immediately, ignoring their current
+ state (mails that have already been sent should not be passed
+ unless they should actually be re-sent).
+ Emails successfully delivered are marked as 'sent', and those
+ that fail to be deliver are marked as 'exception', and the
+ corresponding error mail is output in the server logs.
+
+ :param bool auto_commit: whether to force a commit of the mail status
+ after sending each mail (meant only for scheduler processing);
+ should never be True during normal transactions (default: False)
+ :return: True
+ """
+ ir_mail_server = self.pool.get('ir.mail_server')
+ for mail in self.browse(cr, uid, ids, context=context):
+ try:
+ body = mail.body_html
+ subject = self._send_get_mail_subject(cr, uid, mail, context=context)
+
+ # handle attachments
+ attachments = []
+ for attach in mail.attachment_ids:
+ attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
+
+ # use only sanitized html and set its plaintexted version as alternative
+ body_alternative = tools.html2plaintext(body)
+ content_subtype_alternative = 'plain'
+
+ # build an RFC2822 email.message.Message object and send it without queuing
+ msg = ir_mail_server.build_email(
+ email_from = mail.email_from,
+ email_to = tools.email_split(mail.email_to),
+ subject = subject,
+ body = body,
+ body_alternative = body_alternative,
+ email_cc = tools.email_split(mail.email_cc),
+ reply_to = mail.reply_to,
+ attachments = attachments,
+ message_id = mail.message_id,
+ references = mail.references,
+ object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
+ subtype = 'html',
+ subtype_alternative = content_subtype_alternative)
+ res = ir_mail_server.send_email(cr, uid, msg,
+ mail_server_id=mail.mail_server_id.id, context=context)
+ if res:
+ mail.write({'state':'sent', 'message_id': res})
+ else:
+ mail.write({'state':'exception'})
+ mail.refresh()
+ if mail.state == 'sent':
+ self._postprocess_sent_message(cr, uid, mail, context=context)
+ except Exception:
+ _logger.exception('failed sending mail.mail %s', mail.id)
+ mail.write({'state':'exception'})
+
+ if auto_commit == True:
+ cr.commit()
+ return True
diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml
new file mode 100644
index 00000000000..72d41e33a84
--- /dev/null
+++ b/addons/mail/mail_mail_view.xml
@@ -0,0 +1,127 @@
+
+
+
+
+ mail.mail.form
+ mail.mail
+
+
+
+
+
+
+ mail.mail.tree
+ mail.mail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.mail.search
+ mail.mail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Emails
+ mail.mail
+ form
+ tree,form
+ {'search_default_outgoing': 1, 'search_default_type_email': 1}
+
+
+
+
+
+
+
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 7801a9b42d9..e4862ca9fb9 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -19,25 +19,13 @@
#
##############################################################################
-import ast
-import base64
-import dateutil.parser
-import email
import logging
-import re
-import time
-import datetime
-from email.header import decode_header
-from email.message import Message
-
-from openerp import SUPERUSER_ID
-from osv import osv
-from osv import fields
-import pytz
-from tools import DEFAULT_SERVER_DATETIME_FORMAT
-from tools.translate import _
import tools
+from email.header import decode_header
+from operator import itemgetter
+from osv import osv, fields
+
_logger = logging.getLogger(__name__)
""" Some tools for parsing / creating email fields """
@@ -47,633 +35,309 @@ def decode(text):
text = decode_header(text.replace('\r', ''))
return ''.join([tools.ustr(x[0], x[1]) for x in text])
-def mail_tools_to_email(text):
- """Return a list of the email addresses found in ``text``"""
- if not text: return []
- return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
+class mail_message(osv.Model):
+ """ Messages model: system notification (replacing res.log notifications),
+ comments (OpenChatter discussion) and incoming emails. """
+ _name = 'mail.message'
+ _description = 'Message'
+ _inherit = ['ir.needaction_mixin']
+ _order = 'id desc'
-# TODO: remove that after cleaning
-def to_email(text):
- return mail_tools_to_email(text)
+ _message_read_limit = 10
+ _message_record_name_length = 18
-class mail_message_common(osv.TransientModel):
- """ Common abstract class for holding the main attributes of a
- message object. It could be reused as parent model for any
- database model or wizard screen that needs to hold a kind of
- message.
- All internal logic should be in another model while this
- model holds the basics of a message. For example, a wizard for writing
- emails should inherit from this class and not from mail.message."""
+ def _shorten_name(self, name):
+ if len(name) <= (self._message_record_name_length + 3):
+ return name
+ return name[:self._message_record_name_length] + '...'
- def get_body(self, cr, uid, ids, name, arg, context=None):
- """ get correct body version: body_html for html messages, and
- body_text for plain text messages
- """
- result = dict.fromkeys(ids, '')
- for message in self.browse(cr, uid, ids, context=context):
- if message.content_subtype == 'html':
- result[message.id] = message.body_html
- else:
- result[message.id] = message.body_text
- return result
-
- def search_body(self, cr, uid, obj, name, args, context=None):
- # will receive:
- # - obj: mail.message object
- # - name: 'body'
- # - args: [('body', 'ilike', 'blah')]
- return ['|', '&', ('content_subtype', '=', 'html'), ('body_html', args[0][1], args[0][2]), ('body_text', args[0][1], args[0][2])]
-
- def get_record_name(self, cr, uid, ids, name, arg, context=None):
+ def _get_record_name(self, cr, uid, ids, name, arg, context=None):
+ """ Return the related document name, using get_name. """
result = dict.fromkeys(ids, '')
for message in self.browse(cr, uid, ids, context=context):
if not message.model or not message.res_id:
continue
- result[message.id] = self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1]
+ result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
return result
+ def _get_unread(self, cr, uid, ids, name, arg, context=None):
+ """ Compute if the message is unread by the current user. """
+ res = dict((id, {'unread': False}) for id in ids)
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+ notif_obj = self.pool.get('mail.notification')
+ notif_ids = notif_obj.search(cr, uid, [
+ ('partner_id', 'in', [partner_id]),
+ ('message_id', 'in', ids),
+ ('read', '=', False)
+ ], context=context)
+ for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
+ res[notif.message_id.id]['unread'] = True
+ return res
+
+ def _search_unread(self, cr, uid, obj, name, domain, context=None):
+ """ Search for messages unread by the current user. Condition is
+ inversed because we search unread message on a read column. """
+ if domain[0][2]:
+ read_cond = '(read = false or read is null)'
+ else:
+ read_cond = 'read = true'
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+ cr.execute("SELECT message_id FROM mail_notification "\
+ "WHERE partner_id = %%s AND %s" % read_cond,
+ (partner_id,))
+ return [('id', 'in', [r[0] for r in cr.fetchall()])]
+
def name_get(self, cr, uid, ids, context=None):
# name_get may receive int id instead of an id list
if isinstance(ids, (int, long)):
ids = [ids]
res = []
for message in self.browse(cr, uid, ids, context=context):
- name = ''
- if message.subject:
- name = '%s: ' % (message.subject)
- if message.body_text:
- name = '%s%s ' % (name, message.body_text[0:20])
- if message.date:
- name = '%s(%s)' % (name, message.date)
- res.append((message.id, name))
+ name = '%s: %s' % (message.subject or '', message.body or '')
+ res.append((message.id, self._shorten_name(name.lstrip(' :'))))
return res
- _name = 'mail.message.common'
- _rec_name = 'subject'
- _columns = {
- 'subject': fields.char('Subject', size=512),
- 'model': fields.char('Related Document Model', size=128, select=1),
- 'res_id': fields.integer('Related Document ID', select=1),
- 'record_name': fields.function(get_record_name, type='string',
- string='Message Record Name',
- help="Name get of the related document."),
- 'date': fields.datetime('Date'),
- 'email_from': fields.char('From', size=128, help='Message sender, taken from user preferences.'),
- 'email_to': fields.char('To', size=256, help='Message recipients'),
- 'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'),
- 'email_bcc': fields.char('Bcc', size=256, help='Blind carbon copy message recipients'),
- 'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'),
- 'headers': fields.text('Message Headers', readonly=1,
- help="Full message headers, e.g. SMTP session headers (usually available on inbound messages only)"),
- 'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1),
- 'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
- 'content_subtype': fields.char('Message content subtype', size=32,
- oldname="subtype", readonly=1,
- help="Type of message, usually 'html' or 'plain', used to select "\
- "plain-text or rich-text contents accordingly"),
- 'body_text': fields.text('Text Contents', help="Plain-text version of the message"),
- 'body_html': fields.html('Rich-text Contents', help="Rich-text/HTML version of the message"),
- 'body': fields.function(get_body, fnct_search = search_body, type='text',
- string='Message Content', store=True,
- help="Content of the message. This content equals the body_text field "\
- "for plain-test messages, and body_html for rich-text/HTML "\
- "messages. This allows having one field if we want to access "\
- "the content matching the message content_subtype."),
- 'parent_id': fields.many2one('mail.message.common', 'Parent Message',
- select=True, ondelete='set null',
- help="Parent message, used for displaying as threads with hierarchy"),
- }
-
- _defaults = {
- 'content_subtype': 'plain',
- 'date': (lambda *a: fields.datetime.now()),
- }
-
-class mail_message(osv.Model):
- """Model holding messages: system notification (replacing res.log
- notifications), comments (for OpenChatter feature) and
- RFC2822 email messages. This model also provides facilities to
- parse, queue and send new email messages. Type of messages
- are differentiated using the 'type' column. """
-
- _name = 'mail.message'
- _inherit = 'mail.message.common'
- _description = 'Mail Message (email, comment, notification)'
- _order = 'date desc'
-
- def open_document(self, cr, uid, ids, context=None):
- """ Open the message related document. Note that only the document of
- ids[0] will be opened.
- TODO: how to determine the action to use ?
- """
- action_data = False
- if not ids:
- return action_data
- msg = self.browse(cr, uid, ids[0], context=context)
- ir_act_window = self.pool.get('ir.actions.act_window')
- action_ids = ir_act_window.search(cr, uid, [('res_model', '=', msg.model)], context=context)
- if action_ids:
- action_data = ir_act_window.read(cr, uid, action_ids[0], context=context)
- action_data.update({
- 'domain' : "[('id', '=', %d)]" % (msg.res_id),
- 'nodestroy': True,
- 'context': {}
- })
- return action_data
-
- def open_attachment(self, cr, uid, ids, context=None):
- """ Open the message related attachments.
- TODO: how to determine the action to use ?
- """
- action_data = False
- if not ids:
- return action_data
- action_pool = self.pool.get('ir.actions.act_window')
- messages = self.browse(cr, uid, ids, context=context)
- att_ids = [x.id for message in messages for x in message.attachment_ids]
- action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')], context=context)
- if action_ids:
- action_data = action_pool.read(cr, uid, action_ids[0], context=context)
- action_data.update({
- 'domain': [('id', 'in', att_ids)],
- 'nodestroy': True
- })
- return action_data
-
_columns = {
'type': fields.selection([
- ('email', 'email'),
+ ('email', 'Email'),
('comment', 'Comment'),
('notification', 'System notification'),
], 'Type',
help="Message type: email for email message, notification for system "\
- "message, comment for other messages such as user replies"),
- 'partner_id': fields.many2one('res.partner', 'Related partner',
- help="Deprecated field. Use partner_ids instead."),
- 'partner_ids': fields.many2many('res.partner',
- 'mail_message_res_partner_rel',
- 'message_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."),
- 'user_id': fields.many2one('res.users', 'Related User', readonly=1),
+ "message, comment for other messages such as user replies"),
+ 'author_id': fields.many2one('res.partner', 'Author', required=True),
+ 'partner_ids': fields.many2many('res.partner', 'mail_notification', 'message_id', 'partner_id', 'Recipients'),
'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
'message_id', 'attachment_id', 'Attachments'),
- 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
- 'state': fields.selection([
- ('outgoing', 'Outgoing'),
- ('sent', 'Sent'),
- ('received', 'Received'),
- ('exception', 'Delivery Failed'),
- ('cancel', 'Cancelled'),
- ], 'Status', readonly=True),
- 'auto_delete': fields.boolean('Auto Delete',
- help="Permanently delete this email after sending it, to save space"),
- 'original': fields.binary('Original', readonly=1,
- help="Original version of the message, as it was sent on the network"),
- 'parent_id': fields.many2one('mail.message', 'Parent Message',
- select=True, ondelete='set null',
- help="Parent message, used for displaying as threads with hierarchy"),
+ 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
+ 'model': fields.char('Related Document Model', size=128, select=1),
+ 'res_id': fields.integer('Related Document ID', select=1),
+ 'record_name': fields.function(_get_record_name, type='string',
+ string='Message Record Name',
+ help="Name get of the related document."),
+ 'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
+ 'subject': fields.char('Subject'),
+ 'date': fields.datetime('Date'),
+ 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
+ 'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
+ 'unread': fields.function(_get_unread, fnct_search=_search_unread,
+ type='boolean', string='Unread',
+ help='Functional field to search for unread messages linked to uid'),
}
-
+
+ def _needaction_domain_get(self, cr, uid, context=None):
+ if self._needaction:
+ return [('unread', '=', True)]
+ return []
+
+ def _get_default_author(self, cr, uid, context=None):
+ return self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+
_defaults = {
'type': 'email',
- 'state': 'received',
+ 'date': lambda *a: fields.datetime.now(),
+ 'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
+ 'body': '',
}
-
+
+ #------------------------------------------------------
+ # Message loading for web interface
+ #------------------------------------------------------
+
+ def _message_dict_get(self, cr, uid, msg, context=None):
+ """ Return a dict representation of the message browse record. """
+ attachment_ids = self.pool.get('ir.attachment').name_get(cr, uid, [x.id for x in msg.attachment_ids], context=context)
+ author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0]
+ author_user_id = self.pool.get('res.users').name_get(cr, uid, [msg.author_id.user_ids[0].id], context=context)[0]
+ partner_ids = self.pool.get('res.partner').name_get(cr, uid, [x.id for x in msg.partner_ids], context=context)
+ return {
+ 'id': msg.id,
+ 'type': msg.type,
+ 'attachment_ids': attachment_ids,
+ 'body': msg.body,
+ 'model': msg.model,
+ 'res_id': msg.res_id,
+ 'record_name': msg.record_name,
+ 'subject': msg.subject,
+ 'date': msg.date,
+ 'author_id': author_id,
+ 'author_user_id': author_user_id,
+ 'partner_ids': partner_ids,
+ 'child_ids': [],
+ }
+
+ def message_read_tree_flatten(self, cr, uid, messages, current_level, level, context=None):
+ """ Given a tree with several roots of following structure :
+ [ {'id': 1, 'child_ids': [
+ {'id': 11, 'child_ids': [...] },],
+ {...} ]
+ Flatten it to have a maximum number of levels, 0 being flat and
+ sort messages in a level according to a key of the messages.
+ Perform the flattening at leafs if above the maximum depth, then get
+ back in the tree.
+ :param context: ``sort_key``: key for sorting (id by default)
+ :param context: ``sort_reverse``: reverser order for sorting (True by default)
+ """
+ def _flatten(msg_dict):
+ """ from {'id': x, 'child_ids': [{child1}, {child2}]}
+ get [{'id': x, 'child_ids': []}, {child1}, {child2}]
+ """
+ child_ids = msg_dict.pop('child_ids', [])
+ msg_dict['child_ids'] = []
+ return [msg_dict] + child_ids
+ # return sorted([msg_dict] + child_ids, key=itemgetter('id'), reverse=True)
+ context = context or {}
+ # Depth-first flattening
+ for message in messages:
+ if message.get('type') == 'expandable':
+ continue
+ message['child_ids'] = self.message_read_tree_flatten(cr, uid, message['child_ids'], current_level + 1, level, context=context)
+ # Flatten if above maximum depth
+ if current_level < level:
+ return_list = messages
+ else:
+ return_list = []
+ for message in messages:
+ for flat_message in _flatten(message):
+ return_list.append(flat_message)
+ return sorted(return_list, key=itemgetter(context.get('sort_key', 'id')), reverse=context.get('sort_reverse', True))
+
+ def message_read(self, cr, uid, ids=False, domain=[], thread_level=0, limit=None, context=None):
+ """ If IDs are provided, fetch these records. Otherwise use the domain
+ to fetch the matching records.
+ After having fetched the records provided by IDs, it will fetch the
+ parents to have well-formed threads.
+ :return list: list of trees of messages
+ """
+ limit = limit or self._message_read_limit
+ context = context or {}
+ if not ids:
+ ids = self.search(cr, uid, domain, context=context, limit=limit)
+ messages = self.browse(cr, uid, ids, context=context)
+
+ result = []
+ tree = {} # key: ID, value: record
+ for msg in messages:
+ if len(result) < (limit - 1):
+ record = self._message_dict_get(cr, uid, msg, context=context)
+ if thread_level and msg.parent_id:
+ while msg.parent_id:
+ if msg.parent_id.id in tree:
+ record_parent = tree[msg.parent_id.id]
+ else:
+ record_parent = self._message_dict_get(cr, uid, msg.parent_id, context=context)
+ if msg.parent_id.parent_id:
+ tree[msg.parent_id.id] = record_parent
+ if record['id'] not in [x['id'] for x in record_parent['child_ids']]:
+ record_parent['child_ids'].append(record)
+ record = record_parent
+ msg = msg.parent_id
+ if msg.id not in tree:
+ result.append(record)
+ tree[msg.id] = record
+ else:
+ result.append({
+ 'type': 'expandable',
+ 'domain': [('id', '<=', msg.id)] + domain,
+ 'context': context,
+ 'thread_level': thread_level, # should be improve accodting to level of records
+ 'id': -1,
+ })
+ break
+
+ # Flatten the result
+ if thread_level > 0:
+ result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
+ return result
+
#------------------------------------------------------
# Email api
#------------------------------------------------------
-
+
def init(self, cr):
cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
if not cr.fetchone():
cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
- def check(self, cr, uid, ids, mode, context=None, values=None):
- """Restricts the access to a mail.message, according to referred model
- """
- if not ids:
- return
- res_ids = {}
+ def check_access_rule(self, cr, uid, ids, operation, context=None):
+ """ mail.message access rule check
+ - message received (a notification exists) -> ok
+ - check rules of related document if exists
+ - fallback on normal mail.message check """
if isinstance(ids, (int, long)):
ids = [ids]
- cr.execute('SELECT DISTINCT model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
- for rmod, rid in cr.fetchall():
+
+ # check messages for which you have a notification
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+ not_obj = self.pool.get('mail.notification')
+ not_ids = not_obj.search(cr, uid, [
+ ('partner_id', '=', partner_id),
+ ('message_id', 'in', ids),
+ ], context=context)
+ notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, uid, not_ids, context=context)
+ if notification.message_id.id in ids]
+
+ # check messages linked to an existing document
+ model_record_ids = {}
+ document_ids = []
+ cr.execute('SELECT DISTINCT id, model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
+ for id, rmod, rid in cr.fetchall():
if not (rmod and rid):
continue
- res_ids.setdefault(rmod,set()).add(rid)
- if values:
- if 'res_model' in values and 'res_id' in values:
- res_ids.setdefault(values['res_model'],set()).add(values['res_id'])
+ document_ids.append(id)
+ model_record_ids.setdefault(rmod, set()).add(rid)
+ for model, mids in model_record_ids.items():
+ model_obj = self.pool.get(model)
+ mids = model_obj.exists(cr, uid, mids)
+ model_obj.check_access_rights(cr, uid, operation)
+ model_obj.check_access_rule(cr, uid, mids, operation, context=context)
+
+ # fall back on classic operation for other ids
+ other_ids = set(ids).difference(set(notified_ids), set(document_ids))
+ super(mail_message, self).check_access_rule(cr, uid, other_ids, operation, context=None)
- ima_obj = self.pool.get('ir.model.access')
- for model, mids in res_ids.items():
- # ignore mail messages that are not attached to a resource anymore when checking access rights
- # (resource was deleted but message was not)
- mids = self.pool.get(model).exists(cr, uid, mids)
- ima_obj.check(cr, uid, model, mode)
- self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
-
def create(self, cr, uid, values, context=None):
- self.check(cr, uid, [], mode='create', context=context, values=values)
- return super(mail_message, self).create(cr, uid, values, context)
+ if not values.get('message_id') and values.get('res_id') and values.get('model'):
+ values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s' % values)
+ newid = super(mail_message, self).create(cr, uid, values, context)
+ self.notify(cr, uid, newid, context=context)
+ return newid
- def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
- self.check(cr, uid, ids, 'read', context=context)
- return super(mail_message, self).read(cr, uid, ids, fields_to_read, context, load)
+ def unlink(self, cr, uid, ids, context=None):
+ # cascade-delete attachments that are directly attached to the message (should only happen
+ # for mail.messages that act as parent for a standalone mail.mail record.
+ attachments_to_delete = []
+ for mail in self.browse(cr, uid, ids, context=context):
+ for attach in mail.attachment_ids:
+ if attach.res_model == 'mail.message' and attach.res_id == mail.id:
+ attachments_to_delete.append(attach.id)
+ if attachments_to_delete:
+ self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
+ return super(mail_message,self).unlink(cr, uid, ids, context=context)
+
+ def notify(self, cr, uid, newid, context=None):
+ """ Add the related record followers to the destination partner_ids.
+ Call mail_notification.notify to manage the email sending
+ """
+ message = self.browse(cr, uid, newid, context=context)
+ partners_to_notify = set([])
+ # add all partner_ids of the message
+ if message.partner_ids:
+ partners_to_notify |= set(partner.id for partner in message.partner_ids)
+ # add all followers and set add them in partner_ids
+ if message.model and message.res_id:
+ record = self.pool.get(message.model).browse(cr, uid, message.res_id, context=context)
+ extra_notified = set(partner.id for partner in record.message_follower_ids)
+ missing_notified = extra_notified - partners_to_notify
+ if missing_notified:
+ message.write({'partner_ids': [(4, p_id) for p_id in missing_notified]})
+ partners_to_notify |= extra_notified
+ self.pool.get('mail.notification').notify(cr, uid, list(partners_to_notify), newid, context=context)
def copy(self, cr, uid, id, default=None, context=None):
"""Overridden to avoid duplicating fields that are unique to each email"""
if default is None:
default = {}
- self.check(cr, uid, [id], 'read', context=context)
- default.update(message_id=False, original=False, headers=False)
- return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
-
- def write(self, cr, uid, ids, vals, context=None):
- self.check(cr, uid, ids, 'write', context=context, values=vals)
- return super(mail_message, self).write(cr, uid, ids, vals, context)
-
- def unlink(self, cr, uid, ids, context=None):
- self.check(cr, uid, ids, 'unlink', context=context)
- return super(mail_message, self).unlink(cr, uid, ids, context)
-
- def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, type='email',
- email_cc=None, email_bcc=None, reply_to=False, partner_ids=None, attachments=None,
- message_id=False, references=False, res_id=False, content_subtype='plain',
- headers=None, mail_server_id=False, auto_delete=False, context=None):
- """ Schedule sending a new email message, to be sent the next time the
- mail scheduler runs, or the next time :meth:`process_email_queue` is
- called explicitly.
-
- :param string email_from: sender email address
- :param list email_to: list of recipient addresses (to be joined with commas)
- :param string subject: email subject (no pre-encoding/quoting necessary)
- :param string body: email body, according to the ``content_subtype``
- (by default, plaintext). If html content_subtype is used, the
- message will be automatically converted to plaintext and wrapped
- in multipart/alternative.
- :param list email_cc: optional list of string values for CC header
- (to be joined with commas)
- :param list email_bcc: optional list of string values for BCC header
- (to be joined with commas)
- :param string model: optional model name of the document this mail
- is related to (this will also be used to generate a tracking id,
- used to match any response related to the same document)
- :param int res_id: optional resource identifier this mail is related
- to (this will also be used to generate a tracking id, used to
- match any response related to the same document)
- :param string reply_to: optional value of Reply-To header
- :param partner_ids: destination partner_ids
- :param string content_subtype: optional mime content_subtype for
- the text body (usually 'plain' or 'html'), must match the format
- of the ``body`` parameter. Default is 'plain', making the content
- part of the mail "text/plain".
- :param dict attachments: map of filename to filecontents, where
- filecontents is a string containing the bytes of the attachment
- :param dict headers: optional map of headers to set on the outgoing
- mail (may override the other headers, including Subject,
- Reply-To, Message-Id, etc.)
- :param int mail_server_id: optional id of the preferred outgoing
- mail server for this mail
- :param bool auto_delete: optional flag to turn on auto-deletion of
- the message after it has been successfully sent (default to False)
- """
- if context is None:
- context = {}
- if attachments is None:
- attachments = {}
- if partner_ids is None:
- partner_ids = []
- attachment_obj = self.pool.get('ir.attachment')
- for param in (email_to, email_cc, email_bcc):
- if param and not isinstance(param, list):
- param = [param]
- msg_vals = {
- 'subject': subject,
- 'date': fields.datetime.now(),
- 'user_id': uid,
- 'model': model,
- 'res_id': res_id,
- 'type': type,
- 'body_text': body if content_subtype != 'html' else False,
- 'body_html': body if content_subtype == 'html' else False,
- 'email_from': email_from,
- 'email_to': email_to and ','.join(email_to) or '',
- 'email_cc': email_cc and ','.join(email_cc) or '',
- 'email_bcc': email_bcc and ','.join(email_bcc) or '',
- 'partner_ids': partner_ids,
- 'reply_to': reply_to,
- 'message_id': message_id,
- 'references': references,
- 'content_subtype': content_subtype,
- 'headers': headers, # serialize the dict on the fly
- 'mail_server_id': mail_server_id,
- 'state': 'outgoing',
- 'auto_delete': auto_delete
- }
- email_msg_id = self.create(cr, uid, msg_vals, context)
- attachment_ids = []
- for attachment in attachments:
- fname, fcontent = attachment
- attachment_data = {
- 'name': fname,
- 'datas_fname': fname,
- 'datas': fcontent and fcontent.encode('base64'),
- 'res_model': self._name,
- 'res_id': email_msg_id,
- }
- if context.has_key('default_type'):
- del context['default_type']
- attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context))
- if attachment_ids:
- self.write(cr, uid, email_msg_id, { 'attachment_ids': [(6, 0, attachment_ids)]}, context=context)
- return email_msg_id
-
- def mark_outgoing(self, cr, uid, ids, context=None):
- return self.write(cr, uid, ids, {'state':'outgoing'}, context=context)
-
- def cancel(self, cr, uid, ids, context=None):
- return self.write(cr, uid, ids, {'state':'cancel'}, context=context)
-
- def process_email_queue(self, cr, uid, ids=None, context=None):
- """Send immediately queued messages, committing after each
- message is sent - this is not transactional and should
- not be called during another transaction!
-
- :param list ids: optional list of emails ids to send. If passed
- no search is performed, and these ids are used
- instead.
- :param dict context: if a 'filters' key is present in context,
- this value will be used as an additional
- filter to further restrict the outgoing
- messages to send (by default all 'outgoing'
- messages are sent).
- """
- if context is None:
- context = {}
- if not ids:
- filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')]
- if 'filters' in context:
- filters.extend(context['filters'])
- ids = self.search(cr, uid, filters, context=context)
- res = None
- try:
- # Force auto-commit - this is meant to be called by
- # the scheduler, and we can't allow rolling back the status
- # of previously sent emails!
- res = self.send(cr, uid, ids, auto_commit=True, context=context)
- except Exception:
- _logger.exception("Failed processing mail queue")
- return res
-
- def parse_message(self, message, save_original=False, context=None):
- """Parses a string or email.message.Message representing an
- RFC-2822 email, and returns a generic dict holding the
- message details.
-
- :param message: the message to parse
- :type message: email.message.Message | string | unicode
- :param bool save_original: whether the returned dict
- should include an ``original`` entry with the base64
- encoded source of the message.
- :rtype: dict
- :return: A dict with the following structure, where each
- field may not be present if missing in original
- message::
-
- { 'message-id': msg_id,
- 'subject': subject,
- 'from': from,
- 'to': to,
- 'cc': cc,
- 'headers' : { 'X-Mailer': mailer,
- #.. all X- headers...
- },
- 'content_subtype': msg_mime_subtype,
- 'body_text': plaintext_body
- 'body_html': html_body,
- 'attachments': [('file1', 'bytes'),
- ('file2', 'bytes') }
- # ...
- 'original': source_of_email,
- }
- """
- msg_txt = message
- if isinstance(message, str):
- msg_txt = email.message_from_string(message)
-
- # Warning: message_from_string doesn't always work correctly on unicode,
- # we must use utf-8 strings here :-(
- if isinstance(message, unicode):
- message = message.encode('utf-8')
- msg_txt = email.message_from_string(message)
-
- message_id = msg_txt.get('message-id', False)
- msg = {}
-
- if save_original:
- # save original, we need to be able to read the original email sometimes
- msg['original'] = message.as_string() if isinstance(message, Message) \
- else message
- msg['original'] = base64.b64encode(msg['original']) # binary fields are b64
-
- if not message_id:
- # Very unusual situation, be we should be fault-tolerant here
- message_id = time.time()
- msg_txt['message-id'] = message_id
- _logger.info('Parsing Message without message-id, generating a random one: %s', message_id)
-
- msg_fields = msg_txt.keys()
- msg['id'] = message_id
- msg['message-id'] = message_id
-
- if 'Subject' in msg_fields:
- msg['subject'] = decode(msg_txt.get('Subject'))
-
- if 'Content-Type' in msg_fields:
- msg['content-type'] = msg_txt.get('Content-Type')
-
- if 'From' in msg_fields:
- msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())
-
- if 'To' in msg_fields:
- msg['to'] = decode(msg_txt.get('To'))
-
- if 'Delivered-To' in msg_fields:
- msg['to'] = decode(msg_txt.get('Delivered-To'))
-
- if 'CC' in msg_fields:
- msg['cc'] = decode(msg_txt.get('CC'))
-
- if 'Cc' in msg_fields:
- msg['cc'] = decode(msg_txt.get('Cc'))
-
- if 'Reply-To' in msg_fields:
- msg['reply'] = decode(msg_txt.get('Reply-To'))
-
- 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(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')
-
- if 'References' in msg_fields:
- msg['references'] = msg_txt.get('References')
-
- if 'In-Reply-To' in msg_fields:
- msg['in-reply-to'] = msg_txt.get('In-Reply-To')
-
- msg['headers'] = {}
- msg['content_subtype'] = 'plain'
- for item in msg_txt.items():
- if item[0].startswith('X-'):
- msg['headers'].update({item[0]: item[1]})
- if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''):
- encoding = msg_txt.get_content_charset()
- body = msg_txt.get_payload(decode=True)
- if 'text/html' in msg.get('content-type', ''):
- msg['body_html'] = body
- msg['content_subtype'] = 'html'
- if body:
- body = tools.html2plaintext(body)
- msg['body_text'] = tools.ustr(body, encoding)
-
- attachments = []
- if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
- body = ""
- if 'multipart/alternative' in msg.get('content-type', ''):
- msg['content_subtype'] = 'alternative'
- else:
- msg['content_subtype'] = 'mixed'
- for part in msg_txt.walk():
- if part.get_content_maintype() == 'multipart':
- continue
-
- encoding = part.get_content_charset()
- filename = part.get_filename()
- if part.get_content_maintype()=='text':
- content = part.get_payload(decode=True)
- if filename:
- attachments.append((filename, content))
- content = tools.ustr(content, encoding)
- if part.get_content_subtype() == 'html':
- msg['body_html'] = content
- msg['content_subtype'] = 'html' # html version prevails
- body = tools.ustr(tools.html2plaintext(content))
- body = body.replace('
', '')
- elif part.get_content_subtype() == 'plain':
- body = content
- elif part.get_content_maintype() in ('application', 'image'):
- if filename :
- attachments.append((filename,part.get_payload(decode=True)))
- else:
- res = part.get_payload(decode=True)
- body += tools.ustr(res, encoding)
-
- msg['body_text'] = body
- msg['attachments'] = attachments
-
- # for backwards compatibility:
- msg['body'] = msg['body_text']
- msg['sub_type'] = msg['content_subtype'] or 'plain'
- return msg
-
- def _postprocess_sent_message(self, cr, uid, message, context=None):
- """Perform any post-processing necessary after sending ``message``
- successfully, including deleting it completely along with its
- attachment if the ``auto_delete`` flag of the message was set.
- Overridden by subclasses for extra post-processing behaviors.
-
- :param browse_record message: the message that was just sent
- :return: True
- """
- if message.auto_delete:
- self.pool.get('ir.attachment').unlink(cr, uid,
- [x.id for x in message.attachment_ids
- if x.res_model == self._name and x.res_id == message.id],
- context=context)
- message.unlink()
- return True
-
- def send(self, cr, uid, ids, auto_commit=False, context=None):
- """Sends the selected emails immediately, ignoring their current
- state (mails that have already been sent should not be passed
- unless they should actually be re-sent).
- Emails successfully delivered are marked as 'sent', and those
- that fail to be deliver are marked as 'exception', and the
- corresponding error message is output in the server logs.
-
- :param bool auto_commit: whether to force a commit of the message
- status after sending each message (meant
- only for processing by the scheduler),
- should never be True during normal
- transactions (default: False)
- :return: True
- """
- ir_mail_server = self.pool.get('ir.mail_server')
- self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
- for message in self.browse(cr, uid, ids, context=context):
- try:
- attachments = []
- for attach in message.attachment_ids:
- attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
-
- body = message.body_html if message.content_subtype == 'html' else message.body_text
- body_alternative = None
- content_subtype_alternative = None
- if message.content_subtype == 'html' and message.body_text:
- # we have a plain text alternative prepared, pass it to
- # build_message instead of letting it build one
- body_alternative = message.body_text
- content_subtype_alternative = 'plain'
-
- # handle destination_partners
- partner_ids_email_to = ''
- for partner in message.partner_ids:
- partner_ids_email_to += '%s ' % (partner.email or '')
- message_email_to = '%s %s' % (partner_ids_email_to, message.email_to or '')
-
- # build an RFC2822 email.message.Message object and send it
- # without queuing
- msg = ir_mail_server.build_email(
- email_from=message.email_from,
- email_to=mail_tools_to_email(message_email_to),
- subject=message.subject,
- body=body,
- body_alternative=body_alternative,
- email_cc=mail_tools_to_email(message.email_cc),
- email_bcc=mail_tools_to_email(message.email_bcc),
- reply_to=message.reply_to,
- attachments=attachments, message_id=message.message_id,
- references = message.references,
- object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)),
- subtype=message.content_subtype,
- subtype_alternative=content_subtype_alternative,
- headers=message.headers and ast.literal_eval(message.headers))
- res = ir_mail_server.send_email(cr, uid, msg,
- mail_server_id=message.mail_server_id.id,
- context=context)
- if res:
- message.write({'state':'sent', 'message_id': res, 'email_to': message_email_to})
- else:
- message.write({'state':'exception', 'email_to': message_email_to})
- message.refresh()
- if message.state == 'sent':
- self._postprocess_sent_message(cr, uid, message, context=context)
- except Exception:
- _logger.exception('failed sending mail.message %s', message.id)
- message.write({'state':'exception'})
-
- if auto_commit == True:
- cr.commit()
- return True
-
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+ default.update(message_id=False, headers=False)
+ return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml
index 0aa5261d7e7..914937a34f3 100644
--- a/addons/mail/mail_message_view.xml
+++ b/addons/mail/mail_message_view.xml
@@ -2,15 +2,15 @@
-
- mail.message.tree.short
+
+ mail.message.treemail.message20
-
+
@@ -18,8 +18,8 @@
-
- mail.message.form.short
+
+ mail.message.formmail.message20
@@ -28,10 +28,9 @@
-
+
-
@@ -40,45 +39,22 @@
-
-
-
-
-
-
-
-
+
-
-
- mail.message.search.short
- mail.message
- 20
-
-
-
-
-
-
-
-
-
-
- mail.message.search.wall
+
+ mail.message.searchmail.message25
-
+
@@ -88,159 +64,17 @@
-
-
-
+
-
- mail.message.form
- mail.message
-
-
\n')
+ _mail_bodyalt2 = 'Pigs rules\nAdmin'
+ _attachments = [('First', 'My first attachment'), ('Second', 'My second attachment')]
+
+ # CASE1: post comment, body and subject specified
+ msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment')
+ message = self.mail_message.browse(cr, uid, msg_id)
+ sent_email = self._build_email_kwargs
+ # Test: notifications have been deleted
+ self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id)]), 'mail.mail notifications should have been auto-deleted!')
+ # Test: mail_message: subject is _subject, body is _body1 (no formatting done)
+ self.assertEqual(message.subject, _subject, 'mail.message subject incorrect')
+ self.assertEqual(message.body, _body1, 'mail.message body incorrect')
+ # Test: sent_email: email send by server: correct subject, body; body_alternative
+ self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect')
+ self.assertEqual(sent_email['body'], _mail_body1, 'sent_email body incorrect')
+ self.assertEqual(sent_email['body_alternative'], _mail_bodyalt1, 'sent_email body_alternative is incorrect')
+ # Test: mail_message: partner_ids = group followers
+ message_pids = set([partner.id for partner in message.partner_ids])
+ test_pids = set([p_a_id, p_b_id, p_c_id])
+ self.assertEqual(test_pids, message_pids, 'mail.message partners incorrect')
+ # Test: notification linked to this message = group followers = partner_ids
+ notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
+ notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
+ self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
+ # Test: sent_email: email_to should contain b@b, not c@c (pref email), not a@a (writer)
+ self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect')
+
+ # CASE2: post an email with attachments, parent_id, partner_ids
+ # TESTS: automatic subject, signature in body_html, attachments propagation
+ msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email',
+ partner_ids=[(6, 0, [p_d_id])], parent_id=msg_id, attachments=_attachments)
+ message = self.mail_message.browse(cr, uid, msg_id2)
+ sent_email = self._build_email_kwargs
+ self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id2)]), 'mail.mail notifications should have been auto-deleted!')
+
+ # Test: mail_message: subject is False, body is _body2 (no formatting done), parent_id is msg_id
+ self.assertEqual(message.subject, False, 'mail.message subject incorrect')
+ self.assertEqual(message.body, html_sanitize(_body2), 'mail.message body incorrect')
+ self.assertEqual(message.parent_id.id, msg_id, 'mail.message parent_id incorrect')
+ # Test: sent_email: email send by server: correct subject, body, body_alternative
+ self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect')
+ self.assertEqual(sent_email['body'], _mail_body2, 'sent_email body incorrect')
+ self.assertEqual(sent_email['body_alternative'], _mail_bodyalt2, 'sent_email body_alternative incorrect')
+ # Test: mail_message: partner_ids = group followers
+ message_pids = set([partner.id for partner in message.partner_ids])
+ test_pids = set([p_a_id, p_b_id, p_c_id, p_d_id])
+ self.assertEqual(message_pids, test_pids, 'mail.message partners incorrect')
+ # Test: notifications linked to this message = group followers = partner_ids
+ notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
+ notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
+ self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
+ # Test: sent_email: email_to should contain b@b, c@c, not a@a (writer)
+ self.assertEqual(set(sent_email['email_to']), set(['b@b', 'c@c']), 'sent_email email_to incorrect')
+ # Test: attachments
+ for attach in message.attachment_ids:
+ self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
+ self.assertEqual(attach.res_id, self.group_pigs_id, 'mail.message attachment res_id incorrect')
+ self.assertIn((attach.name, attach.datas.decode('base64')), _attachments,
+ 'mail.message attachment name / data incorrect')
+
+ def test_21_message_compose_wizard(self):
+ """ Tests designed for the mail.compose.message wizard. """
+ cr, uid = self.cr, self.uid
+ mail_compose = self.registry('mail.compose.message')
+ self.res_users.write(cr, uid, [uid], {'signature': 'Admin', 'email': 'a@a'})
+ user_admin = self.res_users.browse(cr, uid, uid)
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ group_bird_id = self.mail_group.create(cr, uid, {'name': 'Bird', 'description': 'Bird resistance'})
+ group_bird = self.mail_group.browse(cr, uid, group_bird_id)
+
+ # Mail data
+ _subject = 'Pigs'
+ _body_text = 'Pigs rules'
+ _msg_reply = 'Re: Pigs'
+ _msg_body = '
Pigs rules
'
+ _attachments = [
+ {'name': 'First', 'datas_fname': 'first.txt', 'datas': 'My first attachment'.encode('base64')},
+ {'name': 'Second', 'datas_fname': 'second.txt', 'datas': 'My second attachment'.encode('base64')}
+ ]
+ _attachments_test = [('first.txt', 'My first attachment'), ('second.txt', 'My second attachment')]
+
+ # Create partners
+ # 0 - Admin
+ p_a_id = user_admin.partner_id.id
+ # 1 - Bert Tartopoils, with email, should receive emails for comments and emails
+ p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
+ # 2 - Carine Poilvache, with email, should never receive emails
+ p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notification_email_send': 'email'})
+ # 3 - Dédé Grosbedon, without email, to test email verification; should receive emails for every message
+ p_d_id = self.res_partner.create(cr, uid, {'name': 'Dédé Grosbedon', 'notification_email_send': 'all'})
+
+ # Subscribe #1
+ group_pigs.message_subscribe([p_b_id])
+
+ # ----------------------------------------
+ # CASE1: comment on group_pigs
+ # ----------------------------------------
+
+ # 1. Comment group_pigs with body_text and subject
+ compose_id = mail_compose.create(cr, uid,
+ {'subject': _subject, 'body_text': _body_text, 'partner_ids': [(4, p_c_id), (4, p_d_id)]},
+ {'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id})
+ compose = mail_compose.browse(cr, uid, compose_id)
+ # Test: mail.compose.message: composition_mode, model, res_id
+ self.assertEqual(compose.composition_mode, 'comment', 'mail.compose.message incorrect composition_mode')
+ self.assertEqual(compose.model, 'mail.group', 'mail.compose.message incorrect model')
+ self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
+
+ # 2. Post the comment, get created message
+ mail_compose.send_mail(cr, uid, [compose_id])
+ group_pigs.refresh()
+ message = group_pigs.message_ids[0]
+ # Test: mail.message: subject, body inside pre
+ self.assertEqual(message.subject, False, 'mail.message incorrect subject')
+ self.assertEqual(message.body, _msg_body, 'mail.message incorrect body')
+ # Test: mail.message: partner_ids = entries in mail.notification: group_pigs fans (a, b) + mail.compose.message partner_ids (c, d)
+ msg_pids = [partner.id for partner in message.partner_ids]
+ test_pids = [p_a_id, p_b_id, p_c_id, p_d_id]
+ notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
+ self.assertEqual(len(notif_ids), 4, 'mail.message: too much notifications created')
+ self.assertEqual(set(msg_pids), set(test_pids), 'mail.message partner_ids incorrect')
+
+ # ----------------------------------------
+ # CASE2: reply to last comment with attachments
+ # ----------------------------------------
+
+ # 1. Update last comment subject, reply with attachments
+ message.write({'subject': _subject})
+ compose_id = mail_compose.create(cr, uid,
+ {'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])]},
+ {'default_composition_mode': 'reply', 'default_model': 'mail.thread', 'default_res_id': self.group_pigs_id, 'default_parent_id': message.id})
+ compose = mail_compose.browse(cr, uid, compose_id)
+ # Test: model, res_id, parent_id, content_subtype
+ self.assertEqual(compose.model, 'mail.group', 'mail.compose.message incorrect model')
+ self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
+ self.assertEqual(compose.parent_id.id, message.id, 'mail.compose.message incorrect parent_id')
+ self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message incorrect content_subtype')
+
+ # 2. Post the comment, get created message
+ mail_compose.send_mail(cr, uid, [compose_id])
+ group_pigs.refresh()
+ message = group_pigs.message_ids[0]
+ # Test: mail.message: subject as Re:.., body in html
+ self.assertEqual(message.subject, _msg_reply, 'mail.message incorrect subject')
+ self.assertIn('Administrator wrote:
Pigs rules
', message.body, 'mail.message body is incorrect')
+ # Test: mail.message: attachments
+ for attach in message.attachment_ids:
+ self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
+ self.assertEqual(attach.res_id, self.group_pigs_id, 'mail.message attachment res_id incorrect')
+ self.assertIn((attach.name, attach.datas.decode('base64')), _attachments_test,
+ 'mail.message attachment name / data incorrect')
+
+ # ----------------------------------------
+ # CASE3: mass_mail on Pigs and Bird
+ # ----------------------------------------
+
+ # 1. mass_mail on pigs and bird
+ compose_id = mail_compose.create(cr, uid,
+ {'subject': _subject, 'body': '${object.description}'},
+ {'default_composition_mode': 'mass_mail', 'default_model': 'mail.group', 'default_res_id': -1,
+ 'active_ids': [self.group_pigs_id, group_bird_id]})
+ compose = mail_compose.browse(cr, uid, compose_id)
+ # Test: content_subtype is html
+ self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message content_subtype incorrect')
+
+ # 2. Post the comment, get created message for each group
+ mail_compose.send_mail(cr, uid, [compose_id],
+ context={'default_res_id': -1, 'active_ids': [self.group_pigs_id, group_bird_id]})
+ group_pigs.refresh()
+ group_bird.refresh()
+ message1 = group_pigs.message_ids[0]
+ message2 = group_bird.message_ids[0]
+ # Test: Pigs and Bird did receive their message
+ test_msg_ids = self.mail_message.search(cr, uid, [], limit=2)
+ self.assertIn(message1.id, test_msg_ids, 'Pigs did not receive its mass mailing message')
+ self.assertIn(message2.id, test_msg_ids, 'Bird did not receive its mass mailing message')
+ # Test: mail.message: subject, body
+ self.assertEqual(message1.subject, _subject, 'mail.message subject incorrect')
+ self.assertEqual(message1.body, group_pigs.description, 'mail.message body incorrect')
+ self.assertEqual(message2.subject, _subject, 'mail.message subject incorrect')
+ self.assertEqual(message2.body, group_bird.description, 'mail.message body incorrect')
+
+ def test_30_message_read(self):
+ """ Tests designed for message_read. """
+ # TDE NOTE: this test is not finished, as the message_read method is not fully specified.
+ # It will be updated as soon as we have fixed specs !
+ cr, uid = self.cr, self.uid
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ def _compare_structures(struct1, struct2, n=0):
+ # print '%scompare structure' % ('\t' * n)
+ self.assertEqual(len(struct1), len(struct2), 'message_read structure number of childs incorrect')
+ for x in range(len(struct1)):
+ # print '%s' % ('\t' * n), struct1[x]['id'], struct2[x]['id'], struct1[x].get('subject') or ''
+ self.assertEqual(struct1[x]['id'], struct2[x]['id'], 'message_read failure %s' % struct1[x].get('subject'))
+ _compare_structures(struct1[x]['child_ids'], struct2[x]['child_ids'], n + 1)
+ # print '%send compare' % ('\t' * n)
+
+ # ----------------------------------------
+ # CASE1: Flattening test
+ # ----------------------------------------
+
+ # Create dummy message structure
+ import copy
+ tree = [{'id': 2, 'child_ids': [
+ {'id': 6, 'child_ids': [
+ {'id': 8, 'child_ids': []},
+ ]},
+ ]},
+ {'id': 1, 'child_ids':[
+ {'id': 7, 'child_ids': [
+ {'id': 9, 'child_ids': []},
+ ]},
+ {'id': 4, 'child_ids': [
+ {'id': 10, 'child_ids': []},
+ {'id': 5, 'child_ids': []},
+ ]},
+ {'id': 3, 'child_ids': []},
+ ]},
+ ]
+ # Test: completely flat
+ new_tree = self.mail_message.message_read_tree_flatten(cr, uid, copy.deepcopy(tree), 0, 0)
+ self.assertEqual(len(new_tree), 10, 'message_read_tree_flatten wrong in flat')
+ # Test: 1 thread level
+ tree_test = [{'id': 2, 'child_ids': [
+ {'id': 8, 'child_ids': []}, {'id': 6, 'child_ids': []},
+ ]},
+ {'id': 1, 'child_ids': [
+ {'id': 10, 'child_ids': []}, {'id': 9, 'child_ids': []},
+ {'id': 7, 'child_ids': []}, {'id': 5, 'child_ids': []},
+ {'id': 4, 'child_ids': []}, {'id': 3, 'child_ids': []},
+ ]},
+ ]
+ new_tree = self.mail_message.message_read_tree_flatten(cr, uid, copy.deepcopy(tree), 0, 1)
+ _compare_structures(new_tree, tree_test)
+ # Test: 2 thread levels
+ new_tree = self.mail_message.message_read_tree_flatten(cr, uid, copy.deepcopy(tree), 0, 2)
+ _compare_structures(new_tree, tree)
+
+ # ----------------------------------------
+ # CASE2: message_read test
+ # ----------------------------------------
+
+ # 1. Add a few messages to pigs group
+ msgid1 = group_pigs.message_post(body='1', subject='1', parent_id=False)
+ msgid2 = group_pigs.message_post(body='2', subject='1-1', parent_id=msgid1)
+ msgid3 = group_pigs.message_post(body='3', subject='1-2', parent_id=msgid1)
+ msgid4 = group_pigs.message_post(body='4', subject='2', parent_id=False)
+ msgid5 = group_pigs.message_post(body='5', subject='1-1-1', parent_id=msgid2)
+ msgid6 = group_pigs.message_post(body='6', subject='2-1', parent_id=msgid4)
+
+ # Test: read all messages flat
+ tree_test = [{'id': msgid6, 'child_ids': []}, {'id': msgid5, 'child_ids': []},
+ {'id': msgid4, 'child_ids': []}, {'id': msgid3, 'child_ids': []},
+ {'id': msgid2, 'child_ids': []}, {'id': msgid1, 'child_ids': []}]
+ tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], thread_level=0, limit=10)
+ _compare_structures(tree, tree_test)
+ # Test: read with 1 level of thread
+ tree_test = [{'id': msgid4, 'child_ids': [{'id': msgid6, 'child_ids': []}, ]},
+ {'id': msgid1, 'child_ids': [
+ {'id': msgid5, 'child_ids': []}, {'id': msgid3, 'child_ids': []},
+ {'id': msgid2, 'child_ids': []},
+ ]},
+ ]
+ tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], thread_level=1, limit=10)
+ _compare_structures(tree, tree_test)
+ # Test: read with 2 levels of thread
+ tree_test = [{'id': msgid4, 'child_ids': [{'id': msgid6, 'child_ids': []}, ]},
+ {'id': msgid1, 'child_ids': [
+ {'id': msgid3, 'child_ids': []},
+ {'id': msgid2, 'child_ids': [{'id': msgid5, 'child_ids': []}, ]},
+ ]},
+ ]
+ tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], thread_level=2, limit=10)
+ _compare_structures(tree, tree_test)
+
+ # 2. Test expandables
+ # TDE FIXME: add those tests when expandables are specified and implemented
+
+ def test_40_needaction(self):
+ """ Tests for mail.message needaction. """
+ cr, uid = self.cr, self.uid
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ user_admin = self.res_users.browse(cr, uid, uid)
+
+ # Demo values: check unread notification = needaction on mail.message
+ notif_ids = self.mail_notification.search(cr, uid, [
+ ('partner_id', '=', user_admin.partner_id.id),
+ ('read', '=', False)
+ ])
+ na_count = self.mail_message._needaction_count(cr, uid, domain=[])
+ self.assertEqual(len(notif_ids), na_count, 'unread notifications count does not match needaction count')
+
+ # Post 4 message on group_pigs
+ for dummy in range(4):
+ group_pigs.message_post(body='My Body')
+
+ # Check there are 4 new needaction on mail.message
+ notif_ids = self.mail_notification.search(cr, uid, [
+ ('partner_id', '=', user_admin.partner_id.id),
+ ('read', '=', False)
+ ])
+ na_count = self.mail_message._needaction_count(cr, uid, domain=[])
+ self.assertEqual(len(notif_ids), na_count, 'unread notifications count does not match needaction count')
+
+ # Check there are 4 needaction on mail.message with particular domain
+ na_count = self.mail_message._needaction_count(cr, uid, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
+ self.assertEqual(na_count, 4, 'posted message count does not match needaction count')
+
+ def test_50_thread_parent_resolution(self):
+ """Verify parent/child relationships are correctly established when processing incoming mails"""
+ cr, uid = self.cr, self.uid
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ msg1 = group_pigs.message_post(body='My Body', subject='1')
+ msg2 = group_pigs.message_post(body='My Body', subject='2')
+ msg1, msg2 = self.mail_message.browse(cr, uid, [msg1, msg2])
+ self.assertTrue(msg1.message_id, "New message should have a proper message_id")
+
+ # Reply to msg1, make sure the reply is properly attached using the various reply identification mechanisms
+ # 1. In-Reply-To header
+ reply_msg = MAIL_TEMPLATE.format(to='Pretty Pigs , other@gmail.com', subject='Re: 1',
+ extra='In-Reply-To: %s' % msg1.message_id)
+ self.mail_thread.message_process(cr, uid, None, reply_msg)
+ # 2. References header
+ reply_msg2 = MAIL_TEMPLATE.format(to='Pretty Pigs , other@gmail.com', subject='Re: Re: 1',
+ extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id)
+ self.mail_thread.message_process(cr, uid, None, reply_msg2)
+ # 3. Subject contains [] + model passed to message+process -> only attached to group, not to mail
+ reply_msg3 = MAIL_TEMPLATE.format(to='Pretty Pigs , other@gmail.com',
+ extra='', subject='Re: [%s] 1' % self.group_pigs_id)
+ self.mail_thread.message_process(cr, uid, 'mail.group', reply_msg3)
+ group_pigs.refresh()
+ msg1.refresh()
+ self.assertEqual(5, len(group_pigs.message_ids), 'group should contain 5 messages')
+ self.assertEqual(2, len(msg1.child_ids), 'msg1 should have 2 children now')
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index fe87b7584e0..3153ccc2069 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -19,417 +19,289 @@
#
##############################################################################
-import ast
+import base64
import re
-
import tools
+
from osv import osv
from osv import fields
from tools.safe_eval import safe_eval as eval
from tools.translate import _
-from ..mail_message import to_email
-
# main mako-like expression pattern
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 level 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 in
- reply to a previous message mode and pre-populated with the original
- quote. If set to 'comment', it means you are writing a new message to
- be attached to a document. If set to 'mass_mail', the wizard is in
- mass mailing 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 depends on the composition_mode field:
+ - '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.common'
+ _inherit = 'mail.message'
_description = 'Email composition wizard'
+ _log_access = True
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,
- - 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,
- - mass_mailing mode: active_model, active_id = model and ID of a
- document we are commenting,
-
- :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
+ - default_parent_id or message_id or active_id: ID of the
+ mail.message we reply to
+ - message.res_model or default_model
+ - message.res_id or default_res_id
+ - mass_mail: model and IDs of records the user mass-mails
+ - active_ids: record IDs
+ - default_model or active_model
"""
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('default_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'))
+ message_id = context.get('default_parent_id', context.get('message_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 == 'reply':
+ vals = self.get_message_data(cr, uid, message_id, context=context)
+ elif composition_mode == 'comment' and model and res_id:
+ vals = self.get_record_data(cr, uid, model, res_id, context=context)
+ elif composition_mode == 'mass_mail' and model and active_ids:
+ vals = {'model': model, 'res_id': res_id, 'content_subtype': 'html'}
+ 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
-
- # Try to provide default email_from if not specified yet
- if not result.get('email_from'):
- current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
- result['email_from'] = current_user.email or False
return result
+ def _get_composition_mode_selection(self, cr, uid, context=None):
+ return [('comment', 'Comment a document'), ('reply', 'Reply to a message'), ('mass_mail', 'Mass mailing')]
+
_columns = {
- 'dest_partner_ids': fields.many2many('res.partner',
- 'email_message_send_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','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'),
- 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
+ 'composition_mode': fields.selection(
+ lambda s, *a, **k: s._get_composition_mode_selection(*a, **k),
+ string='Composition mode'),
+ 'partner_ids': fields.many2many('res.partner',
+ 'mail_compose_message_res_partner_rel',
+ 'wizard_id', 'partner_id', 'Additional contacts'),
+ 'attachment_ids': fields.many2many('ir.attachment',
+ 'mail_compose_message_ir_attachments_rel',
+ 'wizard_id', 'attachment_id', 'Attachments'),
'filter_id': fields.many2one('ir.filters', 'Filters'),
+ 'body_text': fields.text('Plain-text Contents'),
+ 'content_subtype': fields.char('Message content subtype', size=32, readonly=1,
+ help="Type of message, usually 'html' or 'plain', used to select "\
+ "plain-text or rich-text contents accordingly"),
}
- def get_value(self, cr, uid, model, res_id, context=None):
+ _defaults = {
+ 'composition_mode': 'comment',
+ 'content_subtype': lambda self, cr, uid, ctx={}: 'plain',
+ 'body_text': lambda self, cr, uid, ctx={}: False,
+ 'body': lambda self, cr, uid, ctx={}: '',
+ 'subject': lambda self, cr, uid, ctx={}: False,
+ }
+
+ def notify(self, cr, uid, newid, context=None):
+ """ Override specific notify method of mail.message, because we do
+ not want that feature in the wizard. """
+ return
+
+ 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
"""
- result = {}
- user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
- result.update({
- 'model': model,
- 'res_id': res_id,
- 'email_from': user.email or tools.config.get('email_from', False),
- 'body_html': False,
- 'body_text': False,
- 'subject': False,
- 'dest_partner_ids': [],
- })
- return result
-
- def onchange_email_mode(self, cr, uid, ids, value, model, res_id, context=None):
- """ email_mode (values: True or False). This onchange on the email mode
- allows to have some specific behavior when going in email mode, or
- when going out of email mode.
- Basically, dest_partner_ids is reset when going out of email
- mode.
- This method can be overridden for models that want to have their
- specific behavior.
- Note that currently, this onchange is used in mail.js and called
- manually on the form instantiated in the Chatter.
- """
- if not value:
- return {'value': {'dest_partner_ids': []}}
- return {'value': {}}
-
- def onchange_formatting(self, cr, uid, ids, value, model, res_id, context=None):
- """ onchange_formatting (values: True or False). This onchange on the
- formatting allows to have some specific behavior when going in
- formatting mode, or when going out of formatting.
- Basically, subject is reset when going out of formatting mode.
- This method can be overridden for models that want to have their
- specific behavior.
- Note that currently, this onchange is used in mail.js and called
- manually on the form instantiated in the Chatter.
- """
- if not value:
- return {'value': {'subject': False}}
- return {'value': {}}
+ 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 recipient). It should not be
- called unless ``context['mail.compose.message.mode'] == 'reply'``.
+ of the initial message, and the correct recipients).
: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 not message_id:
+ return {}
if context is None:
context = {}
- result = {}
- if not message_id:
- return result
+ message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context=context)
- 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:")
+ # 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)
- # 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_text = message_data.body_text or ''
- body_html = message_data.body_html or ''
- quoted_body_text = '> %s' % tools.ustr(body_text.replace('\n', "\n> ") or '')
- quoted_body_html = '
%s%s %s %s' % (sent_date, sender, quoted_body_html, current_user.signature)
- # form dest_partner_ids
- dest_partner_ids = [partner.id for partner in message_data.partner_ids]
- # Update header and references
- reply_headers = {}
- reply_references = message_data.references and tools.ustr(message_data.references) or False
- reply_message_id = message_data.message_id or False
- if reply_message_id:
- reply_references = (reply_references or '') + " " + message_data.message_id
- reply_headers['In-Reply-To'] = message_data.message_id
+ # create the reply in the body
+ reply_body = _('
On %(date)s, %(sender_name)s wrote:
%(body)s
') % {
+ 'date': message_data.date if message_data.date else '',
+ 'sender_name': message_data.author_id.name,
+ 'body': message_data.body,
+ }
+ # 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({
- 'body_text': reply_body_text,
- 'body_html': reply_body_html,
+ result = {
+ 'model': message_data.model,
+ 'res_id': message_data.res_id,
+ 'parent_id': message_data.id,
+ 'body': reply_body,
'subject': reply_subject,
- 'attachment_ids': [],
- 'dest_partner_ids': dest_partner_ids,
- 'model': message_data.model or False,
- 'res_id': message_data.res_id or False,
- 'email_from': current_user.email or message_data.email_to or False,
- 'email_to': message_data.reply_to or message_data.email_from or False,
- 'email_cc': message_data.email_cc or False,
- 'user_id': uid,
- # pass msg-id and references of mail we're replying to, to construct the
- # new ones later when sending
- 'message_id': reply_message_id,
- 'references': reply_references,
- 'headers': reply_headers,
- })
+ 'partner_ids': partner_ids,
+ 'content_subtype': 'html',
+ }
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.
+ 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 True
- :param dict context: several context values will modify the behavior
- of the wizard, cfr. the class description.
- '''
+ def onchange_content_subtype(self, cr, uid, ids, value, model, res_id, context=None):
+ """ onchange_content_subtype (values: 'plain' or 'html'). This onchange
+ on the subtype allows to have some specific behavior when switching
+ between text or html mode.
+ This method can be overridden for models that want to have their
+ specific behavior. """
+ return {'value': {'content_subtype': value}}
+
+ def _verify_partner_email(self, cr, uid, partner_ids, context=None):
+ """ Verify that selected partner_ids have an email_address defined.
+ Otherwise throw a warning. """
+ partner_wo_email_lst = []
+ for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
+ if not partner.email:
+ partner_wo_email_lst.append(partner)
+ if not partner_wo_email_lst:
+ return {}
+ warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
+ for partner in partner_wo_email_lst:
+ warning_msg += '\n- %s' % (partner.name)
+ return {'warning': {
+ 'title': _('Partners email addresses not found'),
+ 'message': warning_msg,
+ }
+ }
+
+ def onchange_partner_ids(self, cr, uid, ids, value, context=None):
+ """ onchange_partner_ids (value format: [[6, 0, [3, 4]]]). The
+ basic purpose of this method is to check that destination partners
+ effectively have email addresses. Otherwise a warning is thrown.
+ """
+ res = {'value': {}}
+ if not value or not value[0] or not value[0][0] == 6:
+ return
+ res.update(self._verify_partner_email(cr, uid, value[0][2], context=context))
+ return res
+
+ def unlink(self, cr, uid, ids, context=None):
+ # Cascade delete all attachments, as they are owned by the composition wizard
+ for wizard in self.read(cr, uid, ids, ['attachment_ids'], context=context):
+ self.pool.get('ir.attachment').unlink(cr, uid, wizard['attachment_ids'], context=context)
+ return super(mail_compose_message, self).unlink(cr, uid, ids, context=context)
+
+ def dummy(self, cr, uid, ids, context=None):
+ """ 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
+
+ #------------------------------------------------------
+ # Wizard validation and send
+ #------------------------------------------------------
+
+ def send_mail(self, cr, uid, ids, context=None):
+ """ 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 = {}
- # composition wizard options
- email_mode = context.get('email_mode')
- formatting = context.get('formatting')
- mass_mail_mode = context.get('mail.compose.message.mode') == 'mass_mail'
+ active_ids = context.get('active_ids')
- mail_message_obj = self.pool.get('mail.message')
- for mail_wiz in self.browse(cr, uid, ids, context=context):
- # attachments
- attachment = {}
- for attach in mail_wiz.attachment_ids:
- attachment[attach.datas_fname] = attach.datas and attach.datas.decode('base64')
+ for wizard in self.browse(cr, uid, ids, context=context):
+ mass_mail_mode = wizard.composition_mode == 'mass_mail'
+ active_model_pool = self.pool.get(wizard.model if wizard.model else 'mail.thread')
- # default values, according to the wizard options
- subject = mail_wiz.subject if formatting else False
- content_subtype = 'html' if formatting else 'plain'
- type = 'email' if email_mode else 'comment'
- state = 'outgoing' if email_mode else False
- partner_ids = [partner.id for partner in mail_wiz.dest_partner_ids]
- references = None
- headers = {}
- body = mail_wiz.body_html if content_subtype == 'html' else mail_wiz.body_text
-
- # get model, active_ids, and check if model is openchatter-enabled
- if mass_mail_mode and context.get('active_ids') and context.get('active_model'):
- active_ids = context['active_ids']
- active_model = context['active_model']
- elif mass_mail_mode:
- active_model = mail_wiz.model
- active_model_pool = self.pool.get(active_model)
- active_ids = active_model_pool.search(cr, uid, ast.literal_eval(mail_wiz.filter_id.domain), context=ast.literal_eval(mail_wiz.filter_id.context))
- else:
- active_model = mail_wiz.model
- active_ids = [mail_wiz.res_id]
- active_model_pool = self.pool.get(active_model)
- mail_thread_enabled = hasattr(active_model_pool, 'message_append')
-
- if context.get('mail.compose.message.mode') == 'mass_mail':
- # Mass mailing: must render the template patterns
- for active_id in active_ids:
- rendered_subject = self.render_template(cr, uid, subject, active_model, active_id)
- rendered_body_html = self.render_template(cr, uid, mail_wiz.body_html, active_model, active_id)
- rendered_body_text = self.render_template(cr, uid, mail_wiz.body_text, active_model, active_id)
- email_from = self.render_template(cr, uid, mail_wiz.email_from, active_model, active_id)
- email_to = self.render_template(cr, uid, mail_wiz.email_to, active_model, active_id)
- email_cc = self.render_template(cr, uid, mail_wiz.email_cc, active_model, active_id)
- email_bcc = self.render_template(cr, uid, mail_wiz.email_bcc, active_model, active_id)
- reply_to = self.render_template(cr, uid, mail_wiz.reply_to, active_model, active_id)
-
- # in mass-mailing mode we only schedule the mail for sending, it will be
- # processed as soon as the mail scheduler runs.
- if mail_thread_enabled:
- active_model_pool.message_append(cr, uid, [active_id], rendered_subject, rendered_body_text, rendered_body_html,
- type=type, content_subtype=content_subtype, state=state, partner_ids=partner_ids,
- email_from=email_from, email_to=email_to, email_cc=email_cc, email_bcc=email_bcc,
- reply_to=reply_to, references=references, attachments=attachment, headers=headers, context=context)
- else:
- mail_message_obj.schedule_with_attach(cr, uid, email_from, to_email(email_to), subject, rendered_body_text,
- model=mail_wiz.model, email_cc=to_email(email_cc), email_bcc=to_email(email_bcc), reply_to=reply_to,
- attachments=attachment, references=references, res_id=active_id, partner_ids=partner_ids,
- content_subtype=mail_wiz.content_subtype, headers=headers, context=context)
- else:
- # normal mode - no mass-mailing
- if mail_thread_enabled:
- msg_ids = active_model_pool.message_append(cr, uid, active_ids, subject, mail_wiz.body_text, mail_wiz.body_html,
- type=type, content_subtype=content_subtype, state=state, partner_ids=partner_ids,
- email_from=mail_wiz.email_from, email_to=mail_wiz.email_to, email_cc=mail_wiz.email_cc, email_bcc=mail_wiz.email_bcc,
- reply_to=mail_wiz.reply_to, references=references, attachments=attachment, headers=headers, context=context)
- else:
- msg_ids = [mail_message_obj.schedule_with_attach(cr, uid, mail_wiz.email_from, to_email(mail_wiz.email_to), subject, mail_wiz.body_text,
- type=type, model=mail_wiz.model, email_cc=to_email(mail_wiz.email_cc), email_bcc=to_email(mail_wiz.email_bcc), reply_to=mail_wiz.reply_to,
- attachments=attachment, references=references, res_id=int(mail_wiz.res_id), partner_ids=partner_ids,
- content_subtype=mail_wiz.content_subtype, headers=headers, context=context)]
- # in normal mode, we send the email immediately, as the user expects us to (delay should be sufficiently small)
- if type == 'email':
- mail_message_obj.send(cr, uid, msg_ids, context=context)
+ # wizard works in batch mode: [res_id] or active_ids
+ res_ids = active_ids if mass_mail_mode and wizard.model and active_ids else [wizard.res_id]
+ for res_id in res_ids:
+ # default values, according to the wizard options
+ post_values = {
+ 'subject': wizard.subject if wizard.content_subtype == 'html' else False,
+ 'body': wizard.body if wizard.content_subtype == 'html' else '
%s
' % tools.ustr(wizard.body_text),
+ 'partner_ids': [(4, partner.id) for partner in wizard.partner_ids],
+ 'attachments': [(attach.datas_fname or attach.name, base64.b64decode(attach.datas)) for attach in wizard.attachment_ids],
+ }
+ # mass mailing: render and override default values
+ if mass_mail_mode and wizard.model:
+ email_dict = self.render_message(cr, uid, wizard, res_id, context=context)
+ new_partner_ids = email_dict.pop('partner_ids', [])
+ post_values['partner_ids'] += [(4, partner_id) for partner_id in new_partner_ids]
+ new_attachments = email_dict.pop('attachments', [])
+ post_values['attachments'] += new_attachments
+ post_values.update(email_dict)
+ # post the message
+ active_model_pool.message_post(cr, uid, [res_id], type='comment', context=context, **post_values)
return {'type': 'ir.actions.act_window_close'}
+ def render_message(self, cr, uid, wizard, res_id, context=None):
+ """ Generate an email from the template for given (wizard.model, res_id)
+ pair. This method is meant to be inherited by email_template that
+ will produce a more complete dictionary. """
+ return {
+ 'subject': self.render_template(cr, uid, wizard.subject, wizard.model, res_id, context),
+ 'body': self.render_template(cr, uid, wizard.body, wizard.model, res_id, context),
+ }
+
def render_template(self, cr, uid, template, model, res_id, context=None):
- """Render the given template text, replace mako-like expressions ``${expr}``
- with the result of evaluating these expressions with an evaluation context
- containing:
+ """ Render the given template text, replace mako-like expressions ``${expr}``
+ with the result of evaluating these expressions with an evaluation context
+ containing:
* ``user``: browse_record of the current user
* ``object``: browse_record of the document record this mail is
related to
* ``context``: the context passed to the mail composition wizard
- :param str template: the template text to render
- :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 str template: the template text to render
+ :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.
"""
if context is None:
context = {}
def merge(match):
exp = str(match.group()[2:-1]).strip()
- result = eval(exp,
- {
- 'user' : self.pool.get('res.users').browse(cr, uid, uid, context=context),
- 'object' : self.pool.get(model).browse(cr, uid, res_id, context=context),
- 'context': dict(context), # copy context to prevent side-effects of eval
- })
- if result in (None, False):
- return ""
- return tools.ustr(result)
+ result = eval(exp, {
+ 'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
+ 'object': self.pool.get(model).browse(cr, uid, res_id, context=context),
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ })
+ return result and tools.ustr(result) or ''
return template and EXPRESSION_PATTERN.sub(merge, template)
-
-
-class mail_compose_message_extended(osv.TransientModel):
- """ Extension of 'mail.compose.message' to support default field values related
- to CRM-like models that follow the following conventions:
-
- 1. The model object must have an attribute '_mail_compose_message' equal to True.
-
- 2. The model should define the following fields:
- - 'name' as subject of the message (required);
- - 'email_from' as destination email address (required);
- - 'email_cc' as cc email addresses (required);
- - 'section_id.reply_to' as reply-to address (optional).
- """
- _inherit = 'mail.compose.message'
-
- def get_value(self, cr, uid, model, res_id, context=None):
- """ Overrides the default implementation to provide more default field values
- related to the corresponding CRM case.
- """
- result = super(mail_compose_message_extended, self).get_value(cr, uid, model, res_id, context=context)
- model_obj = self.pool.get(model)
- if getattr(model_obj, '_mail_compose_message', False) and res_id:
- data = model_obj.browse(cr, uid , res_id, context)
- result.update({
- 'email_to': data.email_from or False,
- 'email_cc': tools.ustr(data.email_cc or ''),
- 'subject': data.name or False,
- })
- if hasattr(data, 'section_id'):
- result['reply_to'] = data.section_id and data.section_id.reply_to or False
- return result
-
- def onchange_email_mode(self, cr, uid, ids, value, model, res_id, context=None):
- """ Overrides the default implementation to provide default values for
- dest_partner_ids. This method checks that a partner maching the
- ``email_from`` of the record exists. It it does not exist, it
- creates a new partner. The found or created partner is then added
- in dest_partner_ids.
- Partner check/creation valid inly if the value is True, and if
- the model has the ``_mail_compose_message`` attribute.
- """
- result = super(mail_compose_message_extended, self).onchange_email_mode(cr, uid, ids, value, model, res_id, context=context)
- model_obj = self.pool.get(model)
- if not value or not (getattr(model_obj, '_mail_compose_message', False) and res_id):
- return result
- data = model_obj.browse(cr, uid , res_id, context=context)
- partner_obj = self.pool.get('res.partner')
- partner_ids = partner_obj.search(cr, uid, [('email', '=', data.email_from)], context=context)
- if partner_ids:
- partner_id = partner_ids[0]
- else:
- partner_id = partner_obj.name_create(cr, uid, data.email_from, context=context)[0]
- result['value'].update({
- 'dest_partner_ids': [partner_id],
- 'email_cc': tools.ustr(data.email_cc or ''),
- })
- if hasattr(data, 'section_id'):
- result['value']['reply_to'] = data.section_id and data.section_id.reply_to or False
- return result
-
- def onchange_formatting(self, cr, uid, ids, value, model, res_id, context=None):
- """ Overrides the default implementation to provide default values for
- the subject.
- Subject re-creation valid only if the value is True, and if the
- model has the ``_mail_compose_message`` attribute.
- """
- result = super(mail_compose_message_extended, self).onchange_formatting(cr, uid, ids, value, model, res_id, context=context)
- model_obj = self.pool.get(model)
- if not value or not (getattr(model_obj, '_mail_compose_message', False) and res_id):
- return result
- data = model_obj.browse(cr, uid , res_id, context=context)
- result['value'].update({
- 'subject': data.name or False,
- })
- return result
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index 73fb03cdc28..70b50c154ac 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -6,73 +6,87 @@
mail.compose.message
@@ -97,6 +111,6 @@
target="new"
key2="client_action_multi"
id="base.action_partner_mass_mail"
- context="{'mail.compose.message.mode':'mass_mail'}"/>
+ context="{'default_composition_mode': 'mass_mail'}"/>
diff --git a/addons/marketing_campaign/marketing_campaign_demo.xml b/addons/marketing_campaign/marketing_campaign_demo.xml
index 54897cd111a..92443fe987d 100644
--- a/addons/marketing_campaign/marketing_campaign_demo.xml
+++ b/addons/marketing_campaign/marketing_campaign_demo.xml
@@ -3,29 +3,29 @@
- Template for New Partner
+ welcome new partnerinfo@openerp.com
- Welcome in OpenERP Partner Channel!
+ Welcome to the OpenERP Partner Channel!${object.email or ''}
- Hello, We are very happy to send Welcome message.
+ Hello, you will receive your welcome pack via email shortly.
- Template for Silver Partner
+ congrats silver partnerinfo@openerp.com
- Congratulation! You become now our Silver Partner.
+ Congratulations! You are now a Silver Partner!${object.email or ''}
- Hello, We are happy to announce that you now become our Silver Partner.
+ Hi, we are delighted to welcome you among our Silver Partners as of today!
- Template for Gold Partner
+ congrats gold partnerinfo@openerp.com
- Congratulation! You become our Gold Partner.
+ Congratulations! You are now one of our Gold Partners!${object.email or ''}
- Hello, We are happy to announce that you become our Gold Partner.
+ Hi, we are delighted to let you know that you have entered the select circle of our Gold Partners
@@ -64,7 +64,7 @@
-
+
diff --git a/addons/marketing_campaign/test/marketing_campaign.yml b/addons/marketing_campaign/test/marketing_campaign.yml
index 04063a6fbc1..3d540b4ccb7 100644
--- a/addons/marketing_campaign/test/marketing_campaign.yml
+++ b/addons/marketing_campaign/test/marketing_campaign.yml
@@ -42,7 +42,7 @@
record = self.browse(cr, uid, ids[0])
assert record.state == 'todo' or record.state == 'done' , 'Marketing Workitem shoud be in draft state.'
-
- I check follow-up detail of first activity.
+ I process follow-up of first activity.
-
!python {model: marketing.campaign.workitem}: |
ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
@@ -50,12 +50,6 @@
assert ids, 'Follow-up item is not created for first activity.'
work_item_id = self.browse(cr ,uid ,ids[0] ,context)
assert work_item_id.res_name, 'Resource Name is not defined.'
--
- I process follow-up of first activity.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel')), ('activity_id', '=', ref('marketing_campaign_activity_0'))])
self.process(cr, uid, ids)
record = self.browse(cr, uid, ids)[0]
assert record.state == "done", "Follow-up item should be closed after process."
diff --git a/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml b/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml
index 0946ff6e129..141981a21d6 100644
--- a/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml
+++ b/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml
@@ -40,7 +40,10 @@
- Hello,Thanks for generous interest you have shown in the openERP.Regards,OpenERP Team,
+ Hello,
+
Thanks for the genuine interest you have shown in OpenERP.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>For OpenERP OnDemand Free Trial 2010
@@ -50,11 +53,11 @@
- Hello,We have very good offer that might suit you.
- We propose you to subscribe to the OpenERP Discovery Day on May 2010.
- If any further information required kindly revert back.
- We really appreciate your co-operation on this.
- Regards,OpenERP Team,
+ Hello,
+
We have very good offer that might suit you.
+ We suggest you subscribe to the OpenERP Discovery Day on May 2010.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>For OpenERP Discovery Day on May 2010
@@ -65,10 +68,10 @@
- Hello,Thanks for showing intrest and for subscribing to the OpenERP Discovery Day.
- If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
+ Hello,
+
Thanks for showing interest and for subscribing to the OpenERP Discovery Day.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>For OpenERP Discovery Day
@@ -79,10 +82,10 @@
- Hello,Thanks for showing intrest and buying the OpenERP book.
+ Hello,
+
Thanks for showing interest and buying the OpenERP book.
If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
+
Regards,OpenERP Team,
]]>For OpenERP book
@@ -92,12 +95,12 @@
- Hello, We have very good offer that might suit you.
- For our gold partners,We are arranging free technical training on june,2010.
- If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
- For technical training to Gold partners
+ Hello,
+
We have very good offer that might suit you.
+ For our gold partners,We are arranging free technical training on june,2010.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>
+ technical training to gold partners
@@ -107,12 +110,12 @@
- Hello, We have very good offer that might suit you.
- For our silver partners,We are paid technical training on june,2010.
- If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
- For training to Silver partners
+ Hello,
+
We have very good offer that might suit you.
+ For our silver partners,We are paid technical training on june,2010.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>
+ training to silver partners
@@ -122,12 +125,12 @@
- Hello, We have very good offer that might suit you.
- For our silver partners,We are offering Gold partnership.
- If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
- For gold partnership to silver partners
+ Hello,
+
We have very good offer that might suit you.
+ For our silver partners, we are offering Gold partnership.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>
+ gold partnership to silver partners
@@ -137,9 +140,12 @@
- Hello, Thanks for showing intrest and for subscribing to technical training.If any further information required kindly revert back.I really appreciate your co-operation on this.
- Regards,OpenERP Team,
- For subscribing to technical training
+ Hello,
+
Thanks for showing interest and for subscribing to technical training.
+ If any further information required kindly revert back.I really appreciate your co-operation on this.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>
+ subscribing to technical training
diff --git a/addons/mrp/board_manufacturing_view.xml b/addons/mrp/board_manufacturing_view.xml
index e7668517a70..cae1fea07dc 100644
--- a/addons/mrp/board_manufacturing_view.xml
+++ b/addons/mrp/board_manufacturing_view.xml
@@ -9,7 +9,6 @@
-
diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py
index 7b8442b4c46..c667daadbec 100644
--- a/addons/mrp/mrp.py
+++ b/addons/mrp/mrp.py
@@ -369,7 +369,7 @@ class mrp_bom(osv.osv):
prod_obj = self.pool.get('product.product')
for obj in self.browse(cr, uid, ids, context=context):
for prod in prod_obj.browse(cr, uid, [obj.product_id], context=context):
- self.message_append_note(cr, uid, [obj.id], body=_("Bill of Material has been created for %s product.") % (prod.id.name_template), context=context)
+ self.message_post(cr, uid, [obj.id], body=_("Bill of Material has been created for %s product.") % (prod.id.name_template), context=context)
return True
mrp_bom()
@@ -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.
@@ -1046,33 +1046,28 @@ class mrp_production(osv.osv):
# OpenChatter methods and notifications
# ---------------------------------------------------
- def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
- """ Add 'user_id' to the monitored fields """
- res = super(mrp_production, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
- return res + ['user_id']
-
def create_send_note(self, cr, uid, ids, context=None):
- self.message_append_note(cr, uid, ids, body=_("Manufacturing order has been created."), context=context)
+ self.message_post(cr, uid, ids, body=_("Manufacturing order has been created."), context=context)
return True
def action_cancel_send_note(self, cr, uid, ids, context=None):
message = _("Manufacturing order has been canceled.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def action_ready_send_note(self, cr, uid, ids, context=None):
message = _("Manufacturing order is ready to produce.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def action_in_production_send_note(self, cr, uid, ids, context=None):
message = _("Manufacturing order is in production.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def action_done_send_note(self, cr, uid, ids, context=None):
message = _("Manufacturing order has been done.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def action_confirm_send_note(self, cr, uid, ids, context=None):
@@ -1082,7 +1077,7 @@ class mrp_production(osv.osv):
obj_datetime = fields.DT.datetime.strptime(obj.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
obj_date_str = fields.datetime.context_timestamp(cr, uid, obj_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
message = _("Manufacturing order has been confirmed and is scheduled for the %s.") % (obj_date_str)
- self.message_append_note(cr, uid, [obj.id], body=message, context=context)
+ self.message_post(cr, uid, [obj.id], body=message, context=context)
return True
diff --git a/addons/mrp/mrp_data.xml b/addons/mrp/mrp_data.xml
index e5d5380f96a..0bbf2d3b64c 100644
--- a/addons/mrp/mrp_data.xml
+++ b/addons/mrp/mrp_data.xml
@@ -2,14 +2,16 @@
-
-
-
- Module MRP has been installed
- Manage your manufacturing process in OpenERP by defining bill of materials (BoM), routings and work centers. This module supports complete integration and planification of stockable goods, consumable, and services.
+
+ mail.group
+
+ notification
+ MRP application installed!
+ Manage your manufacturing process with OpenERP by defining your bills of materials (BoM), routings and work centers.
+This application supports complete integration and production scheduling for stockable goods, consumables, and services.
-From the Manufacturing Settings, you can choose to compute production schedules periodically or just-in-time.
-
+From the Manufacturing Settings, you can choose to compute production schedules periodically or just-in-time.
+
Production order
diff --git a/addons/mrp/mrp_demo.xml b/addons/mrp/mrp_demo.xml
index d0f05a2aaff..952a5e0e3de 100644
--- a/addons/mrp/mrp_demo.xml
+++ b/addons/mrp/mrp_demo.xml
@@ -49,7 +49,7 @@
0.05
-
+
diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml
index acb51ea6388..8121f6e84d1 100644
--- a/addons/mrp/mrp_view.xml
+++ b/addons/mrp/mrp_view.xml
@@ -579,8 +579,8 @@
mrp.production.treemrp.production
-
-
+
+
diff --git a/addons/mrp/procurement.py b/addons/mrp/procurement.py
index 363f98e35d1..7f417dc18b1 100644
--- a/addons/mrp/procurement.py
+++ b/addons/mrp/procurement.py
@@ -43,7 +43,7 @@ class procurement_order(osv.osv):
cr.execute('update procurement_order set message=%s where id=%s', (_('No BoM defined for this product !'), procurement.id))
for (id, name) in self.name_get(cr, uid, procurement.id):
message = _("Procurement '%s' has an exception: 'No BoM defined for this product !'") % name
- self.message_append_note(cr, uid, [procurement.id], body=message, context=context)
+ self.message_post(cr, uid, [procurement.id], body=message, context=context)
return False
return True
diff --git a/addons/mrp_operations/mrp_operations.py b/addons/mrp_operations/mrp_operations.py
index 8692dab5c87..ee2547bc7df 100644
--- a/addons/mrp_operations/mrp_operations.py
+++ b/addons/mrp_operations/mrp_operations.py
@@ -224,7 +224,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order has been created for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
def action_start_send_note(self, cr, uid, ids, context=None):
@@ -232,7 +232,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order has been started for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
def action_done_send_note(self, cr, uid, ids, context=None):
@@ -240,7 +240,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order has been done for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
def action_pending_send_note(self, cr, uid, ids, context=None):
@@ -248,7 +248,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order is pending for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
def action_cancel_send_note(self, cr, uid, ids, context=None):
@@ -256,7 +256,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order has been cancelled for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
mrp_production_workcenter_line()
diff --git a/addons/mrp_repair/mrp_repair.py b/addons/mrp_repair/mrp_repair.py
index f41abb58c08..c1ac8f809ce 100644
--- a/addons/mrp_repair/mrp_repair.py
+++ b/addons/mrp_repair/mrp_repair.py
@@ -571,40 +571,40 @@ class mrp_repair(osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for repair in self.browse(cr, uid, ids, context):
message = _("Repair Order for %s has been created." % (repair.product_id.name))
- self.message_append_note(cr, uid, [repair.id], body=message, context=context)
+ self.message_post(cr, uid, [repair.id], body=message, context=context)
return True
def set_start_send_note(self, cr, uid, ids, context=None):
for repair in self.browse(cr, uid, ids, context):
message = _("Repair Order for %s has been started." % (repair.product_id.name))
- self.message_append_note(cr, uid, [repair.id], body=message, context=context)
+ self.message_post(cr, uid, [repair.id], body=message, context=context)
return True
def set_toinvoiced_send_note(self, cr, uid, ids, context=None):
for repair in self.browse(cr, uid, ids, context):
message = _("Draft Invoice of %s %s waiting for validation.") % (repair.invoice_id.amount_total, repair.invoice_id.currency_id.symbol)
- self.message_append_note(cr, uid, [repair.id], body=message, context=context)
+ self.message_post(cr, uid, [repair.id], body=message, context=context)
return True
def set_confirm_send_note(self, cr, uid, ids, context=None):
for repair in self.browse(cr, uid, ids, context):
message = _( "Repair Order for %s has been accepted." % (repair.product_id.name))
- self.message_append_note(cr, uid, [repair.id], body=message, context=context)
+ self.message_post(cr, uid, [repair.id], body=message, context=context)
return True
def set_cancel_send_note(self, cr, uid, ids, context=None):
message = _("Repair has been cancelled.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def set_ready_send_note(self, cr, uid, ids, context=None):
message = _("Repair Order is now ready to repair.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def set_done_send_note(self, cr, uid, ids, context=None):
message = _("Repair Order is closed.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
mrp_repair()
diff --git a/addons/multi_company/multi_company_demo.xml b/addons/multi_company/multi_company_demo.xml
index 462a355e789..d120550e3b9 100644
--- a/addons/multi_company/multi_company_demo.xml
+++ b/addons/multi_company/multi_company_demo.xml
@@ -52,47 +52,12 @@
-
- editor_demo
- OpenERP Editor
- editor_demo
-
-
-
+
+
-
- sales_demo_us
- OpenERP Salesman US
- sales_demo_us
-
-
-
+
+
-
- sales_demo_be
- OpenERP Salesman BE
- sales_demo_be
-
-
-
-
-
- sales_demo_in
- OpenERP Salesman IN
- sales_demo_in
-
-
-
-
-
- odoo_demo
- Odoo
- odoo_demo
-
-
-
-
-