diff --git a/addons/account/account.py b/addons/account/account.py
index acf27c42392..8c7f058acc6 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -1150,6 +1150,29 @@ class account_move(osv.osv):
_description = "Account Entry"
_order = 'id desc'
+ def account_move_prepare(self, cr, uid, journal_id, date=False, ref='', company_id=False, context=None):
+ '''
+ Prepares and returns a dictionary of values, ready to be passed to create() based on the parameters received.
+ '''
+ if not date:
+ date = fields.date.today()
+ period_obj = self.pool.get('account.period')
+ if not company_id:
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ company_id = user.company_id.id
+ if context is None:
+ context = {}
+ #put the company in context to find the good period
+ ctx = context.copy()
+ ctx.update({'company_id': company_id})
+ return {
+ 'journal_id': journal_id,
+ 'date': date,
+ 'period_id': period_obj.find(cr, uid, date, context=ctx)[0],
+ 'ref': ref,
+ 'company_id': company_id,
+ }
+
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
"""
Returns a list of tupples containing id, name, as internally it is called {def name_get}
@@ -1850,6 +1873,13 @@ class account_tax(osv.osv):
return result in the context
Ex: result=round(price_unit*0.21,4)
"""
+ def copy_data(self, cr, uid, id, default=None, context=None):
+ if default is None:
+ default = {}
+ name = self.read(cr, uid, id, ['name'], context=context)['name']
+ default = default.copy()
+ default.update({'name': name + _(' (Copy)')})
+ return super(account_tax, self).copy_data(cr, uid, id, default=default, context=context)
def get_precision_tax():
def change_digit_tax(cr):
diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py
index dbe17d1c60a..d771bc59506 100644
--- a/addons/account/account_invoice.py
+++ b/addons/account/account_invoice.py
@@ -22,6 +22,7 @@
import time
from lxml import etree
import openerp.addons.decimal_precision as dp
+import openerp.exceptions
from openerp import pooler
from openerp.osv import fields, osv, orm
@@ -302,16 +303,7 @@ class account_invoice(osv.osv):
('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
]
- def _find_partner(self, inv):
- '''
- Find the partner for which the accounting entries will be created
- '''
- #if the chosen partner is not a company and has a parent company, use the parent for the journal entries
- #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
- part = inv.partner_id
- if part.parent_id and not part.is_company:
- part = part.parent_id
- return part
+
def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
@@ -981,7 +973,7 @@ class account_invoice(osv.osv):
date = inv.date_invoice or time.strftime('%Y-%m-%d')
- part = self._find_partner(inv)
+ part = self.pool.get("res.partner")._find_accounting_partner(inv.partner_id)
line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part.id, date, context=ctx)),iml)
@@ -1753,6 +1745,16 @@ class res_partner(osv.osv):
'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
}
+ def _find_accounting_partner(self, part):
+ '''
+ Find the partner for which the accounting entries will be created
+ '''
+ #if the chosen partner is not a company and has a parent company, use the parent for the journal entries
+ #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
+ if part.parent_id and not part.is_company:
+ part = part.parent_id
+ return part
+
def copy(self, cr, uid, id, default=None, context=None):
default = default or {}
default.update({'invoice_ids' : []})
diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml
index a37474db0af..8c0854bd6c6 100644
--- a/addons/account/account_invoice_view.xml
+++ b/addons/account/account_invoice_view.xml
@@ -145,8 +145,7 @@
-
-
+
@@ -300,7 +299,7 @@
-
+
@@ -437,7 +436,7 @@
-
+
@@ -448,11 +447,11 @@
account.invoice
-
-
-
-
-
+
+
+
+
+
diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py
index f867688f179..a96ed889aae 100644
--- a/addons/account/account_move_line.py
+++ b/addons/account/account_move_line.py
@@ -780,7 +780,7 @@ class account_move_line(osv.osv):
else:
currency_id = line.company_id.currency_id
if line.reconcile_id:
- raise osv.except_osv(_('Warning!'), _('Already reconciled.'))
+ raise osv.except_osv(_('Warning'), _("Journal Item '%s' (id: %s), Move '%s' is already reconciled!") % (line.name, line.id, line.move_id.name))
if line.reconcile_partial_id:
for line2 in line.reconcile_partial_id.line_partial_ids:
if not line2.reconcile_id:
diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml
index b4260db6b90..b8dcefdd3a4 100644
--- a/addons/account/account_view.xml
+++ b/addons/account/account_view.xml
@@ -161,7 +161,7 @@
-
+
@@ -435,15 +435,15 @@
-
+
-
+
-
-
+
+
@@ -552,7 +552,7 @@
-
+
@@ -1206,6 +1206,8 @@
+
+
@@ -1256,7 +1258,7 @@
- 15 Daysbalance
@@ -48,7 +47,6 @@
- 30 Net Daysbalance
diff --git a/addons/account/demo/account_demo.xml b/addons/account/demo/account_demo.xml
index 6918ad44702..8a450c0b6aa 100644
--- a/addons/account/demo/account_demo.xml
+++ b/addons/account/demo/account_demo.xml
@@ -135,7 +135,6 @@
30 Days End of Month
- 30 Days End of Monthbalance
@@ -147,16 +146,13 @@
30% Advance End 30 Days
- 30% Advanceprocent
-
- Remaining Balancebalance
diff --git a/addons/account/edi/invoice_action_data.xml b/addons/account/edi/invoice_action_data.xml
index 18ac71f55ce..b3fe8362b8b 100644
--- a/addons/account/edi/invoice_action_data.xml
+++ b/addons/account/edi/invoice_action_data.xml
@@ -24,7 +24,7 @@
Invoice - Send by Email${object.user_id.email or object.company_id.email or 'noreply@localhost'}${object.company_id.name} Invoice (Ref ${object.number or 'n/a'})
- ${object.partner_id.id}
+ ${object.partner_id.id}
diff --git a/addons/account/partner_view.xml b/addons/account/partner_view.xml
index 8623bd03922..1267e20165b 100644
--- a/addons/account/partner_view.xml
+++ b/addons/account/partner_view.xml
@@ -95,6 +95,7 @@
+
diff --git a/addons/account/project/project_view.xml b/addons/account/project/project_view.xml
index 62c8fb37075..aa567d2fc79 100644
--- a/addons/account/project/project_view.xml
+++ b/addons/account/project/project_view.xml
@@ -131,6 +131,7 @@
+
@@ -169,6 +170,7 @@
+
@@ -176,7 +178,6 @@
-
diff --git a/addons/account/report/account_financial_report.py b/addons/account/report/account_financial_report.py
index d94955a764a..864c4bbc7d1 100644
--- a/addons/account/report/account_financial_report.py
+++ b/addons/account/report/account_financial_report.py
@@ -65,7 +65,7 @@ class report_account_common(report_sxw.rml_parse, common_report_header):
vals['debit'] = report.debit
vals['credit'] = report.credit
if data['form']['enable_filter']:
- vals['balance_cmp'] = self.pool.get('account.financial.report').browse(self.cr, self.uid, report.id, context=data['form']['comparison_context']).balance
+ vals['balance_cmp'] = self.pool.get('account.financial.report').browse(self.cr, self.uid, report.id, context=data['form']['comparison_context']).balance * report.sign or 0.0
lines.append(vals)
account_ids = []
if report.display_detail == 'no_detail':
@@ -97,7 +97,7 @@ class report_account_common(report_sxw.rml_parse, common_report_header):
if not currency_obj.is_zero(self.cr, self.uid, account.company_id.currency_id, vals['balance']):
flag = True
if data['form']['enable_filter']:
- vals['balance_cmp'] = account_obj.browse(self.cr, self.uid, account.id, context=data['form']['comparison_context']).balance
+ vals['balance_cmp'] = account_obj.browse(self.cr, self.uid, account.id, context=data['form']['comparison_context']).balance * report.sign or 0.0
if not currency_obj.is_zero(self.cr, self.uid, account.company_id.currency_id, vals['balance_cmp']):
flag = True
if flag:
diff --git a/addons/account_analytic_analysis/account_analytic_analysis_cron.xml b/addons/account_analytic_analysis/account_analytic_analysis_cron.xml
index 14ca1fd2135..970ab275ef9 100644
--- a/addons/account_analytic_analysis/account_analytic_analysis_cron.xml
+++ b/addons/account_analytic_analysis/account_analytic_analysis_cron.xml
@@ -55,6 +55,19 @@ Hello ${object.name},
${account_table(ctx["data"]["future"].iteritems())}
% endif
+
+ You can check all contracts to be renewed using the menu:
+
+ When reinvoicing costs, OpenERP uses the
pricelist of the contract which uses the price
- defined on the product related to each employee to
- define the customer invoice price rate.
+ defined on the product related (e.g timesheet
+ products are defined on each employee).
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index a9d33f4585d..aa23e418d35 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -20,6 +20,8 @@
##############################################################################
from openerp.osv import osv, fields
from openerp import tools, SUPERUSER_ID
+from openerp.tools.translate import _
+from openerp.tools.mail import plaintext2html
class mail_followers(osv.Model):
""" mail_followers holds the data related to the follow mechanism inside
@@ -75,20 +77,25 @@ class mail_notification(osv.Model):
if not cr.fetchone():
cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)')
- def get_partners_to_notify(self, cr, uid, message, context=None):
+ def get_partners_to_notify(self, cr, uid, message, partners_to_notify=None, context=None):
""" Return the list of partners to notify, based on their preferences.
:param browse_record message: mail.message to notify
+ :param list partners_to_notify: optional list of partner ids restricting
+ the notifications to process
"""
notify_pids = []
for notification in message.notification_ids:
if notification.read:
continue
partner = notification.partner_id
+ # If partners_to_notify specified: restrict to them
+ if partners_to_notify and partner.id not in partners_to_notify:
+ continue
# Do not send to partners without email address defined
if not partner.email:
continue
- # Partner does not want to receive any emails
+ # Partner does not want to receive any emails or is opt-out
if partner.notification_email_send == 'none':
continue
# Partner wants to receive only emails and comments
@@ -100,16 +107,72 @@ class mail_notification(osv.Model):
notify_pids.append(partner.id)
return notify_pids
- def _notify(self, cr, uid, msg_id, context=None):
- """ Send by email the notification depending on the user preferences """
+ def get_signature_footer(self, cr, uid, user_id, res_model=None, res_id=None, context=None):
+ """ Format a standard footer for notification emails (such as pushed messages
+ notification or invite emails).
+ Format:
+
+ """
+ footer = ""
+ if not user_id:
+ return footer
+
+ # add user signature
+ user = self.pool.get("res.users").browse(cr, SUPERUSER_ID, [user_id], context=context)[0]
+ if user.signature:
+ signature = plaintext2html(user.signature)
+ else:
+ signature = "-- %s" % user.name
+ footer = tools.append_content_to_html(footer, signature, plaintext=False, container_tag='p')
+
+ # add company signature
+ if user.company_id:
+ company = user.company_id.website and "%s" % (user.company_id.website, user.company_id.name) or user.company_id.name
+ else:
+ company = user.name
+ signature_company = _('Send by %(company)s using %(openerp)s.' % {
+ 'company': company,
+ 'openerp': "OpenERP"
+ })
+ footer = tools.append_content_to_html(footer, signature_company, plaintext=False, container_tag='div')
+
+ return footer
+
+ def _notify(self, cr, uid, msg_id, partners_to_notify=None, context=None):
+ """ Send by email the notification depending on the user preferences
+
+ :param list partners_to_notify: optional list of partner ids restricting
+ the notifications to process
+ """
if context is None:
context = {}
+ mail_message_obj = self.pool.get('mail.message')
+
+ # optional list of partners to notify: subscribe them if not already done or update the notification
+ if partners_to_notify:
+ notifications_to_update = []
+ notified_partners = []
+ notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', msg_id), ('partner_id', 'in', partners_to_notify)], context=context)
+ for notification in self.browse(cr, SUPERUSER_ID, notif_ids, context=context):
+ notified_partners.append(notification.partner_id.id)
+ notifications_to_update.append(notification.id)
+ partners_to_notify = filter(lambda item: item not in notified_partners, partners_to_notify)
+ if notifications_to_update:
+ self.write(cr, SUPERUSER_ID, notifications_to_update, {'read': False}, context=context)
+ mail_message_obj.write(cr, uid, msg_id, {'notified_partner_ids': [(4, id) for id in partners_to_notify]}, context=context)
+
# mail_notify_noemail (do not send email) or no partner_ids: do not send, return
if context.get('mail_notify_noemail'):
return True
# browse as SUPERUSER_ID because of access to res_partner not necessarily allowed
msg = self.pool.get('mail.message').browse(cr, SUPERUSER_ID, msg_id, context=context)
- notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, context=context)
+ notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, partners_to_notify=partners_to_notify, context=context)
if not notify_partner_ids:
return True
@@ -117,35 +180,25 @@ class mail_notification(osv.Model):
# TDE FIXME: commented, to be improved in a future branch
# quote_context = self.pool.get('mail.message').message_quote_context(cr, uid, msg_id, context=context)
- mail_mail = self.pool.get('mail.mail')
# add signature
body_html = msg.body
- # if quote_context:
- # body_html = tools.append_content_to_html(body_html, quote_context, plaintext=False)
- signature = msg.author_id and msg.author_id.user_ids and msg.author_id.user_ids[0].signature or ''
- if signature:
- body_html = tools.append_content_to_html(body_html, signature, plaintext=True, container_tag='div')
-
- # email_from: partner-user alias or partner email or mail.message email_from
- if msg.author_id and msg.author_id.user_ids and msg.author_id.user_ids[0].alias_domain and msg.author_id.user_ids[0].alias_name:
- email_from = '%s <%s@%s>' % (msg.author_id.name, msg.author_id.user_ids[0].alias_name, msg.author_id.user_ids[0].alias_domain)
- elif msg.author_id:
- email_from = '%s <%s>' % (msg.author_id.name, msg.author_id.email)
- else:
- email_from = msg.email_from
+ user_id = msg.author_id and msg.author_id.user_ids and msg.author_id.user_ids[0] and msg.author_id.user_ids[0].id or None
+ signature_company = self.get_signature_footer(cr, uid, user_id, res_model=msg.model, res_id=msg.res_id, context=context)
+ body_html = tools.append_content_to_html(body_html, signature_company, plaintext=False, container_tag='div')
mail_values = {
'mail_message_id': msg.id,
- 'email_to': [],
'auto_delete': True,
'body_html': body_html,
- 'email_from': email_from,
- 'state': 'outgoing',
+ 'recipient_ids': [(4, id) for id in notify_partner_ids]
}
- mail_values['email_to'] = ', '.join(mail_values['email_to'])
+ if msg.email_from:
+ mail_values['email_from'] = msg.email_from
+ if msg.reply_to:
+ mail_values['reply_to'] = msg.reply_to
+ mail_mail = self.pool.get('mail.mail')
email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
try:
- return mail_mail.send(cr, uid, [email_notif_id], recipient_ids=notify_partner_ids, context=context)
+ return mail_mail.send(cr, uid, [email_notif_id], context=context)
except Exception:
return False
-
diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml
index d8ff7864528..d103199d567 100644
--- a/addons/mail/mail_group_view.xml
+++ b/addons/mail/mail_group_view.xml
@@ -106,7 +106,7 @@
-
+
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index 1b3ca152a9a..43d0d707699 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -55,22 +55,25 @@ class mail_mail(osv.Model):
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_to': fields.text('To', help='Message recipients (emails)'),
+ 'recipient_ids': fields.many2many('res.partner', string='To (Partners)'),
'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"),
-
+ # If not set in create values, auto-detected based on create values (res_id, model, email_from)
+ 'reply_to': fields.char('Reply-To',
+ help='Preferred response address for the message'),
# Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
# and during unlink() we will not cascade delete the parent and its attachments
- 'notification': fields.boolean('Is Notification')
+ 'notification': fields.boolean('Is Notification',
+ help='Mail has been created to notify people of an existing mail.message')
}
def _get_default_from(self, cr, uid, context=None):
- this = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ this = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
if this.alias_domain:
- return '%s@%s' % (this.alias_name, this.alias_domain)
+ return '%s <%s@%s>' % (this.name, this.alias_name, this.alias_domain)
elif this.email:
- return this.email
+ return '%s <%s>' % (this.name, this.email)
raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
_defaults = {
@@ -82,12 +85,41 @@ class mail_mail(osv.Model):
# protection for `default_type` values leaking from menu action context (e.g. for invoices)
# To remove when automatic context propagation is removed in web client
if context and context.get('default_type') and context.get('default_type') not in self._all_columns['type'].column.selection:
- context = dict(context, default_type = None)
+ context = dict(context, default_type=None)
return super(mail_mail, self).default_get(cr, uid, fields, context=context)
+ def _get_reply_to(self, cr, uid, values, context=None):
+ """ Return a specific reply_to: alias of the document through message_get_reply_to
+ or take the email_from
+ """
+ if values.get('reply_to'):
+ return values.get('reply_to')
+ email_reply_to = False
+
+ # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
+ if values.get('model') and values.get('res_id') and hasattr(self.pool.get(values.get('model')), 'message_get_reply_to'):
+ email_reply_to = self.pool.get(values.get('model')).message_get_reply_to(cr, uid, [values.get('res_id')], context=context)[0]
+ # no alias reply_to -> reply_to will be the email_from, only the email part
+ if not email_reply_to and values.get('email_from'):
+ emails = tools.email_split(values.get('email_from'))
+ if emails:
+ email_reply_to = emails[0]
+
+ # format 'Document name '
+ if email_reply_to and values.get('model') and values.get('res_id'):
+ document_name = self.pool.get(values.get('model')).name_get(cr, SUPERUSER_ID, [values.get('res_id')], context=context)[0]
+ if document_name:
+ email_reply_to = _('Followers of %s <%s>') % (document_name[1], email_reply_to)
+
+ return email_reply_to
+
def create(self, cr, uid, values, context=None):
+ # notification field: if not set, set if mail comes from an existing mail.message
if 'notification' not in values and values.get('mail_message_id'):
values['notification'] = True
+ # reply_to: if not set, set with default values that require creation values
+ if not values.get('reply_to'):
+ values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
return super(mail_mail, self).create(cr, uid, values, context=context)
def unlink(self, cr, uid, ids, context=None):
@@ -160,15 +192,12 @@ class mail_mail(osv.Model):
return 'Re: %s' % (mail.record_name)
return mail.subject
- def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
- """ Return a specific ir_email body. The main purpose of this method
- is to be inherited by Portal, to add a link for signing in, in
+ def send_get_mail_body_footer(self, cr, uid, mail, partner=None, context=None):
+ """ Return a specific footer for the ir_email body. The main purpose of this method
+ is to be inherited by Portal, to add modify the link for signing in, in
each notification email a partner receives.
-
- :param browse_record mail: mail.mail browse_record
- :param browse_record partner: specific recipient partner
"""
- body = mail.body_html
+ body_footer = ""
# partner is a user, link to a related document (incentive to install portal)
if partner and partner.user_ids and mail.model and mail.res_id \
and self.check_access_rights(cr, partner.user_ids[0].id, 'read', raise_exception=False):
@@ -184,27 +213,24 @@ class mail_mail(osv.Model):
'id': mail.res_id,
}
url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
- text = _("""
""") % url
- body = tools.append_content_to_html(body, ("
%s
" % text), plaintext=False)
+ body_footer = _("""Access this document directly in OpenERP""") % url
except except_orm, e:
pass
- return body
+ return body_footer
- def send_get_mail_reply_to(self, cr, uid, mail, partner=None, context=None):
+ def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
""" Return a specific ir_email body. The main purpose of this method
- is to be inherited by Portal, to add a link for signing in, in
- each notification email a partner receives.
+ is to be inherited to add custom content depending on some module.
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
- if mail.reply_to:
- return mail.reply_to
- if not mail.model or not mail.res_id:
- return False
- if not hasattr(self.pool.get(mail.model), 'message_get_reply_to'):
- return False
- return self.pool.get(mail.model).message_get_reply_to(cr, uid, [mail.res_id], context=context)[0]
+ body = mail.body_html
+
+ # add footer
+ body_footer = self.send_get_mail_body_footer(cr, uid, mail, partner=partner, context=context)
+ body = tools.append_content_to_html(body, body_footer, plaintext=False, container_tag='div')
+ return body
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
""" Return a dictionary for specific email values, depending on a
@@ -215,7 +241,6 @@ class mail_mail(osv.Model):
"""
body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context)
- reply_to = self.send_get_mail_reply_to(cr, uid, mail, partner=partner, context=context)
body_alternative = tools.html2plaintext(body)
email_to = [partner.email] if partner else tools.email_split(mail.email_to)
return {
@@ -223,10 +248,9 @@ class mail_mail(osv.Model):
'body_alternative': body_alternative,
'subject': subject,
'email_to': email_to,
- 'reply_to': reply_to,
}
- def send(self, cr, uid, ids, auto_commit=False, recipient_ids=None, context=None):
+ 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).
@@ -237,14 +261,10 @@ class mail_mail(osv.Model):
: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)
- :param list recipient_ids: specific list of res.partner recipients.
- If set, one email is sent to each partner. Its is possible to
- tune the sent email through ``send_get_mail_body`` and ``send_get_mail_subject``.
- If not specified, one email is sent to mail_mail.email_to.
:return: True
"""
ir_mail_server = self.pool.get('ir.mail_server')
- for mail in self.browse(cr, uid, ids, context=context):
+ for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
try:
# handle attachments
attachments = []
@@ -252,11 +272,10 @@ class mail_mail(osv.Model):
attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
# specific behavior to customize the send email for notified partners
email_list = []
- if recipient_ids:
- for partner in self.pool.get('res.partner').browse(cr, SUPERUSER_ID, recipient_ids, context=context):
- email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
- else:
+ if mail.email_to:
email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
+ for partner in mail.recipient_ids:
+ email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
# build an RFC2822 email.message.Message object and send it without queuing
for email in email_list:
@@ -267,7 +286,7 @@ class mail_mail(osv.Model):
body = email.get('body'),
body_alternative = email.get('body_alternative'),
email_cc = tools.email_split(mail.email_cc),
- reply_to = email.get('reply_to'),
+ reply_to = mail.reply_to,
attachments = attachments,
message_id = mail.message_id,
references = mail.references,
diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml
index 72d41e33a84..1ff9a2a292e 100644
--- a/addons/mail/mail_mail_view.xml
+++ b/addons/mail/mail_mail_view.xml
@@ -14,31 +14,23 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
@@ -46,6 +38,8 @@
+
+
@@ -67,7 +61,7 @@
-
+
@@ -75,7 +69,7 @@
-
+
@@ -97,16 +91,15 @@
-
+
-
-
-
-
-
+
+
+
+
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 77573f0d2f8..8442891f3f9 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -139,6 +139,8 @@ class mail_message(osv.Model):
"message, comment for other messages such as user replies"),
'email_from': fields.char('From',
help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
+ 'reply_to': fields.char('Reply-To',
+ help='Reply email address. Setting the reply_to bypasses the automatic thread creation.'),
'author_id': fields.many2one('res.partner', 'Author', select=1,
ondelete='set null',
help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
@@ -185,8 +187,9 @@ class mail_message(osv.Model):
_defaults = {
'type': 'email',
'date': lambda *a: fields.datetime.now(),
- 'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
+ 'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx),
'body': '',
+ 'email_from': lambda self, cr, uid, ctx=None: self.pool.get('mail.mail')._get_default_from(cr, uid, ctx),
}
#------------------------------------------------------
@@ -301,8 +304,8 @@ class mail_message(osv.Model):
for key, message in message_tree.iteritems():
if message.author_id:
partner_ids |= set([message.author_id.id])
- if message.partner_ids:
- partner_ids |= set([partner.id for partner in message.partner_ids])
+ if message.notified_partner_ids:
+ partner_ids |= set([partner.id for partner in message.notified_partner_ids])
if message.attachment_ids:
attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
# Read partners as SUPERUSER -> display the names like classic m2o even if no access
@@ -322,7 +325,7 @@ class mail_message(osv.Model):
else:
author = (0, message.email_from)
partner_ids = []
- for partner in message.partner_ids:
+ for partner in message.notified_partner_ids:
if partner.id in partner_tree:
partner_ids.append(partner_tree[partner.id])
attachment_ids = []
@@ -731,7 +734,10 @@ class mail_message(osv.Model):
if context is None:
context = {}
default_starred = context.pop('default_starred', False)
- if not values.get('message_id') and values.get('res_id') and values.get('model'):
+ # generate message_id, to redirect answers to the right discussion thread
+ if not values.get('message_id') and values.get('reply_to'):
+ values['message_id'] = tools.generate_tracking_message_id('reply_to-%(model)s' % values)
+ elif not values.get('message_id') and values.get('res_id') and values.get('model'):
values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
elif not values.get('message_id'):
values['message_id'] = tools.generate_tracking_message_id('private')
@@ -861,7 +867,7 @@ class mail_message(osv.Model):
# message has no subtype_id: pure log message -> no partners, no one notified
if not message.subtype_id:
return True
-
+
# all followers of the mail.message document have to be added as partners and notified
if message.model and message.res_id:
fol_obj = self.pool.get("mail.followers")
@@ -884,8 +890,7 @@ class mail_message(osv.Model):
# notify
if partners_to_notify:
- self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p.id) for p in partners_to_notify]}, context=context)
- notification_obj._notify(cr, uid, newid, context=context)
+ notification_obj._notify(cr, uid, newid, partners_to_notify=[p.id for p in partners_to_notify], context=context)
message.refresh()
# An error appear when a user receive a notification without notifying
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml
index 7d2e9483773..8f2fb3f1020 100644
--- a/addons/mail/mail_message_view.xml
+++ b/addons/mail/mail_message_view.xml
@@ -59,12 +59,12 @@
-
-
+ 9 %d + %d" % (cls, len(thread.message_ids), len(thread.message_follower_ids))
+ for result in cr.fetchall():
+ res[result[0]]['message_unread'] = True
+ res[result[0]]['message_unread_count'] += 1
+ for id in ids:
+ if res[id]['message_unread_count']:
+ title = res[id]['message_unread_count'] > 1 and _("You have %d unread messages") % res[id]['message_unread_count'] or _("You have one unread message")
+ res[id]['message_summary'] = "9 %d %s" % (title, res[id].pop('message_unread_count'), _("New"))
return res
def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
@@ -488,6 +488,7 @@ class mail_thread(osv.AbstractModel):
# 1. Verify if this is a reply to an existing thread
thread_references = references or in_reply_to
ref_match = thread_references and tools.reference_re.search(thread_references)
+
if ref_match:
thread_id = int(ref_match.group(1))
model = ref_match.group(2) or model
@@ -500,7 +501,10 @@ class mail_thread(osv.AbstractModel):
# Verify whether this is a reply to a private message
if in_reply_to:
- message_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', in_reply_to)], limit=1, context=context)
+ message_ids = self.pool.get('mail.message').search(cr, uid, [
+ ('message_id', '=', in_reply_to),
+ '!', ('message_id', 'ilike', 'reply_to')
+ ], limit=1, context=context)
if message_ids:
message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
_logger.debug('Routing mail with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
@@ -627,15 +631,12 @@ class mail_thread(osv.AbstractModel):
if thread_id and hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
else:
+ nosub_ctx = dict(nosub_ctx, mail_create_nolog=True)
thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
else:
assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
model_pool = self.pool.get('mail.thread')
- new_msg_id = model_pool.message_post_user_api(cr, uid, [thread_id], context=context, content_subtype='html', **msg)
-
- # when posting an incoming email to a document: subscribe the author, if a partner, as follower
- if model and thread_id and msg.get('author_id'):
- model_pool.message_subscribe(cr, uid, [thread_id], [msg.get('author_id')], context=context)
+ new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
if partner_ids:
# postponed after message_post, because this is an external message and we don't want to create
@@ -780,7 +781,7 @@ class mail_thread(osv.AbstractModel):
_logger.debug('Parsing Message without message-id, generating a random one: %s', message_id)
msg_dict['message_id'] = message_id
- if 'Subject' in message:
+ if message.get('Subject'):
msg_dict['subject'] = decode(message.get('Subject'))
# Envelope fields not stored in mail.message but made available for message_new()
@@ -788,16 +789,15 @@ class mail_thread(osv.AbstractModel):
msg_dict['to'] = decode(message.get('to'))
msg_dict['cc'] = decode(message.get('cc'))
- if 'From' in message:
+ if message.get('From'):
author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context)
if author_ids:
msg_dict['author_id'] = author_ids[0]
- else:
- msg_dict['email_from'] = decode(message.get('from'))
+ msg_dict['email_from'] = decode(message.get('from'))
partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context)
msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
- if 'Date' in message:
+ if message.get('Date'):
try:
date_hdr = decode(message.get('Date'))
parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True)
@@ -815,12 +815,12 @@ class mail_thread(osv.AbstractModel):
stored_date = datetime.datetime.now()
msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
- if 'In-Reply-To' in message:
+ if message.get('In-Reply-To'):
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))])
if parent_ids:
msg_dict['parent_id'] = parent_ids[0]
- if 'References' in message and 'parent_id' not in msg_dict:
+ if message.get('References') and 'parent_id' not in msg_dict:
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in',
[x.strip() for x in decode(message['References']).split()])])
if parent_ids:
@@ -841,77 +841,151 @@ class mail_thread(osv.AbstractModel):
"now deprecated res.log.")
self.message_post(cr, uid, [id], message, context=context)
- def message_create_partners_from_emails(self, cr, uid, emails, context=None):
+ def _message_add_suggested_recipient(self, cr, uid, result, obj, partner=None, email=None, reason='', context=None):
+ """ Called by message_get_suggested_recipients, to add a suggested
+ recipient in the result dictionary. The form is :
+ partner_id, partner_name or partner_name, reason """
+ if email and not partner:
+ partner_info = self.message_get_partner_info_from_emails(cr, uid, [email], context=context)[0]
+ if partner_info.get('partner_id'):
+ partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info.get('partner_id')], context=context)[0]
+ if email and email in [val[1] for val in result[obj.id]]: # already existing email -> skip
+ return result
+ if partner and partner in obj.message_follower_ids: # recipient already in the followers -> skip
+ return result
+ if partner and partner in [val[0] for val in result[obj.id]]: # already existing partner ID -> skip
+ return result
+ if partner and partner.email: # complete profile: id, name
+ result[obj.id].append((partner.id, '%s<%s>' % (partner.name, partner.email), reason))
+ elif partner: # incomplete profile: id, name
+ result[obj.id].append((partner.id, '%s' % (partner.name), reason))
+ else: # unknown partner, we are probably managing an email address
+ result[obj.id].append((False, email, reason))
+ return result
+
+ def message_get_suggested_recipients(self, cr, uid, ids, context=None):
+ """ Returns suggested recipients for ids. Those are a list of
+ tuple (partner_id, partner_name, reason), to be managed by Chatter. """
+ result = dict.fromkeys(ids, list())
+ if self._all_columns.get('user_id'):
+ for obj in self.browse(cr, SUPERUSER_ID, ids, context=context): # SUPERUSER because of a read on res.users that would crash otherwise
+ if not obj.user_id or not obj.user_id.partner_id:
+ continue
+ self._message_add_suggested_recipient(cr, uid, result, obj, partner=obj.user_id.partner_id, reason=self._all_columns['user_id'].column.string, context=context)
+ return result
+
+ def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None):
""" Convert a list of emails into a list partner_ids and a list
new_partner_ids. The return value is non conventional because
it is meant to be used by the mail widget.
:return dict: partner_ids and new_partner_ids
"""
- partner_obj = self.pool.get('res.partner')
mail_message_obj = self.pool.get('mail.message')
-
- partner_ids = []
- new_partner_ids = []
+ partner_obj = self.pool.get('res.partner')
+ result = list()
for email in emails:
+ partner_info = {'full_name': email, 'partner_id': False}
m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL)
- name = m.group(2) or m.group(0)
- email = m.group(3)
- ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email)], context=context)
+ if not m:
+ continue
+ email_address = m.group(3)
+ ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email_address)], context=context)
if ids:
- partner_ids.append(ids[0])
- partner_id = ids[0]
- else:
- partner_id = partner_obj.create(cr, uid, {
- 'name': name or email,
- 'email': email,
- }, context=context)
- new_partner_ids.append(partner_id)
+ partner_info['partner_id'] = ids[0]
+ result.append(partner_info)
# link mail with this from mail to the new partner id
- message_ids = mail_message_obj.search(cr, SUPERUSER_ID, ['|', ('email_from', '=', email), ('email_from', 'ilike', '<%s>' % email), ('author_id', '=', False)], context=context)
- if message_ids:
- mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'email_from': None, 'author_id': partner_id}, context=context)
- return {
- 'partner_ids': partner_ids,
- 'new_partner_ids': new_partner_ids,
- }
+ if link_mail and ids:
+ message_ids = mail_message_obj.search(cr, SUPERUSER_ID, [
+ '|',
+ ('email_from', '=', email),
+ ('email_from', 'ilike', '<%s>' % email),
+ ('author_id', '=', False)
+ ], context=context)
+ if message_ids:
+ mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'author_id': ids[0]}, context=context)
+ return result
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
- subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
+ subtype=None, parent_id=False, attachments=None, context=None,
+ content_subtype='html', **kwargs):
""" Post a new message in an existing thread, returning the new
- mail.message ID. Extra keyword arguments will be used as default
- column values for the new mail.message record.
- Auto link messages for same id and object
+ mail.message ID.
+
:param int thread_id: thread ID to post into, or list with one ID;
if False/0, mail.message model will also be set as False
:param str body: body of the message, usually raw HTML that will
be sanitized
- :param str subject: optional subject
- :param str type: mail_message.type
- :param int parent_id: optional ID of parent message in this thread
+ :param str type: see mail_message.type field
+ :param str content_subtype:: if plaintext: convert body into html
+ :param int parent_id: handle reply to a previous message by adding the
+ parent partners to the message in case of private discussion
:param tuple(str,str) attachments or list id: list of attachment tuples in the form
``(name,content)``, where content is NOT base64 encoded
- :return: ID of newly created mail.message
+
+ Extra keyword arguments will be used as default column values for the
+ new mail.message record. Special cases:
+ - attachment_ids: supposed not attached to any document; attach them
+ to the related document. Should only be set by Chatter.
+ :return int: ID of newly created mail.message
"""
if context is None:
context = {}
if attachments is None:
attachments = {}
-
- assert (not thread_id) or isinstance(thread_id, (int, long)) or \
- (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id; should be 0, False, an ID or a list with one ID"
- if isinstance(thread_id, (list, tuple)):
- thread_id = thread_id and thread_id[0]
mail_message = self.pool.get('mail.message')
+ ir_attachment = self.pool.get('ir.attachment')
+
+ assert (not thread_id) or \
+ isinstance(thread_id, (int, long)) or \
+ (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), \
+ "Invalid thread_id; should be 0, False, an ID or a list with one ID"
+ if isinstance(thread_id, (list, tuple)):
+ thread_id = thread_id[0]
# if we're processing a message directly coming from the gateway, the destination model was
- # set in the context.
+ # set in the context.
model = False
if thread_id:
model = context.get('thread_model', self._name) if self._name == 'mail.thread' else self._name
- attachment_ids = kwargs.pop('attachment_ids', [])
+ # 1: Handle content subtype: if plaintext, converto into HTML
+ if content_subtype == 'plaintext':
+ body = tools.plaintext2html(body)
+
+ # 2: Private message: add recipients (recipients and author of parent message)
+ # + legacy-code management (! we manage only 4 and 6 commands)
+ partner_ids = set()
+ kwargs_partner_ids = kwargs.pop('partner_ids', [])
+ for partner_id in kwargs_partner_ids:
+ if isinstance(partner_id, (list, tuple)) and partner_id[0] == 4 and len(partner_id) == 2:
+ partner_ids.add(partner_id[1])
+ if isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3:
+ partner_ids |= set(partner_id[2])
+ elif isinstance(partner_id, (int, long)):
+ partner_ids.add(partner_id)
+ else:
+ pass # we do not manage anything else
+ if parent_id and model == 'mail.thread':
+ parent_message = mail_message.browse(cr, uid, parent_id, context=context)
+ partner_ids |= set([partner.id for partner in parent_message.partner_ids])
+ if parent_message.author_id:
+ partner_ids.add(parent_message.author_id.id)
+
+ # 3. Attachments
+ # - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
+ attachment_ids = kwargs.pop('attachment_ids', []) or [] # because we could receive None (some old code sends None)
+ if attachment_ids:
+ filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
+ ('res_model', '=', 'mail.compose.message'),
+ ('res_id', '=', 0),
+ ('create_uid', '=', uid),
+ ('id', 'in', attachment_ids)], context=context)
+ if filtered_attachment_ids:
+ ir_attachment.write(cr, SUPERUSER_ID, filtered_attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
+ attachment_ids = [(4, id) for id in attachment_ids]
+ # Handle attachments parameter, that is a dictionary of attachments
for name, content in attachments:
if isinstance(content, unicode):
content = content.encode('utf-8')
@@ -920,20 +994,25 @@ class mail_thread(osv.AbstractModel):
'datas': base64.b64encode(str(content)),
'datas_fname': name,
'description': name,
- 'res_model': context.get('thread_model') or self._name,
+ 'res_model': model,
'res_id': thread_id,
}
attachment_ids.append((0, 0, data_attach))
- # fetch subtype
+ # 4: mail.message.subtype
+ subtype_id = False
if subtype:
- s_data = subtype.split('.')
- if len(s_data) == 1:
- s_data = ('mail', s_data[0])
- ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, s_data[0], s_data[1])
+ if '.' not in subtype:
+ subtype = 'mail.%s' % subtype
+ ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, *subtype.split('.'))
subtype_id = ref and ref[1] or False
- else:
- subtype_id = False
+
+ # automatically subscribe recipients if asked to
+ if context.get('mail_post_autofollow') and thread_id and partner_ids:
+ partner_to_subscribe = partner_ids
+ if context.get('mail_post_autofollow_partner_ids'):
+ partner_to_subscribe = filter(lambda item: item in context.get('mail_post_autofollow_partner_ids'), partner_ids)
+ self.message_subscribe(cr, uid, [thread_id], list(partner_to_subscribe), context=context)
# _mail_flat_thread: automatically set free messages to the first posted message
if self._mail_flat_thread and not parent_id and thread_id:
@@ -961,86 +1040,34 @@ class mail_thread(osv.AbstractModel):
'parent_id': parent_id,
'attachment_ids': attachment_ids,
'subtype_id': subtype_id,
+ 'partner_ids': [(4, pid) for pid in partner_ids],
})
# Avoid warnings about non-existing fields
for x in ('from', 'to', 'cc'):
values.pop(x, None)
- return mail_message.create(cr, uid, values, context=context)
+ # Create and auto subscribe the author
+ msg_id = mail_message.create(cr, uid, values, context=context)
+ message = mail_message.browse(cr, uid, msg_id, context=context)
+ if message.author_id and thread_id and type != 'notification' and not context.get('mail_create_nosubscribe'):
+ self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context)
+ return msg_id
+
+ #------------------------------------------------------
+ # Compatibility methods: do not use
+ # TDE TODO: remove me in 8.0
+ #------------------------------------------------------
+
+ def message_create_partners_from_emails(self, cr, uid, emails, context=None):
+ return {'partner_ids': [], 'new_partner_ids': []}
def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False,
attachment_ids=None, content_subtype='plaintext',
context=None, **kwargs):
- """ Wrapper on message_post, used for user input :
- - mail gateway
- - quick reply in Chatter (refer to mail.js), not
- the mail.compose.message wizard
- The purpose is to perform some pre- and post-processing:
- - if body is plaintext: convert it into html
- - if parent_id: handle reply to a previous message by adding the
- parent partners to the message
- - type and subtype: comment and mail.mt_comment by default
- - attachment_ids: supposed not attached to any document; attach them
- to the related document. Should only be set by Chatter.
- """
- mail_message_obj = self.pool.get('mail.message')
- ir_attachment = self.pool.get('ir.attachment')
-
- # 1.A.1: add recipients of parent message (# TDE FIXME HACK: mail.thread -> private message)
- partner_ids = set([])
- if parent_id and self._name == 'mail.thread':
- parent_message = mail_message_obj.browse(cr, uid, parent_id, context=context)
- partner_ids |= set([(4, partner.id) for partner in parent_message.partner_ids])
- if parent_message.author_id.id:
- partner_ids.add((4, parent_message.author_id.id))
-
- # 1.A.2: add specified recipients
- param_partner_ids = set()
- for item in kwargs.pop('partner_ids', []):
- if isinstance(item, (list)):
- param_partner_ids.add((item[0], item[1]))
- elif isinstance(item, (int, long)):
- param_partner_ids.add((4, item))
- else:
- param_partner_ids.add(item)
- partner_ids |= param_partner_ids
-
- # 1.A.3: add parameters recipients as follower
- # TDE FIXME in 7.1: should check whether this comes from email_list or partner_ids
- if param_partner_ids and self._name != 'mail.thread':
- self.message_subscribe(cr, uid, [thread_id], [pid[1] for pid in param_partner_ids], context=context)
-
- # 1.B: handle body, message_type and message_subtype
- if content_subtype == 'plaintext':
- body = tools.plaintext2html(body)
- msg_type = kwargs.pop('type', 'comment')
- msg_subtype = kwargs.pop('subtype', 'mail.mt_comment')
-
- # 2. Pre-processing: attachments
- # HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
- if attachment_ids:
- # TDE FIXME (?): when posting a private message, we use mail.thread as a model
- # However, attaching doc to mail.thread is not possible, mail.thread does not have any table
- model = self._name
- if model == 'mail.thread':
- model = False
- filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
- ('res_model', '=', 'mail.compose.message'),
- ('res_id', '=', 0),
- ('create_uid', '=', uid),
- ('id', 'in', attachment_ids)], context=context)
- if filtered_attachment_ids:
- if thread_id and model:
- ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
- else:
- attachment_ids = []
- attachment_ids = [(4, id) for id in attachment_ids]
-
- # 3. Post message
- return self.message_post(cr, uid, thread_id=thread_id, body=body,
- type=msg_type, subtype=msg_subtype, parent_id=parent_id,
- attachment_ids=attachment_ids, partner_ids=list(partner_ids), context=context, **kwargs)
+ return self.message_post(cr, uid, thread_id, body=body, parent_id=parent_id,
+ attachment_ids=attachment_ids, content_subtype=content_subtype,
+ context=context, **kwargs)
#------------------------------------------------------
# Followers API
@@ -1162,12 +1189,25 @@ class mail_thread(osv.AbstractModel):
# add followers coming from res.users relational fields that are tracked
user_ids = [getattr(record, name).id for name in user_field_lst if getattr(record, name)]
- for partner_id in [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]:
+ user_id_partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]
+ for partner_id in user_id_partner_ids:
new_followers.setdefault(partner_id, None)
for pid, subtypes in new_followers.items():
subtypes = list(subtypes) if subtypes is not None else None
self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context)
+
+ # find first email message, set it as unread for auto_subscribe fields for them to have a notification
+ if user_id_partner_ids:
+ msg_ids = self.pool.get('mail.message').search(cr, uid, [
+ ('model', '=', self._name),
+ ('res_id', '=', record.id),
+ ('type', '=', 'email')], limit=1, context=context)
+ if not msg_ids and record.message_ids:
+ msg_ids = [record.message_ids[-1].id]
+ if msg_ids:
+ self.pool.get('mail.notification')._notify(cr, uid, msg_ids[0], partners_to_notify=user_id_partner_ids, context=context)
+
return True
#------------------------------------------------------
diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml
index 6769fb0d063..75c86e4645d 100644
--- a/addons/mail/mail_thread_view.xml
+++ b/addons/mail/mail_thread_view.xml
@@ -6,7 +6,7 @@
mail.wallmail.message{
- 'default_model': 'res.users',
+ 'default_model': 'res.users',
'default_res_id': uid,
}mail.wallmail.message{
- 'default_model': 'res.users',
- 'default_res_id': uid,
+ 'default_model': 'res.users',
+ 'default_res_id': uid,
'search_default_message_unread': True
}
+ 'show_compose_message': False
+ }""/>
No message found and no message sent yet.
diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py
index 77f680bcab2..8527e60aba0 100644
--- a/addons/mail/res_partner.py
+++ b/addons/mail/res_partner.py
@@ -18,9 +18,10 @@
# along with this program. If not, see .
#
##############################################################################
-
+from openerp.tools.translate import _
from openerp.osv import fields, osv
+
class res_partner_mail(osv.Model):
""" Update partner to add a field about notification preferences """
_name = "res.partner"
@@ -29,19 +30,28 @@ class res_partner_mail(osv.Model):
_columns = {
'notification_email_send': fields.selection([
- ('all', 'All feeds'),
- ('comment', 'Comments and Emails'),
- ('email', 'Emails only'),
- ('none', 'Never')
- ], 'Receive Feeds by Email', required=True,
- help="Choose in which case you want to receive an email when you "\
- "receive new feeds."),
+ ('none', 'Never'),
+ ('email', 'Incoming Emails only'),
+ ('comment', 'Incoming Emails and Discussions'),
+ ('all', 'All Messages (discussions, emails, followed system notifications)'),
+ ], 'Receive Messages by Email', required=True,
+ help="Policy to receive emails for new messages pushed to your personal Inbox:\n"
+ "- Never: no emails are sent\n"
+ "- Incoming Emails only: for messages received by the system via email\n"
+ "- Incoming Emails and Discussions: for incoming emails along with internal discussions\n"
+ "- All Messages: for every notification you receive in your Inbox"),
}
_defaults = {
'notification_email_send': lambda *args: 'comment'
}
+ def message_get_suggested_recipients(self, cr, uid, ids, context=None):
+ recipients = super(res_partner_mail, self).message_get_suggested_recipients(cr, uid, ids, context=context)
+ for partner in self.browse(cr, uid, ids, context=context):
+ self._message_add_suggested_recipient(cr, uid, recipients, partner, partner=partner, reason=_('Partner Profile'))
+ return recipients
+
def message_post(self, cr, uid, thread_id, **kwargs):
""" Override related to res.partner. In case of email message, set it as
private:
diff --git a/addons/mail/res_partner_view.xml b/addons/mail/res_partner_view.xml
index a4aa56ae971..7df5d27c9d0 100644
--- a/addons/mail/res_partner_view.xml
+++ b/addons/mail/res_partner_view.xml
@@ -7,6 +7,9 @@
res.partner
+
+
+
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index 41b0a46c2b4..f7c91939b98 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -113,13 +113,6 @@ class res_users(osv.Model):
thread_id = thread_id[0]
return self.browse(cr, SUPERUSER_ID, thread_id).partner_id.id
- def message_post_user_api(self, cr, uid, thread_id, context=None, **kwargs):
- """ Redirect the posting of message on res.users to the related partner.
- This is done because when giving the context of Chatter on the
- various mailboxes, we do not have access to the current partner_id. """
- partner_id = self._message_post_get_pid(cr, uid, thread_id, context=context)
- return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, context=context, **kwargs)
-
def message_post(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the
@@ -139,6 +132,27 @@ class res_users(osv.Model):
self.pool.get('res.partner').message_subscribe(cr, uid, [partner_id], partner_ids, subtype_ids=subtype_ids, context=context)
return True
+ def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None):
+ return self.pool.get('res.partner').message_get_partner_info_from_emails(cr, uid, emails, link_mail=link_mail, context=context)
+
+ def message_get_suggested_recipients(self, cr, uid, ids, context=None):
+ partner_ids = []
+ for id in ids:
+ partner_ids.append(self.browse(cr, SUPERUSER_ID, id).partner_id.id)
+ return self.pool.get('res.partner').message_get_suggested_recipients(cr, uid, partner_ids, context=context)
+
+ #------------------------------------------------------
+ # Compatibility methods: do not use
+ # TDE TODO: remove me in 8.0
+ #------------------------------------------------------
+
+ def message_post_user_api(self, cr, uid, thread_id, context=None, **kwargs):
+ """ Redirect the posting of message on res.users to the related partner.
+ This is done because when giving the context of Chatter on the
+ various mailboxes, we do not have access to the current partner_id. """
+ partner_id = self._message_post_get_pid(cr, uid, thread_id, context=context)
+ return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, context=context, **kwargs)
+
def message_create_partners_from_emails(self, cr, uid, emails, context=None):
return self.pool.get('res.partner').message_create_partners_from_emails(cr, uid, emails, context=context)
diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml
index a119765e1ec..f49ff72194a 100644
--- a/addons/mail/res_users_view.xml
+++ b/addons/mail/res_users_view.xml
@@ -23,7 +23,7 @@
-
+
diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css
index 2f73217eb21..e54a2d0bc4a 100644
--- a/addons/mail/static/src/css/mail.css
+++ b/addons/mail/static/src/css/mail.css
@@ -53,6 +53,9 @@
min-height: 42px;
border: solid 1px rgba(0,0,0,0.03);
}
+.openerp .oe_mail .oe_msg.oe_msg_nobody{
+ background: #F8F8F8;
+}
.openerp .oe_mail .oe_msg .oe_msg_left{
position: absolute;
left:0; top: 0; bottom: 0; width: 40px;
@@ -223,20 +226,30 @@
height: 24px;
width: 100%;
}
+.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_sep_word{
+ margin-right: 8px;
+ margin-left: 8px;
+}
.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact{
height: 24px;
width: 100%;
padding: 2px 4px;
+ color: #AAA;
+ cursor: text;
+}
+.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_record {
+ font-size: 13px;
+ font-style: bold;
+ text-align: center;
+}
+.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_inbox {
border: 1px solid #CCC;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
background: white;
font-size: 14px;
- color: #AAA;
font-style: italic;
- word-spacing: 3px;
- cursor: text;
}
/* d) I.E. tweaks for Message action icons */
@@ -268,20 +281,20 @@
margin-top: 4px;
margin-bottom: 4px;
}
-.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list{
+.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list {
display: block;
}
-.openerp .oe_mail .oe_msg_composer .oe_emails_from{
+.openerp .oe_mail .oe_msg_composer .oe_recipients {
font-size: 12px;
margin-left: 20px;
margin-bottom: 2px;
}
-.openerp .oe_mail .oe_msg_composer .oe_emails_from label{
+.openerp .oe_mail .oe_msg_composer .oe_recipients label{
vertical-align: middle;
display: block;
line-height: 14px;
}
-.openerp .oe_mail .oe_msg_composer .oe_emails_from input{
+.openerp .oe_mail .oe_msg_composer .oe_recipients input{
vertical-align: middle;
}
.openerp .oe_mail .oe_attachment{
@@ -438,7 +451,8 @@
line-height: 12px;
vertical-align: middle;
}
-.openerp .oe_mail .oe_msg_footer button.oe_post{
+.openerp .oe_mail .oe_msg_footer button.oe_post,
+.openerp .oe_mail .oe_msg_footer button.oe_log{
position: relative;
z-index: 2;
}
@@ -457,18 +471,20 @@
.openerp .oe_mail .oe_hidden_input_file, .openerp .oe_mail .oe_hidden_input_file form{
display:inline;
}
-.openerp .oe_mail .oe_msg_footer button.oe_full{
- width:24px;
- overflow:hidden;
+.openerp .oe_mail .oe_msg_center button.oe_full{
+ width: 24px;
+ height: 22px;
+ overflow: hidden;
float: right;
- filter:none;
+ filter: none;
}
-.openerp .oe_mail .oe_msg_footer button.oe_full .oe_e{
+.openerp .oe_mail .oe_msg_center button.oe_full .oe_e{
position: relative;
- top: -4px;
- margin-left: -9px;
- vertical-align: middle;
- filter:none;
+ top: -9px;
+ margin-left: -5px;
+ vertical-align: top;
+ filter: none;
+ height: 14px;
}
.openerp .oe_mail button.oe_attach, .openerp .oe_mail button.oe_full{
background: transparent;
@@ -544,6 +560,14 @@
text-align: center;
width:100%;
}
+.openerp .oe_followers button.oe_invite{
+ margin: 5px 0;
+ padding: 2px 8px;
+ font-size: 12px;
+ text-shadow: none;
+ width: 100%;
+}
+.openerp .oe_followers button.oe_invite,
.openerp .oe_followers button.oe_follower.oe_following{
color: white;
background-color: #3465A4;
@@ -597,9 +621,6 @@
margin-top: 12px;
margin-bottom: 4px;
}
-.openerp .oe_followers .oe_invite{
- float: right;
-}
.openerp .oe_followers .oe_partner {
height: 32px;
overflow: hidden;
diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js
index e5c8f40eb2e..f6e098b466d 100644
--- a/addons/mail/static/src/js/mail.js
+++ b/addons/mail/static/src/js/mail.js
@@ -21,6 +21,19 @@ openerp.mail = function (session) {
mail.ChatterUtils = {
+ /** parse text to find email: Tagada -> [Tagada, address@mail.fr] or False */
+ parse_email: function (text) {
+ var result = text.match(/(.*)<(.*@.*)>/);
+ if (result) {
+ return [_.str.trim(result[1]), _.str.trim(result[2])];
+ }
+ result = text.match(/(.*@.*)/);
+ if (result) {
+ return [_.str.trim(result[1]), _.str.trim(result[1])];
+ }
+ return [text, false];
+ },
+
/* Get an image in /web/binary/image?... */
get_image: function (session, model, field, id, resize) {
var r = resize ? encodeURIComponent(resize) : '';
@@ -255,11 +268,15 @@ openerp.mail = function (session) {
this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
}
if (this.author_id && this.author_id[1]) {
- var email = this.author_id[1].match(/(.*)<(.*@.*)>/);
- if (!email) {
- this.author_id.push(_.str.escapeHTML(this.author_id[1]), '', this.author_id[1]);
- } else {
- this.author_id.push(_.str.escapeHTML(email[0]), _.str.trim(email[1]), email[2]);
+ var parsed_email = mail.ChatterUtils.parse_email(this.author_id[1]);
+ this.author_id.push(parsed_email[0], parsed_email[1]);
+ }
+ if (this.partner_ids && this.partner_ids.length > 3) {
+ this.extra_partners_nbr = this.partner_ids.length - 3;
+ this.extra_partners_str = ''
+ var extra_partners = this.partner_ids.slice(3);
+ for (var key in extra_partners) {
+ this.extra_partners_str += extra_partners[key][1];
}
}
},
@@ -371,14 +388,24 @@ openerp.mail = function (session) {
* @param {Object} [context] context passed to the
* mail.compose.message DataSetSearch. Please refer to this model
* for more details about fields and default values.
+ * @param {Object} recipients = [
+ {
+ 'email_address': [str],
+ 'partner_id': False/[int],
+ 'name': [str],
+ 'full_name': name,
+ },
+ { ... },
+ ]
*/
init: function (parent, datasets, options) {
this._super(parent, datasets, options);
this.show_compact_message = false;
this.show_delete_attachment = true;
- this.emails_from = [];
- this.partners_from = [];
+ this.is_log = false;
+ this.recipients = [];
+ this.recipient_ids = [];
},
start: function () {
@@ -479,15 +506,13 @@ openerp.mail = function (session) {
bind_events: function () {
var self = this;
-
- this.$('.oe_compact').on('click', _.bind( this.on_compose_expandable, this));
-
- // set the function called when attachments are added
- this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this) );
-
- this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this) );
- this.$('.oe_post').on('click', _.bind( this.on_message_post, this) );
- this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment') );
+ this.$('.oe_compact_inbox').on('click', self.on_toggle_quick_composer);
+ this.$('.oe_compose_post').on('click', self.on_toggle_quick_composer);
+ this.$('.oe_compose_log').on('click', self.on_toggle_quick_composer);
+ this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this));
+ this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this));
+ this.$('.oe_post').on('click', self.on_message_post);
+ this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment'));
/* stack for don't close the compose form if the user click on a button */
this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this));
this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this));
@@ -497,13 +522,12 @@ openerp.mail = function (session) {
this.$('textarea').autosize();
// auto close
- this.$('textarea').on('blur', _.bind( this.on_compose_expandable, this));
+ this.$('textarea').on('blur', self.on_toggle_quick_composer);
// event: delete child attachments off the oe_msg_attachment_list box
this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete);
- this.$(".oe_emails_from").on('change', 'input', this.on_checked_email_from);
- this.$(".oe_partners_from").on('change', 'input', this.on_checked_partner_from);
+ this.$(".oe_recipients").on('change', 'input', this.on_checked_recipient);
},
on_compose_fullmail: function (default_composition_mode) {
@@ -511,16 +535,26 @@ openerp.mail = function (session) {
if(!this.do_check_attachment_upload()) {
return false;
}
-
- // create list of new partners
- this.check_recipient_partners().done(function (partner_ids) {
+ var recipient_done = $.Deferred();
+ if (this.is_log) {
+ recipient_done.resolve([]);
+ }
+ else {
+ recipient_done = this.check_recipient_partners();
+ }
+ $.when(recipient_done).done(function (partner_ids) {
var context = {
'default_composition_mode': default_composition_mode,
'default_parent_id': self.id,
'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}),
'default_partner_ids': partner_ids,
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
};
+ if (self.is_log) {
+ _.extend(context, {'mail_compose_log': true});
+ }
if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) {
context.default_model = self.context.default_model;
context.default_res_id = self.context.default_res_id;
@@ -573,68 +607,122 @@ openerp.mail = function (session) {
check_recipient_partners: function () {
var self = this;
- var partners_from = [];
- var emails = [];
- _.each(this.emails_from, function (email_from) {
- if (email_from[1] && !_.find(emails, function (email) {return email == email_from[0][4];})) {
- emails.push(email_from[0][1]);
- }
- });
- var deferred_check = $.Deferred();
- if (emails.length == 0) {
- return deferred_check.resolve(partners_from);
+ var check_done = $.Deferred();
+ var recipients = _.filter(this.recipients, function (recipient) { return recipient.checked });
+ var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id) });
+ var names_to_find = _.pluck(recipients_to_find, 'full_name');
+ var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address) });
+ var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address }), 'partner_id');
+ var names_to_remove = [];
+ var recipient_ids_to_remove = [];
+
+ // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id
+ var find_done = $.Deferred();
+ if (names_to_find.length > 0) {
+ find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [names_to_find]);
}
- self.parent_thread.ds_thread._model.call('message_create_partners_from_emails', [emails]).then(function (partners) {
- partners_from = _.clone(partners.partner_ids);
- var deferreds = [];
- _.each(partners.new_partner_ids, function (id) {
+ else {
+ find_done.resolve([]);
+ }
+
+ // for unknown names + incomplete partners -> open popup - cancel = remove from recipients
+ $.when(find_done).pipe(function (result) {
+ var emails_deferred = [];
+ var recipient_popups = result.concat(recipients_to_check);
+
+ _.each(recipient_popups, function (partner_info) {
var deferred = $.Deferred()
- deferreds.push(deferred);
- var pop = new session.web.form.FormOpenPopup(this);
+ emails_deferred.push(deferred);
+
+ var partner_name = partner_info.full_name;
+ var partner_id = partner_info.partner_id;
+ var parsed_email = mail.ChatterUtils.parse_email(partner_name);
+
+ var pop = new session.web.form.FormOpenPopup(this);
pop.show_element(
'res.partner',
- id,
- {
- 'force_email': true,
+ partner_id,
+ { 'force_email': true,
'ref': "compound_context",
- },
- {
+ 'default_name': parsed_email[0],
+ 'default_email': parsed_email[1],
+ }, {
title: _t("Please complete partner's informations"),
}
);
pop.on('closed', self, function () {
deferred.resolve();
});
- partners_from.push(id);
+ pop.view_form.on('on_button_cancel', self, function () {
+ names_to_remove.push(partner_name);
+ if (partner_id) {
+ recipient_ids_to_remove.push(partner_id);
+ }
+ });
});
- $.when.apply( $, deferreds ).then(function () {
- deferred_check.resolve(partners_from);
+
+ $.when.apply($, emails_deferred).then(function () {
+ var new_names_to_find = _.difference(names_to_find, names_to_remove);
+ find_done = $.Deferred();
+ if (new_names_to_find.length > 0) {
+ find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [new_names_to_find, true]);
+ }
+ else {
+ find_done.resolve([]);
+ }
+ $.when(find_done).pipe(function (result) {
+ var recipient_popups = result.concat(recipients_to_check);
+ _.each(recipient_popups, function (partner_info) {
+ if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) {
+ recipient_ids.push(partner_info.partner_id);
+ }
+ });
+ }).pipe(function () {
+ check_done.resolve(recipient_ids);
+ });
});
});
- return deferred_check;
+
+ return check_done;
},
on_message_post: function (event) {
var self = this;
if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) {
- // create list of new partners
- this.check_recipient_partners().done(function (partner_ids) {
- self.do_send_message_post(partner_ids);
- });
+ if (this.is_log) {
+ this.do_send_message_post([], this.is_log);
+ }
+ else {
+ this.check_recipient_partners().done(function (partner_ids) {
+ self.do_send_message_post(partner_ids, self.is_log);
+ });
+ }
}
},
- /*do post a message and fetch the message*/
- do_send_message_post: function (partner_ids) {
+ /* do post a message and fetch the message */
+ do_send_message_post: function (partner_ids, log) {
var self = this;
- this.parent_thread.ds_thread._model.call('message_post_user_api', [this.context.default_res_id], {
+ var values = {
'body': this.$('textarea').val(),
'subject': false,
'parent_id': this.context.default_parent_id,
'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}),
'partner_ids': partner_ids,
- 'context': this.parent_thread.context,
- }).done(function (message_id) {
+ 'context': _.extend(this.parent_thread.context, {
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
+ }),
+ 'type': 'comment',
+ 'content_subtype': 'plaintext',
+ };
+ if (log) {
+ values['subtype'] = false;
+ }
+ else {
+ values['subtype'] = 'mail.mt_comment';
+ }
+ this.parent_thread.ds_thread._model.call('message_post', [this.context.default_res_id], values).done(function (message_id) {
var thread = self.parent_thread;
var root = thread == self.options.root_thread;
if (self.options.display_indented_thread < self.thread_level && thread.parent_message) {
@@ -650,18 +738,55 @@ openerp.mail = function (session) {
});
},
- /* convert the compact mode into the compose message
- */
- on_compose_expandable: function (event) {
- this.get_emails_from();
- if ((!this.stay_open || (event && event.type == 'click')) && (!this.show_composer || !this.$('textarea:not(.oe_compact)').val().match(/\S+/) && !this.attachment_ids.length)) {
- this.show_composer = !this.show_composer || this.stay_open;
- this.reinit();
+ /* Quick composer: toggle minimal / expanded mode
+ * - toggle minimal (one-liner) / expanded (textarea, buttons) mode
+ * - when going into expanded mode:
+ * - call `message_get_suggested_recipients` to have a list of partners to add
+ * - compute email_from list (list of unknown email_from to propose to create partners)
+ */
+ on_toggle_quick_composer: function (event) {
+ var self = this;
+ var $input = $(event.target);
+ this.compute_emails_from();
+ var email_addresses = _.pluck(this.recipients, 'email_address');
+ var suggested_partners = $.Deferred();
+
+ // if clicked: call for suggested recipients
+ if (event.type == 'click') {
+ this.is_log = $input.hasClass('oe_compose_log');
+ suggested_partners = this.parent_thread.ds_thread.call('message_get_suggested_recipients', [[this.context.default_res_id]]).done(function (additional_recipients) {
+ var thread_recipients = additional_recipients[self.context.default_res_id];
+ _.each(thread_recipients, function (recipient) {
+ var parsed_email = mail.ChatterUtils.parse_email(recipient[1]);
+ if (_.indexOf(email_addresses, parsed_email[1]) == -1) {
+ self.recipients.push({
+ 'checked': true,
+ 'partner_id': recipient[0],
+ 'full_name': recipient[1],
+ 'name': parsed_email[0],
+ 'email_address': parsed_email[1],
+ 'reason': recipient[2],
+ })
+ }
+ });
+ });
}
- if (!this.stay_open && this.show_composer && (!event || event.type != 'blur')) {
- this.$('textarea:not(.oe_compact):first').focus();
+ else {
+ suggested_partners.resolve({});
}
- return true;
+
+ // when call for suggested partners finished: re-render the widget
+ $.when(suggested_partners).pipe(function (additional_recipients) {
+ if ((!self.stay_open || (event && event.type == 'click')) && (!self.show_composer || !self.$('textarea:not(.oe_compact)').val().match(/\S+/) && !self.attachment_ids.length)) {
+ self.show_composer = !self.show_composer || self.stay_open;
+ self.reinit();
+ }
+ if (!self.stay_open && self.show_composer && (!event || event.type != 'blur')) {
+ self.$('textarea:not(.oe_compact):first').focus();
+ }
+ });
+
+ return suggested_partners;
},
do_hide_compact: function () {
@@ -678,7 +803,10 @@ openerp.mail = function (session) {
}
},
- get_emails_from: function () {
+ /** Compute the list of unknown email_from the the given thread
+ * TDE FIXME: seems odd to delegate to the composer
+ * TDE TODO: please de-obfuscate and comment your code */
+ compute_emails_from: function () {
var self = this;
var messages = [];
@@ -691,22 +819,28 @@ openerp.mail = function (session) {
// get all wall messages if is not a mail.Wall
_.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());});
}
-
+
_.each(messages, function (thread) {
if (thread.author_id && !thread.author_id[0] &&
- !_.find(self.emails_from, function (from) {return from[0][4] == thread.author_id[4];})) {
- self.emails_from.push([thread.author_id, true]);
+ !_.find(self.recipients, function (recipient) {return recipient.email_address == thread.author_id[3];})) {
+ self.recipients.push({ 'full_name': thread.author_id[1],
+ 'name': thread.author_id[2],
+ 'email_address': thread.author_id[3],
+ 'partner_id': false,
+ 'checked': true,
+ 'reason': 'Incoming email author'
+ });
}
});
- return self.emails_from;
+ return self.recipients;
},
- on_checked_email_from: function (event) {
+ on_checked_recipient: function (event) {
var $input = $(event.target);
var email = $input.attr("data");
- _.each(this.emails_from, function (email_from) {
- if (email_from[0][4] == email) {
- email_from[1] = $input.is(":checked");
+ _.each(this.recipients, function (recipient) {
+ if (recipient.email_address == email) {
+ recipient.checked = $input.is(":checked");
}
});
},
@@ -1236,12 +1370,12 @@ openerp.mail = function (session) {
/**
*If compose_message doesn't exist, instantiate the compose message.
- * Call the on_compose_expandable method to allow the user to write his message.
+ * Call the on_toggle_quick_composer method to allow the user to write his message.
* (Is call when a user click on "Reply" button)
*/
on_compose_message: function (event) {
this.instantiate_compose_message();
- this.compose_message.on_compose_expandable(event);
+ this.compose_message.on_toggle_quick_composer(event);
return false;
},
@@ -1810,7 +1944,7 @@ openerp.mail = function (session) {
};
session.client.action_manager.do_action(action);
});
- this.$(".oe_write_onwall").click(function(){ self.root.thread.on_compose_message(); });
+ this.$(".oe_write_onwall").click(function (event) { self.root.thread.on_compose_message(event); });
}
});
diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js
index 41634fd2135..6fc2f0fdfcd 100644
--- a/addons/mail/static/src/js/mail_followers.js
+++ b/addons/mail/static/src/js/mail_followers.js
@@ -106,9 +106,12 @@ openerp_mail_followers = function(session, mail) {
on_remove_follower: function (event) {
var partner_id = $(event.target).data('id');
- var context = new session.web.CompoundContext(this.build_context(), {});
- return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context])
- .then(this.proxy('read_value'));
+ var name = $(event.target).parent().find("a").html();
+ if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) {
+ var context = new session.web.CompoundContext(this.build_context(), {});
+ return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context])
+ .then(this.proxy('read_value'));
+ }
},
read_value: function () {
@@ -217,23 +220,16 @@ openerp_mail_followers = function(session, mail) {
/** Display subtypes: {'name': default, followed} */
display_subtypes:function (data, id) {
var self = this;
- var subtype_list_ul = this.$('.oe_subtype_list');
- var records = [];
- var nb_subtype = 0;
- subtype_list_ul.empty();
- if (data[id]) {
- records = data[id].message_subtype_data;
- }
- _(records).each(function (record) {nb_subtype++;});
- if (nb_subtype > 1) {
- this.$('hr').show();
- _(records).each(function (record, record_name) {
- record.name = record_name;
- record.followed = record.followed || undefined;
- $(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo( self.$('.oe_subtype_list') );
- });
- } else {
- this.$('hr').hide();
+ var $list = this.$('.oe_subtype_list');
+ $list.empty().hide();
+ var records = data[this.view.datarecord.id || this.view.dataset.ids[0]].message_subtype_data;
+ _(records).each(function (record, record_name) {
+ record.name = record_name;
+ record.followed = record.followed || undefined;
+ $(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo( self.$('.oe_subtype_list') );
+ });
+ if (_.size(records) > 1) {
+ $list.show();
}
},
@@ -248,12 +244,15 @@ openerp_mail_followers = function(session, mail) {
},
do_unfollow: function () {
- _(this.$('.oe_msg_subtype_check')).each(function (record) {
- $(record).attr('checked',false);
- });
- var context = new session.web.CompoundContext(this.build_context(), {});
- return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context])
- .then(this.proxy('read_value'));
+ if (confirm(_t("Warning! \nYou won't be notified of any email or discussion on this document. Do you really want to unfollow this document ?"))) {
+ _(this.$('.oe_msg_subtype_check')).each(function (record) {
+ $(record).attr('checked',false);
+ });
+ var context = new session.web.CompoundContext(this.build_context(), {});
+ return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context])
+ .then(this.proxy('read_value'));
+ }
+ return false;
},
do_update_subscription: function (event) {
@@ -267,7 +266,9 @@ openerp_mail_followers = function(session, mail) {
});
if (!checklist.length) {
- this.do_unfollow();
+ if (!this.do_unfollow()) {
+ $(event.target).attr("checked", "checked");
+ }
} else {
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], checklist, context])
diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml
index 4693adeb1ee..b26ef06f5e4 100644
--- a/addons/mail/static/src/xml/mail.xml
+++ b/addons/mail/static/src/xml/mail.xml
@@ -26,24 +26,35 @@
+ ò
-
-
- Write to the followers of this document...
- Share with my followers...
-
') % user_name
return result
_columns = {
@@ -46,8 +53,12 @@ class invite_wizard(osv.osv_memory):
help='Model of the followed resource'),
'res_id': fields.integer('Related Document ID', select=1,
help='Id of the followed resource'),
- 'partner_ids': fields.many2many('res.partner', string='Partners'),
+ 'partner_ids': fields.many2many('res.partner', string='Recipients',
+ help="List of partners that will be added as follower of the current document."),
'message': fields.html('Message'),
+ 'send_mail': fields.boolean('Send Email',
+ help="If checked, the partners will receive an email warning they have been "
+ "added in the document's followers."),
}
def add_followers(self, cr, uid, ids, context=None):
@@ -59,23 +70,27 @@ class invite_wizard(osv.osv_memory):
new_follower_ids = [p.id for p in wizard.partner_ids if p not in document.message_follower_ids]
model_obj.message_subscribe(cr, uid, [wizard.res_id], new_follower_ids, context=context)
- # send an email
- if wizard.message:
+ ir_model = self.pool.get('ir.model')
+ model_ids = ir_model.search(cr, uid, [('model', '=', model_obj._name)], context=context)
+ model_name = ir_model.name_get(cr, uid, model_ids, context=context)[0][1]
+
+ # send an email if option checked and if a message exists (do not send void emails)
+ if wizard.send_mail and wizard.message:
# add signature
- user_id = self.pool.get("res.users").read(cr, uid, [uid], fields=["signature"], context=context)[0]
- signature = user_id and user_id["signature"] or ''
- if signature:
- wizard.message = tools.append_content_to_html(wizard.message, signature, plaintext=True, container_tag='div')
+ # FIXME 8.0: use notification_email_send, send a wall message and let mail handle email notification + message box
+ signature_company = self.pool.get('mail.notification').get_signature_footer(cr, uid, user_id=uid, res_model=wizard.res_model, res_id=wizard.res_id, context=context)
+ wizard.message = tools.append_content_to_html(wizard.message, signature_company, plaintext=False, container_tag='div')
+
# send mail to new followers
- for follower_id in new_follower_ids:
- mail_mail = self.pool.get('mail.mail')
- # the invite wizard should create a private message not related to any object -> no model, no res_id
- mail_id = mail_mail.create(cr, uid, {
- 'model': wizard.res_model,
- 'res_id': wizard.res_id,
- 'subject': 'Invitation to follow %s' % document.name_get()[0][1],
- 'body_html': '%s' % wizard.message,
- 'auto_delete': True,
- }, context=context)
- mail_mail.send(cr, uid, [mail_id], recipient_ids=[follower_id], context=context)
+ # the invite wizard should create a private message not related to any object -> no model, no res_id
+ mail_mail = self.pool.get('mail.mail')
+ mail_id = mail_mail.create(cr, uid, {
+ 'model': wizard.res_model,
+ 'res_id': wizard.res_id,
+ 'subject': _('Invitation to follow %s: %s') % (model_name, document.name_get()[0][1]),
+ 'body_html': '%s' % wizard.message,
+ 'auto_delete': True,
+ 'recipient_ids': [(4, id) for id in new_follower_ids]
+ }, context=context)
+ mail_mail.send(cr, uid, [mail_id], context=context)
return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/mail/wizard/invite_view.xml b/addons/mail/wizard/invite_view.xml
index e957d546d37..2d550da2ecd 100644
--- a/addons/mail/wizard/invite_view.xml
+++ b/addons/mail/wizard/invite_view.xml
@@ -14,7 +14,8 @@
-
+
+