diff --git a/addons/account/account.py b/addons/account/account.py
index 3537c4f4c55..b87caf547ab 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -840,16 +840,11 @@ class account_journal(osv.osv):
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
if not args:
args = []
- if context is None:
- context = {}
- ids = []
- if context.get('journal_type', False):
- args += [('type','=',context.get('journal_type'))]
- if name:
- ids = self.search(cr, user, [('code', 'ilike', name)]+ args, limit=limit, context=context)
- if not ids:
- ids = self.search(cr, user, [('name', 'ilike', name)]+ args, limit=limit, context=context)#fix it ilike should be replace with operator
-
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ domain = [('code', operator, name), ('name', operator, name)]
+ else:
+ domain = ['|', ('code', operator, name), ('name', operator, name)]
+ ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context)
return self.name_get(cr, user, ids, context=context)
@@ -938,13 +933,11 @@ class account_fiscalyear(osv.osv):
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
if args is None:
args = []
- if context is None:
- context = {}
- ids = []
- if name:
- ids = self.search(cr, user, [('code', 'ilike', name)]+ args, limit=limit)
- if not ids:
- ids = self.search(cr, user, [('name', operator, name)]+ args, limit=limit)
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ domain = [('code', operator, name), ('name', operator, name)]
+ else:
+ domain = ['|', ('code', operator, name), ('name', operator, name)]
+ ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context)
return self.name_get(cr, user, ids, context=context)
@@ -1040,19 +1033,11 @@ class account_period(osv.osv):
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
if args is None:
args = []
- if context is None:
- context = {}
- ids = []
- if name:
- ids = self.search(cr, user,
- [('code', 'ilike', name)] + args,
- limit=limit,
- context=context)
- if not ids:
- ids = self.search(cr, user,
- [('name', operator, name)] + args,
- limit=limit,
- context=context)
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ domain = [('code', operator, name), ('name', operator, name)]
+ else:
+ domain = ['|', ('code', operator, name), ('name', operator, name)]
+ ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context)
return self.name_get(cr, user, ids, context=context)
def write(self, cr, uid, ids, vals, context=None):
@@ -1187,36 +1172,6 @@ class account_move(osv.osv):
'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}
- result format: {[(id, name), (id, name), ...]}
-
- @param cr: A database cursor
- @param user: ID of the user currently logged in
- @param name: name to search
- @param args: other arguments
- @param operator: default operator is 'ilike', it can be changed
- @param context: context arguments, like lang, time zone
- @param limit: Returns first 'n' ids of complete result, default is 80.
-
- @return: Returns a list of tuples containing id and name
- """
-
- if not args:
- args = []
- ids = []
- if name:
- ids += self.search(cr, user, [('name','ilike',name)]+args, limit=limit, context=context)
-
- if not ids and name and type(name) == int:
- ids += self.search(cr, user, [('id','=',name)]+args, limit=limit, context=context)
-
- if not ids:
- ids += self.search(cr, user, args, limit=limit, context=context)
-
- return self.name_get(cr, user, ids, context=context)
-
def name_get(self, cursor, user, ids, context=None):
if isinstance(ids, (int, long)):
ids = [ids]
@@ -1842,10 +1797,12 @@ class account_tax_code(osv.osv):
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
if not args:
args = []
- if context is None:
- context = {}
- ids = self.search(cr, user, ['|',('name',operator,name),('code',operator,name)] + args, limit=limit, context=context)
- return self.name_get(cr, user, ids, context)
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ domain = [('code', operator, name), ('name', operator, name)]
+ else:
+ domain = ['|', ('code', operator, name), ('name', operator, name)]
+ ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context)
+ return self.name_get(cr, user, ids, context=context)
def name_get(self, cr, uid, ids, context=None):
if isinstance(ids, (int, long)):
@@ -1974,15 +1931,11 @@ class account_tax(osv.osv):
"""
if not args:
args = []
- if context is None:
- context = {}
- ids = []
- if name:
- ids = self.search(cr, user, [('description', '=', name)] + args, limit=limit, context=context)
- if not ids:
- ids = self.search(cr, user, [('name', operator, name)] + args, limit=limit, context=context)
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ domain = [('description', operator, name), ('name', operator, name)]
else:
- ids = self.search(cr, user, args, limit=limit, context=context or {})
+ domain = ['|', ('description', operator, name), ('name', operator, name)]
+ ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context)
return self.name_get(cr, user, ids, context=context)
def write(self, cr, uid, ids, vals, context=None):
diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py
index eb7d9926bbf..f7e561c8e24 100644
--- a/addons/account/account_invoice.py
+++ b/addons/account/account_invoice.py
@@ -672,25 +672,14 @@ class account_invoice(osv.osv):
self.create_workflow(cr, uid, ids)
return True
- # ----------------------------------------
- # Mail related methods
- # ----------------------------------------
-
- def _get_formview_action(self, cr, uid, id, context=None):
+ def get_formview_id(self, cr, uid, id, context=None):
""" Update form view id of action to open the invoice """
- action = super(account_invoice, self)._get_formview_action(cr, uid, id, context=context)
obj = self.browse(cr, uid, id, context=context)
if obj.type == 'in_invoice':
model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
- action.update({
- 'views': [(view_id, 'form')],
- })
else:
model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
- action.update({
- 'views': [(view_id, 'form')],
- })
- return action
+ return view_id
# Workflow stuff
#################
diff --git a/addons/account/wizard/account_invoice_refund.py b/addons/account/wizard/account_invoice_refund.py
index 8a583b79383..02b046edfcf 100644
--- a/addons/account/wizard/account_invoice_refund.py
+++ b/addons/account/wizard/account_invoice_refund.py
@@ -165,7 +165,7 @@ class account_invoice_refund(osv.osv_memory):
to_reconcile_ids = {}
for line in movelines:
if line.account_id.id == inv.account_id.id:
- to_reconcile_ids[line.account_id.id] = [line.id]
+ to_reconcile_ids.setdefault(line.account_id.id, []).append(line.id)
if line.reconcile_id:
line.reconcile_id.unlink()
inv_obj.signal_invoice_open(cr, uid, [refund.id])
diff --git a/addons/account_budget/account_budget.py b/addons/account_budget/account_budget.py
index e6fc3a29668..753a5df79f4 100644
--- a/addons/account_budget/account_budget.py
+++ b/addons/account_budget/account_budget.py
@@ -162,7 +162,7 @@ class crossovered_budget_lines(osv.osv):
elapsed = strToDate(date_to) - strToDate(date_to)
if total.days:
- theo_amt = float(elapsed.days / float(total.days)) * line.planned_amount
+ theo_amt = float((elapsed.days + 1) / float(total.days + 1)) * line.planned_amount
else:
theo_amt = line.planned_amount
diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js
index cf756e121da..75854708055 100644
--- a/addons/board/static/src/js/dashboard.js
+++ b/addons/board/static/src/js/dashboard.js
@@ -88,7 +88,7 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({
var qdict = {
current_layout : this.$el.find('.oe_dashboard').attr('data-layout')
};
- var $dialog = instance.web.Dialog(this, {
+ var $dialog = new instance.web.Dialog(this, {
title: _t("Edit Layout"),
}, QWeb.render('DashBoard.layouts', qdict)).open();
$dialog.find('li').click(function() {
diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py
index add0030d76c..6dff8ee2e8b 100644
--- a/addons/calendar/calendar.py
+++ b/addons/calendar/calendar.py
@@ -197,6 +197,10 @@ class calendar_attendee(osv.Model):
@param email_from: email address for user sending the mail
"""
res = False
+
+ if self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_mail', default=False):
+ return res
+
mail_ids = []
data_pool = self.pool['ir.model.data']
mailmess_pool = self.pool['mail.message']
@@ -431,7 +435,7 @@ class calendar_alarm_manager(osv.AbstractModel):
if cron and len(cron) == 1:
cron = self.pool.get('ir.cron').browse(cr, uid, cron[0], context=context)
else:
- raise ("Cron for " + self._name + " not identified :( !")
+ _logger.exception("Cron for " + self._name + " can not be identified !")
if cron.interval_type == "weeks":
cron_interval = cron.interval_number * 7 * 24 * 60 * 60
@@ -445,7 +449,7 @@ class calendar_alarm_manager(osv.AbstractModel):
cron_interval = cron.interval_number
if not cron_interval:
- raise ("Cron delay for " + self._name + " can not be calculated :( !")
+ _logger.exception("Cron delay can not be computed !")
all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
@@ -649,7 +653,7 @@ class calendar_event(osv.Model):
_inherit = ["mail.thread", "ir.needaction_mixin"]
def do_run_scheduler(self, cr, uid, id, context=None):
- self.pool['calendar.alarm_manager'].do_run_scheduler(cr, uid, context=context)
+ self.pool['calendar.alarm_manager'].get_next_mail(cr, uid, context=context)
def get_recurrent_date_by_event(self, cr, uid, event, context=None):
"""Get recurrent dates based on Rule string and all event where recurrent_id is child
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index bf9b6533b4a..210b8c94225 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -79,6 +79,7 @@ class crm_lead(format_address, osv.osv):
'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.fold and obj.stage_id.sequence > 1,
},
}
+ _mail_mass_mailing = _('Leads / Opportunities')
def get_empty_list_help(self, cr, uid, help, context=None):
if context.get('default_type') == 'lead':
@@ -971,15 +972,13 @@ class crm_lead(format_address, osv.osv):
return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False
for lead in self.browse(cr, SUPERUSER_ID, ids, context=context)]
- def _get_formview_action(self, cr, uid, id, context=None):
- action = super(crm_lead, self)._get_formview_action(cr, uid, id, context=context)
+ def get_formview_id(self, cr, uid, id, context=None):
obj = self.browse(cr, uid, id, context=context)
if obj.type == 'opportunity':
model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
- action.update({
- 'views': [(view_id, 'form')],
- })
- return action
+ else:
+ view_id = super(crm_lead, self).get_formview_id(cr, uid, id, model=model, context=context)
+ return view_id
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context)
diff --git a/addons/email_template/__openerp__.py b/addons/email_template/__openerp__.py
index b7977660134..298754c8b8c 100644
--- a/addons/email_template/__openerp__.py
+++ b/addons/email_template/__openerp__.py
@@ -63,7 +63,7 @@ campaigns on any OpenERP document.
'wizard/mail_compose_message_view.xml',
'security/ir.model.access.csv'
],
- 'demo': ['res_partner_demo.yml'],
+ 'demo': [],
'installable': True,
'auto_install': True,
'images': ['images/1_email_account.jpeg','images/2_email_template.jpeg','images/3_emails.jpeg'],
diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py
index 3f0fad1c8a0..dd9ed49d3ef 100644
--- a/addons/email_template/email_template.py
+++ b/addons/email_template/email_template.py
@@ -231,6 +231,11 @@ class email_template(osv.osv):
'email_from': fields.char('From',
help="Sender address (placeholders may be used here). If not set, the default "
"value will be the author's email alias if configured, or email address."),
+ 'use_default_to': fields.boolean(
+ 'Default recipients',
+ help="Default recipients of the record:\n"
+ "- partner (using id on a partner or the partner_id field) OR\n"
+ "- email (using email_from or email field)"),
'email_to': fields.char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)"),
'partner_to': fields.char('To (Partners)',
help="Comma-separated ids of recipient partners (placeholders may be used here)",
@@ -386,6 +391,37 @@ class email_template(osv.osv):
})
return {'value': result}
+ def generate_recipients_batch(self, cr, uid, results, template_id, res_ids, context=None):
+ """Generates the recipients of the template. Default values can ben generated
+ instead of the template values if requested by template or context.
+ Emails (email_to, email_cc) can be transformed into partners if requested
+ in the context. """
+ if context is None:
+ context = {}
+ template = self.browse(cr, uid, template_id, context=context)
+
+ if template.use_default_to or context.get('tpl_force_default_to'):
+ ctx = dict(context, thread_model=template.model)
+ default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx)
+ for res_id, recipients in default_recipients.iteritems():
+ results[res_id].pop('partner_to', None)
+ results[res_id].update(recipients)
+
+ for res_id, values in results.iteritems():
+ partner_ids = values.get('partner_ids', list())
+ if context and context.get('tpl_partners_only'):
+ mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', ''))
+ for mail in mails:
+ partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context)
+ partner_ids.append(partner_id)
+ partner_to = values.pop('partner_to', '')
+ if partner_to:
+ # placeholders could generate '', 3, 2 due to some empty field values
+ tpl_partner_ids = [pid for pid in partner_to.split(',') if pid]
+ partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)
+ results[res_id]['partner_ids'] = partner_ids
+ return results
+
def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None):
"""Generates an email from the template for given the given model based on
records given by res_ids.
@@ -420,14 +456,18 @@ class email_template(osv.osv):
context=context)
for res_id, field_value in generated_field_values.iteritems():
results.setdefault(res_id, dict())[field] = field_value
+ # compute recipients
+ results = self.generate_recipients_batch(cr, uid, results, template.id, template_res_ids, context=context)
# update values for all res_ids
for res_id in template_res_ids:
values = results[res_id]
+ # body: add user signature, sanitize
if 'body_html' in fields and template.user_signature:
signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
if values.get('body_html'):
values['body'] = tools.html_sanitize(values['body_html'])
+ # technical settings
values.update(
mail_server_id=template.mail_server_id.id or False,
auto_delete=template.auto_delete,
@@ -484,17 +524,8 @@ class email_template(osv.osv):
# create a mail_mail based on values, without attachments
values = self.generate_email(cr, uid, template_id, res_id, context=context)
if not values.get('email_from'):
- raise osv.except_osv(_('Warning!'),_("Sender email is missing or empty after template rendering. Specify one to deliver your message"))
- # process partner_to field that is a comma separated list of partner_ids -> recipient_ids
- # NOTE: only usable if force_send is True, because otherwise the value is
- # not stored on the mail_mail, and therefore lost -> fixed in v8
- values['recipient_ids'] = []
- partner_to = values.pop('partner_to', '')
- if partner_to:
- # placeholders could generate '', 3, 2 due to some empty field values
- tpl_partner_ids = [pid for pid in partner_to.split(',') if pid]
- values['recipient_ids'] += [(4, pid) for pid in self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)]
-
+ raise osv.except_osv(_('Warning!'), _("Sender email is missing or empty after template rendering. Specify one to deliver your message"))
+ values['recipient_ids'] = [(4, pid) for pid in values.get('partner_ids', list())]
attachment_ids = values.pop('attachment_ids', [])
attachments = values.pop('attachments', [])
msg_id = mail_mail.create(cr, uid, values, context=context)
diff --git a/addons/email_template/email_template_view.xml b/addons/email_template/email_template_view.xml
index 4f7828cb531..6dda3e97571 100644
--- a/addons/email_template/email_template_view.xml
+++ b/addons/email_template/email_template_view.xml
@@ -9,8 +9,10 @@
-
-
+
+
+
+
@@ -25,43 +27,28 @@
context="{'template_id':active_id}"/>
-
-
-
-
-
-
-
-
-
-
-
-
- Dynamic placeholder generator
-
-
-
-
-
-
-
- Body
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -72,6 +59,21 @@
attrs="{'invisible':[('report_template','=',False)]}"/>
+
+
+
+
+
+
+
+
+
diff --git a/addons/email_template/res_partner_demo.yml b/addons/email_template/res_partner_demo.yml
deleted file mode 100644
index 8eaf48f9e87..00000000000
--- a/addons/email_template/res_partner_demo.yml
+++ /dev/null
@@ -1,9 +0,0 @@
--
- Set opt-out to True on all demo partners
--
- !python {model: res.partner}: |
- partner_ids = self.search(cr, uid, [])
- # assume partners with an external ID come from demo data
- ext_ids = self._get_external_ids(cr, uid, partner_ids)
- ids_to_update = [k for (k,v) in ext_ids.iteritems() if v]
- self.write(cr, uid, ids_to_update, {'opt_out': True})
\ No newline at end of file
diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py
index 580421fe67e..8bc6d62017c 100644
--- a/addons/email_template/tests/test_mail.py
+++ b/addons/email_template/tests/test_mail.py
@@ -74,7 +74,7 @@ class test_message_compose(TestMail):
# 1. Comment on pigs
compose_id = mail_compose.create(cr, uid,
- {'subject': 'Forget me subject', 'body': 'Dummy body
', 'post': True},
+ {'subject': 'Forget me subject', 'body': 'Dummy body
'},
{'default_composition_mode': 'comment',
'default_model': 'mail.group',
'default_res_id': self.group_pigs_id,
@@ -102,7 +102,7 @@ class test_message_compose(TestMail):
'default_template_id': email_template_id,
'active_ids': [self.group_pigs_id, self.group_bird_id]
}
- compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context)
+ compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context)
compose = mail_compose.browse(cr, uid, compose_id, context)
onchange_res = compose.onchange_template_id(email_template_id, 'comment', 'mail.group', self.group_pigs_id)['value']
onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])]
@@ -146,7 +146,7 @@ class test_message_compose(TestMail):
'default_partner_ids': [p_a_id],
'active_ids': [self.group_pigs_id, self.group_bird_id]
}
- compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context)
+ compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context)
compose = mail_compose.browse(cr, uid, compose_id, context)
onchange_res = compose.onchange_template_id(email_template_id, 'mass_mail', 'mail.group', self.group_pigs_id)['value']
onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])]
@@ -172,12 +172,12 @@ class test_message_compose(TestMail):
self.assertIn(_body_html1, message_pigs.body, 'mail.message body on Pigs incorrect')
self.assertIn(_body_html2, message_bird.body, 'mail.message body on Bird incorrect')
# Test: partner_ids: p_a_id (default) + 3 newly created partners
- message_pigs_pids = [partner.id for partner in message_pigs.notified_partner_ids]
- message_bird_pids = [partner.id for partner in message_bird.notified_partner_ids]
- partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])])
- partner_ids.append(p_a_id)
- self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of notified_partner_ids')
- self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird notified_partner_ids incorrect')
+ # message_pigs_pids = [partner.id for partner in message_pigs.notified_partner_ids]
+ # message_bird_pids = [partner.id for partner in message_bird.notified_partner_ids]
+ # partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])])
+ # partner_ids.append(p_a_id)
+ # self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of notified_partner_ids')
+ # self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird notified_partner_ids incorrect')
# ----------------------------------------
# CASE4: test newly introduced partner_to field
@@ -237,8 +237,8 @@ class test_message_compose(TestMail):
email_template.send_mail(cr, uid, email_template_id, self.group_pigs_id, force_send=True, context=context)
sent_emails = self._build_email_kwargs_list
email_to_lst = [
- ['b@b.b', 'c@c.c'], ['"Followers of Pigs" '],
- ['"Followers of Pigs" '], ['"Followers of Pigs" ']]
+ ['b@b.b', 'c@c.c'], ['Administrator '],
+ ['Raoul Grosbedon '], ['Bert Tartignole ']]
self.assertEqual(len(sent_emails), 4, 'email_template: send_mail: 3 valid email recipients + email_to -> should send 4 emails')
for email in sent_emails:
self.assertIn(email['email_to'], email_to_lst, 'email_template: send_mail: wrong email_recipients')
diff --git a/addons/email_template/wizard/email_template_preview.py b/addons/email_template/wizard/email_template_preview.py
index 5fee415a75e..104437ec0d3 100644
--- a/addons/email_template/wizard/email_template_preview.py
+++ b/addons/email_template/wizard/email_template_preview.py
@@ -66,6 +66,7 @@ class email_template_preview(osv.osv_memory):
_columns = {
'res_id': fields.selection(_get_records, 'Sample Document'),
+ 'partner_ids': fields.many2many('res.partner', string='Recipients'),
}
def on_change_res_id(self, cr, uid, ids, res_id, context=None):
@@ -80,7 +81,7 @@ class email_template_preview(osv.osv_memory):
# generate and get template values
mail_values = email_template.generate_email(cr, uid, template_id, res_id, context=context)
- vals = dict((field, mail_values.get(field, False)) for field in ('email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to'))
+ vals = dict((field, mail_values.get(field, False)) for field in ('email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to', 'partner_ids', 'attachment_ids'))
vals['name'] = template.name
return {'value': vals}
diff --git a/addons/email_template/wizard/email_template_preview_view.xml b/addons/email_template/wizard/email_template_preview_view.xml
index 0a74f7d813e..87c1e92fa60 100644
--- a/addons/email_template/wizard/email_template_preview_view.xml
+++ b/addons/email_template/wizard/email_template_preview_view.xml
@@ -8,14 +8,17 @@
@@ -30,10 +34,11 @@
Template Preview
email_template.preview
- email_template.preview
+ email.template
ir.actions.act_window
form
form
+
new
{'template_id':active_id}
diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py
index 71f99471afa..676f11b23e3 100644
--- a/addons/email_template/wizard/mail_compose_message.py
+++ b/addons/email_template/wizard/mail_compose_message.py
@@ -42,7 +42,8 @@ class mail_compose_message(osv.TransientModel):
_inherit = 'mail.compose.message'
def default_get(self, cr, uid, fields, context=None):
- """ Override to pre-fill the data when having a template in single-email mode """
+ """ Override to pre-fill the data when having a template in single-email mode
+ and not going through the view: the on_change is not called in that case. """
if context is None:
context = {}
res = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
@@ -50,19 +51,13 @@ class mail_compose_message(osv.TransientModel):
res.update(
self.onchange_template_id(
cr, uid, [], context['default_template_id'], res.get('composition_mode'),
- res.get('model'), res.get('res_id', context.get('active_id')), context=context
+ res.get('model'), res.get('res_id'), context=context
)['value']
)
return res
_columns = {
'template_id': fields.many2one('email.template', 'Use template', select=True),
- 'partner_to': fields.char('To (Partner IDs)',
- help="Comma-separated list of recipient partners ids (placeholders may be used here)"),
- 'email_to': fields.char('To (Emails)',
- help="Comma-separated recipient addresses (placeholders may be used here)",),
- 'email_cc': fields.char('Cc (Emails)',
- help="Carbon copy recipients (placeholders may be used here)"),
}
def send_mail(self, cr, uid, ids, context=None):
@@ -92,14 +87,13 @@ class mail_compose_message(osv.TransientModel):
""" - mass_mailing: we cannot render, so return the template values
- normal mode: return rendered values """
if template_id and composition_mode == 'mass_mail':
- fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id']
+ fields = ['subject', 'body_html', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id']
template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
elif template_id:
values = self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context=context)[res_id]
# transform attachments into attachment_ids; not attached to the document because this will
# be done further in the posting process, allowing to clean database if email not send
- values['attachment_ids'] = values.pop('attachment_ids', [])
ir_attach_obj = self.pool.get('ir.attachment')
for attach_fname, attach_datas in values.pop('attachments', []):
data_attach = {
@@ -110,7 +104,7 @@ class mail_compose_message(osv.TransientModel):
'res_id': 0,
'type': 'binary', # override default_type from context, possibly meant for another model!
}
- values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context))
+ values.setdefault('attachment_ids', list()).append(ir_attach_obj.create(cr, uid, data_attach, context=context))
else:
values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'email_to', 'email_cc', 'partner_to', 'reply_to', 'attachment_ids', 'mail_server_id'], context=context)
@@ -148,47 +142,29 @@ class mail_compose_message(osv.TransientModel):
# Wizard validation and send
#------------------------------------------------------
- def _get_or_create_partners_from_values(self, cr, uid, rendered_values, context=None):
- """ Check for email_to, email_cc, partner_to """
- partner_ids = []
- mails = tools.email_split(rendered_values.pop('email_to', '')) + tools.email_split(rendered_values.pop('email_cc', ''))
- for mail in mails:
- partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context)
- partner_ids.append(partner_id)
- partner_to = rendered_values.pop('partner_to', '')
- if partner_to:
- # placeholders could generate '', 3, 2 due to some empty field values
- tpl_partner_ids = [pid for pid in partner_to.split(',') if pid]
- partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)
- return partner_ids
-
def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None, fields=None):
""" Call email_template.generate_email(), get fields relevant for
mail.compose.message, transform email_cc and email_to into partner_ids """
- # filter template values
+ if context is None:
+ context = {}
if fields is None:
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id']
- returned_fields = fields + ['attachments']
+ returned_fields = fields + ['partner_ids', 'attachments']
values = dict.fromkeys(res_ids, False)
- template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=context)
+ ctx = dict(context, tpl_partners_only=True)
+ template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=ctx)
for res_id in res_ids:
res_id_values = dict((field, template_values[res_id][field]) for field in returned_fields if template_values[res_id].get(field))
res_id_values['body'] = res_id_values.pop('body_html', '')
-
- # transform email_to, email_cc into partner_ids
- ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_'))
- partner_ids = self._get_or_create_partners_from_values(cr, uid, res_id_values, context=ctx)
- # legacy template behavior: void values do not erase existing values and the
- # related key is removed from the values dict
- if partner_ids:
- res_id_values['partner_ids'] = list(partner_ids)
-
values[res_id] = res_id_values
return values
def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
""" Override to handle templates. """
+ # generate composer values
+ composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context)
+
# generate template-based values
if wizard.template_id:
template_values = self.generate_email_for_composer_batch(
@@ -196,17 +172,18 @@ class mail_compose_message(osv.TransientModel):
fields=['email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id'],
context=context)
else:
- template_values = dict.fromkeys(res_ids, dict())
- # generate composer values
- composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context)
+ template_values = {}
for res_id in res_ids:
- # remove attachments from template values as they should not be rendered
- template_values[res_id].pop('attachment_ids', None)
- # remove some keys from composer that are readonly
- composer_values[res_id].pop('email_to', None)
- composer_values[res_id].pop('email_cc', None)
- composer_values[res_id].pop('partner_to', None)
+ if template_values.get(res_id):
+ # recipients are managed by the template
+ composer_values[res_id].pop('partner_ids')
+ composer_values[res_id].pop('email_to')
+ composer_values[res_id].pop('email_cc')
+ # remove attachments from template values as they should not be rendered
+ template_values[res_id].pop('attachment_ids', None)
+ else:
+ template_values[res_id] = dict()
# update template values by composer values
template_values[res_id].update(composer_values[res_id])
return template_values
diff --git a/addons/email_template/wizard/mail_compose_message_view.xml b/addons/email_template/wizard/mail_compose_message_view.xml
index 44145114fff..2f755a625d6 100644
--- a/addons/email_template/wizard/mail_compose_message_view.xml
+++ b/addons/email_template/wizard/mail_compose_message_view.xml
@@ -7,22 +7,6 @@
mail.compose.message
-
-
-
-
-
-
-
-
-
-
-
-
-
Use template
diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml
index 428cc357047..0437dee031c 100644
--- a/addons/gamification/data/goal_base.xml
+++ b/addons/gamification/data/goal_base.xml
@@ -2,7 +2,7 @@
-
+
@@ -164,7 +164,7 @@
once
personal
never
-
+ [('groups_id', 'in', ref('base.group_user'))]
inprogress
other
@@ -174,7 +174,7 @@
once
personal
never
-
+ [('groups_id', 'in', ref('base.user_root'))]
inprogress
other
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py
index 3ab12ff66e7..295340a309b 100644
--- a/addons/gamification/models/challenge.py
+++ b/addons/gamification/models/challenge.py
@@ -22,11 +22,13 @@
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
+from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
from datetime import date, datetime, timedelta
import calendar
import logging
+import functools
_logger = logging.getLogger(__name__)
# display top 3 in ranking, could be db variable
@@ -115,6 +117,12 @@ class gamification_challenge(osv.Model):
except ValueError:
return False
+ def _get_challenger_users(self, cr, uid, domain, context=None):
+ ref = functools.partial(self.pool['ir.model.data'].xmlid_to_res_id, cr, uid)
+ user_domain = eval(domain, {'ref': ref})
+ return self.pool['res.users'].search(cr, uid, user_domain, context=context)
+
+
_order = 'end_date, start_date, name, id'
_columns = {
'name': fields.char('Challenge Name', required=True, translate=True),
@@ -131,9 +139,7 @@ class gamification_challenge(osv.Model):
'user_ids': fields.many2many('res.users', 'user_ids',
string='Users',
help="List of users participating to the challenge"),
- 'autojoin_group_id': fields.many2one('res.groups',
- string='Auto-subscription Group',
- help='Group of users whose members will be automatically added to user_ids once the challenge is started'),
+ 'user_domain': fields.char('User domain', help="Alternative to a list of users"),
'period': fields.selection([
('once', 'Non recurring'),
@@ -213,12 +219,12 @@ class gamification_challenge(osv.Model):
"""Overwrite the create method to add the user of groups"""
# add users when change the group auto-subscription
- if vals.get('autojoin_group_id'):
- new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
+ if vals.get('user_domain'):
+ user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context)
if not vals.get('user_ids'):
vals['user_ids'] = []
- vals['user_ids'] += [(4, user.id) for user in new_group.users]
+ vals['user_ids'] += [(4, user_id) for user_id in user_ids]
create_res = super(gamification_challenge, self).create(cr, uid, vals, context=context)
@@ -234,23 +240,12 @@ class gamification_challenge(osv.Model):
if isinstance(ids, (int,long)):
ids = [ids]
- # add users when change the group auto-subscription
- if vals.get('autojoin_group_id'):
- new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
-
- if not vals.get('user_ids'):
- vals['user_ids'] = []
- vals['user_ids'] += [(4, user.id) for user in new_group.users]
-
if vals.get('state') == 'inprogress':
- # starting a challenge
- if not vals.get('autojoin_group_id'):
- # starting challenge, add users in autojoin group
- if not vals.get('user_ids'):
- vals['user_ids'] = []
- for challenge in self.browse(cr, uid, ids, context=context):
- if challenge.autojoin_group_id:
- vals['user_ids'] += [(4, user.id) for user in challenge.autojoin_group_id.users]
+ for challenge in self.browse(cr, uid, ids, context=context):
+ user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context)
+ write_op = [(4, user_id) for user_id in user_ids]
+ self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context)
+ self.message_subscribe_users(cr, uid, [challenge.id], user_ids, context=context)
self.generate_goals_from_challenge(cr, uid, ids, context=context)
@@ -264,11 +259,6 @@ class gamification_challenge(osv.Model):
write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
- # subscribe new users to the challenge
- if vals.get('user_ids'):
- # done with browse after super if changes in groups
- for challenge in self.browse(cr, uid, ids, context=context):
- self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
return write_res
@@ -325,9 +315,16 @@ class gamification_challenge(osv.Model):
goal_obj.update(cr, uid, goal_ids, context=context)
for challenge in self.browse(cr, uid, ids, context=context):
- if challenge.autojoin_group_id:
- # check in case of new users in challenge, this happens if manager removed users in challenge manually
- self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context)
+ # in case of new users matching the domain
+ old_user_ids = [user.id for user in challenge.user_ids]
+ new_user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context)
+ to_remove_ids = list(set(old_user_ids) - set(new_user_ids))
+ to_add_ids = list(set(new_user_ids) - set(old_user_ids))
+
+ write_op = [(3, user_id) for user_id in to_remove_ids]
+ write_op += [(4, user_id) for user_id in to_add_ids]
+ self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context)
+
self.generate_goals_from_challenge(cr, uid, [challenge.id], context=context)
# goals closed but still opened at the last report date
diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py
index 6ab4259cca2..af9a763236d 100644
--- a/addons/gamification/models/goal.py
+++ b/addons/gamification/models/goal.py
@@ -287,30 +287,33 @@ class gamification_goal(osv.Model):
field_date_name = definition.field_date_id and definition.field_date_id.name or False
if definition.computation_mode == 'count' and definition.batch_mode:
-
+ # batch mode, trying to do as much as possible in one request
general_domain = safe_eval(definition.domain)
- # goal_distinct_values = {goal.id: safe_eval(definition.batch_user_expression, {'user': goal.user_id}) for goal in goals}
field_name = definition.batch_distinctive_field.name
- # general_domain.append((field_name, 'in', list(set(goal_distinct_values.keys()))))
subqueries = {}
for goal in goals:
start_date = field_date_name and goal.start_date or False
end_date = field_date_name and goal.end_date or False
subqueries.setdefault((start_date, end_date), {}).update({goal.id:safe_eval(definition.batch_user_expression, {'user': goal.user_id})})
+ # the global query should be split by time periods (especially for recurrent goals)
for (start_date, end_date), query_goals in subqueries.items():
subquery_domain = list(general_domain)
subquery_domain.append((field_name, 'in', list(set(query_goals.values()))))
if start_date:
subquery_domain.append((field_date_name, '>=', start_date))
if end_date:
- subquery_domain.append((field_date_name, '>=', end_date))
-
- user_values = obj.read_group(cr, uid, subquery_domain, fields=[field_name], groupby=[field_name], context=context)
+ subquery_domain.append((field_date_name, '<=', end_date))
+ if field_name == 'id':
+ # grouping on id does not work and is similar to search anyway
+ user_ids = obj.search(cr, uid, subquery_domain, context=context)
+ user_values = [{'id': user_id, 'id_count': 1} for user_id in user_ids]
+ else:
+ user_values = obj.read_group(cr, uid, subquery_domain, fields=[field_name], groupby=[field_name], context=context)
+ # user_values has format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...]
for goal in [g for g in goals if g.id in query_goals.keys()]:
for user_value in user_values:
- # return format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...]
queried_value = field_name in user_value and user_value[field_name] or False
if isinstance(queried_value, tuple) and len(queried_value) == 2 and isinstance(queried_value[0], (int, long)):
queried_value = queried_value[0]
diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py
index 85cc18636f0..330311ba05e 100644
--- a/addons/gamification/models/res_users.py
+++ b/addons/gamification/models/res_users.py
@@ -31,43 +31,12 @@ class res_users_gamification_group(osv.Model):
_name = 'res.users'
_inherit = ['res.users']
- def write(self, cr, uid, ids, vals, context=None):
- """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge"""
- write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context)
- if vals.get('groups_id'):
- # 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 vals['groups_id'] if command[0] == 4]
- user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
+ def get_serialised_gamification_summary(self, cr, uid, excluded_categories=None, context=None):
+ return self._serialised_goals_summary(cr, uid, user_id=uid, excluded_categories=excluded_categories, context=context)
- challenge_obj = self.pool.get('gamification.challenge')
- challenge_ids = challenge_obj.search(cr, SUPERUSER_ID, [('autojoin_group_id', 'in', user_group_ids)], context=context)
- if challenge_ids:
- challenge_obj.write(cr, SUPERUSER_ID, challenge_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context)
- challenge_obj.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
- return write_res
-
- def create(self, cr, uid, vals, context=None):
- """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge"""
- write_res = super(res_users_gamification_group, self).create(cr, uid, vals, context=context)
- if vals.get('groups_id'):
- # 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 vals['groups_id'] if command[0] == 4]
- user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
-
- challenge_obj = self.pool.get('gamification.challenge')
- challenge_ids = challenge_obj.search(cr, SUPERUSER_ID, [('autojoin_group_id', 'in', user_group_ids)], context=context)
- if challenge_ids:
- challenge_obj.write(cr, SUPERUSER_ID, challenge_ids, {'user_ids': [(4, write_res)]}, context=context)
- challenge_obj.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
- return write_res
-
- # def get_goals_todo_info(self, cr, uid, context=None):
-
- def get_serialised_gamification_summary(self, cr, uid, context=None):
- return self._serialised_goals_summary(cr, uid, user_id=uid, context=context)
-
- def _serialised_goals_summary(self, cr, uid, user_id, context=None):
+ def _serialised_goals_summary(self, cr, uid, user_id, excluded_categories=None, context=None):
"""Return a serialised list of goals assigned to the user, grouped by challenge
+ :excluded_categories: list of challenge categories to exclude in search
[
{
@@ -81,9 +50,11 @@ class res_users_gamification_group(osv.Model):
"""
all_goals_info = []
challenge_obj = self.pool.get('gamification.challenge')
-
+ domain = [('user_ids', 'in', uid), ('state', '=', 'inprogress')]
+ if excluded_categories and isinstance(excluded_categories, list):
+ domain.append(('category', 'not in', excluded_categories))
user = self.browse(cr, uid, uid, context=context)
- challenge_ids = challenge_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
+ challenge_ids = challenge_obj.search(cr, uid, domain, context=context)
for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context):
# serialize goals info to be able to use it in javascript
lines = challenge_obj._get_serialized_challenge_lines(cr, uid, challenge, user_id, restrict_top=MAX_VISIBILITY_RANKING, context=context)
@@ -111,28 +82,3 @@ class res_users_gamification_group(osv.Model):
}
challenge_info.append(values)
return challenge_info
-
-
-class res_groups_gamification_group(osv.Model):
- """ Update of res.groups class
- - if adding users from a group, check gamification.challenge linked to
- this group, and the user. This is done by overriding the write method.
- """
- _name = 'res.groups'
- _inherit = 'res.groups'
-
- # No need to overwrite create as very unlikely to be the value in the autojoin_group_id field
- def write(self, cr, uid, ids, vals, context=None):
- """Overwrite to autosubscribe users if add users to a group marked as autojoin, these will be added to the challenge"""
- write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context)
- if vals.get('users'):
- # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
- user_ids = [command[1] for command in vals['users'] if command[0] == 4]
- user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]]
-
- challenge_obj = self.pool.get('gamification.challenge')
- challenge_ids = challenge_obj.search(cr, SUPERUSER_ID, [('autojoin_group_id', 'in', ids)], context=context)
- if challenge_ids:
- challenge_obj.write(cr, SUPERUSER_ID, challenge_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context)
- challenge_obj.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
- return write_res
diff --git a/addons/gamification/tests/test_challenge.py b/addons/gamification/tests/test_challenge.py
index 6b93da727ac..cdbca962e0f 100644
--- a/addons/gamification/tests/test_challenge.py
+++ b/addons/gamification/tests/test_challenge.py
@@ -57,6 +57,7 @@ class test_challenge(common.TransactionCase):
'groups_id': [(6, 0, [self.group_user_id])]
}, {'no_reset_password': True})
+ self.challenge_obj._update_all(cr, uid, [self.challenge_base_id], context=context)
challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
self.assertGreaterEqual(len(challenge.user_ids), len(user_ids)+1, "These are not droids you are looking for")
diff --git a/addons/gamification/views/challenge.xml b/addons/gamification/views/challenge.xml
index b37ef0eb0d8..75dcd4a7959 100644
--- a/addons/gamification/views/challenge.xml
+++ b/addons/gamification/views/challenge.xml
@@ -45,9 +45,9 @@
-
+
-
+
@@ -81,12 +81,12 @@
-
+
-
+
Badges are granted when a challenge is finished. This is either at the end of a running period (eg: end of the month for a monthly challenge), at the end date of a challenge (if no periodicity is set) or when the challenge is manually closed.
@@ -94,7 +94,6 @@
-
diff --git a/addons/gamification_sale_crm/sale_crm_goals.xml b/addons/gamification_sale_crm/sale_crm_goals.xml
index 4c1f8e46149..fbc1c952bb8 100644
--- a/addons/gamification_sale_crm/sale_crm_goals.xml
+++ b/addons/gamification_sale_crm/sale_crm_goals.xml
@@ -130,7 +130,7 @@
Monthly Sales Targets
monthly
ranking
-
+ [('groups_id', 'in', ref('base.group_sale_salesman'))]
weekly
@@ -138,7 +138,7 @@
Lead Acquisition
monthly
ranking
-
+ [('groups_id', 'in', ref('base.group_sale_salesman'))]
weekly
diff --git a/addons/hr_attendance/__openerp__.py b/addons/hr_attendance/__openerp__.py
index 631880ec73b..abdc1ad8cc1 100644
--- a/addons/hr_attendance/__openerp__.py
+++ b/addons/hr_attendance/__openerp__.py
@@ -33,7 +33,7 @@ actions(Sign in/Sign out) performed by them.
""",
'author': 'OpenERP SA',
'images': ['images/hr_attendances.jpeg'],
- 'depends': ['hr'],
+ 'depends': ['hr', 'report'],
'data': [
'security/ir_rule.xml',
'security/ir.model.access.csv',
@@ -43,6 +43,7 @@ actions(Sign in/Sign out) performed by them.
'wizard/hr_attendance_byweek_view.xml',
'wizard/hr_attendance_error_view.xml',
'res_config_view.xml',
+ 'views/report_attendanceerrors.xml',
],
'demo': ['hr_attendance_demo.xml'],
'test': [
@@ -51,10 +52,10 @@ actions(Sign in/Sign out) performed by them.
],
'installable': True,
'auto_install': False,
-
#web
"js": ["static/src/js/attendance.js"],
- 'qweb' : ["static/src/xml/attendance.xml"],
- 'css' : ["static/src/css/slider.css"],
+ 'qweb': ["static/src/xml/attendance.xml"],
+ 'css': ["static/src/css/slider.css"],
}
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_attendance/hr_attendance_report.xml b/addons/hr_attendance/hr_attendance_report.xml
index d3e9448ead3..f98eb6c053b 100644
--- a/addons/hr_attendance/hr_attendance_report.xml
+++ b/addons/hr_attendance/hr_attendance_report.xml
@@ -1,8 +1,13 @@
-
-
-
+
diff --git a/addons/hr_attendance/report/attendance_errors.py b/addons/hr_attendance/report/attendance_errors.py
index 811f4d8d906..e37a329ac60 100644
--- a/addons/hr_attendance/report/attendance_errors.py
+++ b/addons/hr_attendance/report/attendance_errors.py
@@ -21,9 +21,10 @@
import datetime
import time
-
+from openerp.osv import osv
from openerp.report import report_sxw
+
class attendance_print(report_sxw.rml_parse):
def __init__(self, cr, uid, name, context):
@@ -39,7 +40,6 @@ class attendance_print(report_sxw.rml_parse):
emp_obj_list = self.pool.get('hr.employee').browse(self.cr, self.uid, emp_ids)
return emp_obj_list
-
def _lst(self, employee_id, dt_from, dt_to, max, *args):
self.cr.execute("select name as date, create_date, action, create_date-name as delay from hr_attendance where employee_id=%s and to_char(name,'YYYY-mm-dd')<=%s and to_char(name,'YYYY-mm-dd')>=%s and action IN (%s,%s) order by name", (employee_id, dt_to, dt_from, 'sign_in', 'sign_out'))
res = self.cr.dictfetchall()
@@ -75,7 +75,11 @@ class attendance_print(report_sxw.rml_parse):
}
return [result_dict]
-report_sxw.report_sxw('report.hr.attendance.error', 'hr.employee', 'addons/hr_attendance/report/attendance_errors.rml', parser=attendance_print, header='internal')
+
+class report_hr_attendanceerrors(osv.AbstractModel):
+ _name = 'report.hr_attendance.report_attendanceerrors'
+ _inherit = 'report.abstract_report'
+ _template = 'hr_attendance.report_attendanceerrors'
+ _wrapped_report_class = attendance_print
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/addons/hr_attendance/report/attendance_errors.rml b/addons/hr_attendance/report/attendance_errors.rml
deleted file mode 100644
index 6806c02c422..00000000000
--- a/addons/hr_attendance/report/attendance_errors.rml
+++ /dev/null
@@ -1,137 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- [[ repeatIn(get_employees(data['form']['emp_ids']),'employee') ]]
- Attendance Errors
- [[ employee.name ]]
-
-
-
- Operation
-
-
- Date Signed
-
-
- Date Recorded
-
-
- Delay
-
-
- Min Delay
-
-
-
-
- [[ repeatIn(lst(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay']), 'att') ]]
-
-
-
- [[ att['action'] ]]
-
-
- [[ formatLang(att['date'],date_time=True) ]]
-
-
- [[ formatLang(att['create_date'],date_time=True) ]]
-
-
- [[ att['delay'] ]]
-
-
- [[ att['delay2'] ]]
-
-
-
-
-
-
-
- Total period:[[ repeatIn(total(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay']),'total') ]]
-
-
- [[ total['total'] ]]
-
-
- [[ total['total2'] ]]
-
-
-
-
-
-
- (*) A positive delay means that the employee worked less than recorded.
- (*) A negative delay means that the employee worked more than encoded.
-
-
\ No newline at end of file
diff --git a/addons/hr_attendance/views/report_attendanceerrors.xml b/addons/hr_attendance/views/report_attendanceerrors.xml
new file mode 100644
index 00000000000..8ee00a60534
--- /dev/null
+++ b/addons/hr_attendance/views/report_attendanceerrors.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
Attendance Errors:
+
+
+
+
+ Operation
+ Date Signed
+ Date Recorded
+ Delay
+ Min Delay
+
+
+
+
+
+
+
+
+
+
+
+ Total period
+
+
+
+
+
+
(*) A positive delay means that the employee worked less than recorded.
+(*) A negative delay means that the employee worked more than encoded.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/hr_attendance/wizard/hr_attendance_error.py b/addons/hr_attendance/wizard/hr_attendance_error.py
index 82d75149428..58a165baff3 100644
--- a/addons/hr_attendance/wizard/hr_attendance_error.py
+++ b/addons/hr_attendance/wizard/hr_attendance_error.py
@@ -23,6 +23,7 @@ import time
from openerp.osv import fields, osv
from openerp.tools.translate import _
+
class hr_attendance_error(osv.osv_memory):
_name = 'hr.attendance.error'
@@ -58,11 +59,8 @@ class hr_attendance_error(osv.osv_memory):
'model': 'hr.employee',
'form': data_error
}
- return {
- 'type': 'ir.actions.report.xml',
- 'report_name': 'hr.attendance.error',
- 'datas': datas,
- }
-
+ return self.pool['report'].get_action(
+ cr, uid, [], 'hr_attendance.report_attendanceerrors', data=datas, context=context
+ )
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_expense/__openerp__.py b/addons/hr_expense/__openerp__.py
index f149bc0c7c6..873da978165 100644
--- a/addons/hr_expense/__openerp__.py
+++ b/addons/hr_expense/__openerp__.py
@@ -46,7 +46,7 @@ This module also uses analytic accounting and is compatible with the invoice on
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'images': ['images/hr_expenses_analysis.jpeg', 'images/hr_expenses.jpeg'],
- 'depends': ['hr', 'account_accountant'],
+ 'depends': ['hr', 'account_accountant', 'report'],
'data': [
'security/ir.model.access.csv',
'hr_expense_data.xml',
@@ -59,6 +59,7 @@ This module also uses analytic accounting and is compatible with the invoice on
'report/hr_expense_report_view.xml',
'board_hr_expense_view.xml',
'hr_expense_installer_view.xml',
+ 'views/report_expense.xml',
],
'demo': ['hr_expense_demo.xml'],
'test': [
@@ -69,4 +70,5 @@ This module also uses analytic accounting and is compatible with the invoice on
'auto_install': False,
'application': True,
}
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_expense/hr_expense_report.xml b/addons/hr_expense/hr_expense_report.xml
index 5defbfec330..ab3200e5a54 100644
--- a/addons/hr_expense/hr_expense_report.xml
+++ b/addons/hr_expense/hr_expense_report.xml
@@ -1,8 +1,13 @@
-
-
-
+
diff --git a/addons/hr_expense/report/__init__.py b/addons/hr_expense/report/__init__.py
index ad0c48a0819..9ec287fb9c7 100644
--- a/addons/hr_expense/report/__init__.py
+++ b/addons/hr_expense/report/__init__.py
@@ -19,8 +19,6 @@
#
##############################################################################
-import expense
import hr_expense_report
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/addons/hr_expense/report/expense.rml b/addons/hr_expense/report/expense.rml
deleted file mode 100644
index 3e07516c01b..00000000000
--- a/addons/hr_expense/report/expense.rml
+++ /dev/null
@@ -1,302 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Date
-
-
- Name
-
-
- Ref.
-
-
- Unit Price
-
-
- Qty
-
-
- Price
-
-
-
-
- [[ repeatIn(objects,'o') ]]
-
-
-
- HR Expenses
-
- [[ o.name or '' ]]
-
-
-
-
-
-
-
- Employee
-
-
- Date
-
-
- Description
-
-
- Validated By
-
-
-
-
-
-
- [[ o.employee_id.name ]]
-
-
- [[ formatLang(o.date,date=True) ]]
-
-
- [[ o.name ]]
-
-
- [[ o.user_valid.name ]]
-
-
-
-
-
-
-
-
-
- Date
-
-
- Name
-
-
- Ref.
-
-
- Unit Price
-
-
- Qty
-
-
- Price
-
-
-
-
-
-
-
- [[ repeatIn(o.line_ids,'line') ]]
-
-
-
- [[ formatLang(line.date_value,date=True) ]]
-
-
- [[ line.name or '' ]] [[ line.description or '' ]]
-
-
- [[ line.ref or '' ]]
-
-
- [[ formatLang(line.unit_amount) ]]
-
-
- [[ formatLang(line.unit_quantity) ]]
-
-
- [[ formatLang(line.total_amount, currency_obj=o.currency_id) ]]
-
-
-
-
-
-
-
-
-
- [[ line.analytic_account and line.analytic_account.complete_name or removeParentNode('tr') ]]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Total:
-
-
- [[ formatLang(o.amount, currency_obj=o.currency_id) ]]
-
-
-
-
-
-
- [[ o.note or '' ]]
-
-
-
- Certified honest and conform,
- (Date and signature)
-
-
-
-
-
-
- This document must be dated and signed for reimbursement
-
-
-
diff --git a/addons/hr_expense/test/expense_process.yml b/addons/hr_expense/test/expense_process.yml
index e9339e24a95..b3e19714c74 100644
--- a/addons/hr_expense/test/expense_process.yml
+++ b/addons/hr_expense/test/expense_process.yml
@@ -32,11 +32,3 @@
!python {model: hr.expense.expense}: |
duplicate_id = self.copy(cr, uid, ref('sep_expenses'), context=context)
self.expense_canceled(cr, uid, [duplicate_id])
--
- I print a report of the expenses.
--
- !python {model: hr.expense.expense}: |
- data, format = self.print_report(cr, uid, [ref('hr_expense.sep_expenses')], 'hr.expense', {}, {})
- if openerp.tools.config['test_report_directory']:
- import os
- file(os.path.join(openerp.tools.config['test_report_directory'], 'hr_expense-report.'+format), 'wb+').write(data)
diff --git a/addons/hr_expense/views/report_expense.xml b/addons/hr_expense/views/report_expense.xml
new file mode 100644
index 00000000000..5bb2e1dd8e2
--- /dev/null
+++ b/addons/hr_expense/views/report_expense.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
HR Expenses
+
+
+
+
+
+
+ Date
+ Name
+ Ref.
+ Unit Price
+ Qty
+ Price
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Certified honest and conform, (Date and signature).
+
This document must be dated and signed for reimbursement.
+
+
+
+
+
+
+
diff --git a/addons/hr_payroll/__openerp__.py b/addons/hr_payroll/__openerp__.py
index dfa87622264..826845834c8 100644
--- a/addons/hr_payroll/__openerp__.py
+++ b/addons/hr_payroll/__openerp__.py
@@ -19,6 +19,7 @@
# along with this program. If not, see .
#
##############################################################################
+
{
'name': 'Payroll',
'version': '1.0',
@@ -37,14 +38,20 @@ Generic Payroll system.
* Monthly Payroll Register
* Integrated with Holiday Management
""",
- 'author':'OpenERP SA',
- 'website':'http://www.openerp.com',
- 'images': ['images/hr_company_contributions.jpeg','images/hr_salary_heads.jpeg','images/hr_salary_structure.jpeg','images/hr_employee_payslip.jpeg'],
+ 'author': 'OpenERP SA',
+ 'website': 'http://www.openerp.com',
+ 'images': [
+ 'images/hr_company_contributions.jpeg',
+ 'images/hr_salary_heads.jpeg',
+ 'images/hr_salary_structure.jpeg',
+ 'images/hr_employee_payslip.jpeg'
+ ],
'depends': [
'hr',
'hr_contract',
'hr_holidays',
'decimal_precision',
+ 'report',
],
'data': [
'security/hr_security.xml',
@@ -57,12 +64,12 @@ Generic Payroll system.
'security/ir.model.access.csv',
'wizard/hr_payroll_contribution_register_report.xml',
'res_config_view.xml',
+ 'views/report_contributionregister.xml',
+ 'views/report_payslip.xml',
+ 'views/report_payslipdetails.xml',
],
'test': [
'test/payslip.yml',
-# 'test/payment_advice.yml',
-# 'test/payroll_register.yml',
- # 'test/hr_payroll_report.yml',
],
'demo': ['hr_payroll_demo.xml'],
'installable': True,
diff --git a/addons/hr_payroll/hr_payroll_report.xml b/addons/hr_payroll/hr_payroll_report.xml
index a1b3587313a..86d5b3b8a66 100644
--- a/addons/hr_payroll/hr_payroll_report.xml
+++ b/addons/hr_payroll/hr_payroll_report.xml
@@ -1,31 +1,30 @@
-
+
+
-
-
-
-
-
+ string="PaySlip Details"
+ report_type="qweb-pdf"
+ name="hr_payroll.report_payslipdetails"
+ file="hr_payroll.report_payslipdetails"
+ />
diff --git a/addons/hr_payroll/report/report_contribution_register.py b/addons/hr_payroll/report/report_contribution_register.py
index 72500a9c6c9..27cc0eb6458 100644
--- a/addons/hr_payroll/report/report_contribution_register.py
+++ b/addons/hr_payroll/report/report_contribution_register.py
@@ -24,9 +24,10 @@
import time
from datetime import datetime
from dateutil import relativedelta
-
+from openerp.osv import osv
from openerp.report import report_sxw
+
class contribution_register_report(report_sxw.rml_parse):
def __init__(self, cr, uid, name, context):
super(contribution_register_report, self).__init__(cr, uid, name, context)
@@ -44,7 +45,6 @@ class contribution_register_report(report_sxw.rml_parse):
return self.regi_total
def _get_payslip_lines(self, obj):
- payslip_obj = self.pool.get('hr.payslip')
payslip_line = self.pool.get('hr.payslip.line')
payslip_lines = []
res = []
@@ -69,6 +69,11 @@ class contribution_register_report(report_sxw.rml_parse):
self.regi_total += line.total
return res
-report_sxw.report_sxw('report.contribution.register.lines', 'hr.contribution.register', 'hr_payroll/report/report_contribution_register.rml', parser=contribution_register_report)
+
+class wrapped_report_contribution_register(osv.AbstractModel):
+ _name = 'report.hr_payroll.report_contributionregister'
+ _inherit = 'report.abstract_report'
+ _template = 'hr_payroll.report_contributionregister'
+ _wrapped_report_class = contribution_register_report
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_payroll/report/report_contribution_register.rml b/addons/hr_payroll/report/report_contribution_register.rml
deleted file mode 100644
index 8a5897c6091..00000000000
--- a/addons/hr_payroll/report/report_contribution_register.rml
+++ /dev/null
@@ -1,234 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- [[repeatIn(objects,'o')]]
-
-
-
- PaySlip Lines by Contribution Register
-
-
-
-
-
-
-
-
-
-
-
-
-
- Register Name
-
-
- Date From
-
-
- Date To
-
-
-
-
-
-
- [[ o.name or '']]
-
-
- [[ data['form']['date_from'] or '']]
-
-
- [[ data['form']['date_to'] or '' ]]
-
-
-
-
-
-
-
-
-
-
-
-
-
- PaySlip Name
-
-
- Code
-
-
- Name
-
-
- Quantity/Rate
-
-
- Amount
-
-
- Total
-
-
-
-
-
-
-
- [[repeatIn(get_payslip_lines(o),'r') ]]
-
-
-
- [[ r.get('payslip_name', False) ]][[ r.get('payslip_name', False) and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]]
-
-
- [[ r['code'] ]]
-
-
- [[ r['name'] ]]
-
-
- [[ formatLang(r['quantity']) ]]
-
-
- [[ formatLang(r['amount']) ]]
-
-
- [[ formatLang(r['total'], currency_obj = o.company_id and o.company_id.currency_id)]]
-
-
-
-
-
-
-
-
-
-
-
-
- Total:
-
-
- [[ formatLang(sum_total(), currency_obj = o.company_id and o.company_id.currency_id)]]
-
-
-
-
-
-
-
-
-
diff --git a/addons/hr_payroll/report/report_payslip.py b/addons/hr_payroll/report/report_payslip.py
index 3e1dc2b1966..0c78ceec49a 100644
--- a/addons/hr_payroll/report/report_payslip.py
+++ b/addons/hr_payroll/report/report_payslip.py
@@ -21,8 +21,9 @@
#
##############################################################################
+from openerp.osv import osv
from openerp.report import report_sxw
-from openerp.tools import amount_to_text_en
+
class payslip_report(report_sxw.rml_parse):
@@ -37,12 +38,17 @@ class payslip_report(report_sxw.rml_parse):
res = []
ids = []
for id in range(len(obj)):
- if obj[id].appears_on_payslip == True:
+ if obj[id].appears_on_payslip is True:
ids.append(obj[id].id)
if ids:
res = payslip_line.browse(self.cr, self.uid, ids)
return res
-report_sxw.report_sxw('report.payslip', 'hr.payslip', 'hr_payroll/report/report_payslip.rml', parser=payslip_report)
+
+class wrapped_report_payslip(osv.AbstractModel):
+ _name = 'report.hr_payroll.report_payslip'
+ _inherit = 'report.abstract_report'
+ _template = 'hr_payroll.report_payslip'
+ _wrapped_report_class = payslip_report
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_payroll/report/report_payslip.rml b/addons/hr_payroll/report/report_payslip.rml
deleted file mode 100644
index 990419ee5f1..00000000000
--- a/addons/hr_payroll/report/report_payslip.rml
+++ /dev/null
@@ -1,340 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- [[repeatIn(objects,'o')]]
-
-
-
- Pay Slip
-
-
-
-
- [[o.credit_note==False and removeParentNode('para')]]
- Credit
-
- Note
-
- ([[o.name or removeParentNode('para')]])
-
-
-
- Name
-
-
- [[o.employee_id.name]]
-
-
- Designation
-
-
- [[ o.employee_id.job_id.name or '' ]]
-
-
-
-
-
-
-
- Address
-
-
-
- [[o.employee_id.address_home_id and o.employee_id.address_home_id.name or '' ]]
- [[o.employee_id.address_home_id and display_address(o.employee_id.address_home_id)]]
-
-
-
-
-
-
- Email
-
-
- [[ o.employee_id.work_email or '' ]]
-
-
-
- Identification No
-
-
-
- [[ o.employee_id.identification_id or '' ]]
-
-
-
-
-
-
- Reference
-
-
- [[ o.number or '' ]]
-
-
- Bank Account
-
-
- [[ o.employee_id.otherid or '' ]]
-
-
-
-
-
-
- Date From
-
-
- [[ o.date_from or '']]
-
-
-
- Date To
-
-
-
- [[ o.date_to or '' ]]
-
-
-
-
-
-
-
-
-
-
- Code
-
-
- Name
-
-
- Quantity/Rate
-
-
- Amount
-
-
- Total
-
-
-
-
- [[repeatIn(get_payslip_lines(o.line_ids),'p') ]]
-
-
-
- [[ p.code ]]
-
-
- [[ p.name ]]
-
-
- [[ formatLang(p.quantity) ]]
-
-
- [[ formatLang(p.amount) ]]
-
-
- [[ formatLang(p.total, currency_obj = o.company_id and o.company_id.currency_id)]]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Authorized Signature
-
-
-
-
-
-
-
-
-
diff --git a/addons/hr_payroll/report/report_payslip_details.py b/addons/hr_payroll/report/report_payslip_details.py
index 6b561b8d688..45d35373455 100644
--- a/addons/hr_payroll/report/report_payslip_details.py
+++ b/addons/hr_payroll/report/report_payslip_details.py
@@ -1,5 +1,4 @@
#-*- coding:utf-8 -*-
-
##############################################################################
#
# OpenERP, Open Source Management Solution
@@ -21,8 +20,9 @@
#
##############################################################################
+from openerp.osv import osv
from openerp.report import report_sxw
-from openerp.tools import amount_to_text_en
+
class payslip_details_report(report_sxw.rml_parse):
@@ -113,6 +113,11 @@ class payslip_details_report(report_sxw.rml_parse):
})
return res
-report_sxw.report_sxw('report.paylip.details', 'hr.payslip', 'hr_payroll/report/report_payslip_details.rml', parser=payslip_details_report)
+
+class wrapped_report_payslipdetails(osv.AbstractModel):
+ _name = 'report.hr_payroll.report_payslipdetails'
+ _inherit = 'report.abstract_report'
+ _template = 'hr_payroll.report_payslipdetails'
+ _wrapped_report_class = payslip_details_report
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_payroll/report/report_payslip_details.rml b/addons/hr_payroll/report/report_payslip_details.rml
deleted file mode 100644
index 4ace0ad2f82..00000000000
--- a/addons/hr_payroll/report/report_payslip_details.rml
+++ /dev/null
@@ -1,426 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- [[repeatIn(objects,'o')]]
-
-
-
- Pay Slip Details
-
-
-
-
- [[o.credit_note==False and removeParentNode('para')]]
- Credit
-
- Note
-
- ([[o.name or removeParentNode('para')]])
-
-
-
- Name
-
-
- [[o.employee_id.name]]
-
-
- Designation
-
-
- [[ o.employee_id.job_id.name or '' ]]
-
-
-
-
-
-
-
- Address
-
-
-
- [[o.employee_id.address_home_id and o.employee_id.address_home_id.name or '' ]]
- [[o.employee_id.address_home_id and display_address(o.employee_id.address_home_id)]]
-
-
-
-
-
-
- Email
-
-
- [[ o.employee_id.work_email or '' ]]
-
-
-
- Identification No
-
-
-
- [[ o.employee_id.identification_id or '' ]]
-
-
-
-
-
-
- Reference
-
-
- [[ o.number or '' ]]
-
-
- Bank Account
-
-
- [[ o.employee_id.otherid or '' ]]
-
-
-
-
-
-
- Date From
-
-
- [[ o.date_from or '']]
-
-
-
- Date To
-
-
-
- [[ o.date_to or '' ]]
-
-
-
-
-
-
-
-
-
-
-
-
-
- Details by Salary Rule Category:
-
-
-
-
-
-
- Code
-
-
- Salary Rule Category
-
-
- Total
-
-
-
-
-
-
-
- [[repeatIn(get_details_by_rule_category(o.details_by_salary_rule_category),'h') ]]
-
-
-
-
- [[ h['code'] ]]
- [[ h['level']!=0 and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]]
-
-
-
- [[ '..'*h['level'] ]] [[ h['rule_category'] ]][[ h['level']!=0 and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font') ]]
-
-
- [[ formatLang(h['total'], currency_obj = o.company_id and o.company_id.currency_id)]] [[ h['level']==0 and ( setTag('para','para',{'style':'terp_default_10'})) or removeParentNode('font') ]]
-
-
-
-
-
-
-
-
-
-
- Payslip Lines by Contribution Register:
-
-
-
-
-
-
- Register Name
-
-
- Code
-
-
- Name
-
-
- Quantity/Rate
-
-
- Amount
-
-
- Total
-
-
-
-
- [[repeatIn(get_lines_by_contribution_register(o.details_by_salary_rule_category),'r') ]]
-
-
-
- [[ r.get('register_name', False) ]][[ h.get('register_name', False) and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]]
-
-
- [[ r['code'] ]]
-
-
- [[ r['name'] ]]
-
-
- [[ formatLang(r['quantity']) ]]
-
-
- [[ formatLang(r['amount']) ]]
-
-
- [[ formatLang(r['total'], currency_obj = o.company_id and o.company_id.currency_id)]][[ r.get('register_name', False) and ( setTag('para','para',{'style':'terp_default_10'})) or removeParentNode('font')]]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Authorized Signature
-
-
-
-
-
-
-
-
-
diff --git a/addons/hr_payroll/test/hr_payroll_report.yml b/addons/hr_payroll/test/hr_payroll_report.yml
deleted file mode 100644
index ab4b0fa97e1..00000000000
--- a/addons/hr_payroll/test/hr_payroll_report.yml
+++ /dev/null
@@ -1,12 +0,0 @@
--
- In order to test the PDF reports defined on HR Payroll, we will print Employees' Salary Structure
--
- Print HR Payslip
--
- !python {model: hr.payslip}: |
- import os
- import openerp.report
- from openerp import tools
- data, format = openerp.report.render_report(cr, uid, [ref('hr_payroll.hr_payslip_salaryslipofbonamyforjune0')], 'payslip.pdf', {}, {})
- if tools.config['test_report_directory']:
- file(os.path.join(tools.config['test_report_directory'], 'hr_payroll-payslip_report.'+format), 'wb+').write(data)
diff --git a/addons/hr_payroll/test/payslip.yml b/addons/hr_payroll/test/payslip.yml
index f78ad4a374d..b4029146b6d 100644
--- a/addons/hr_payroll/test/payslip.yml
+++ b/addons/hr_payroll/test/payslip.yml
@@ -107,8 +107,29 @@
date_from: '2011-09-30'
date_to: '2011-09-01'
-
- I print the report.
+ I print the payslip report
-
- !python {model: payslip.lines.contribution.register}: |
- self.print_report(cr, uid, [ref('payslip_lines_contribution_register0')], context={'active_ids': [ref('hr_houserent_register')]})
-
+ !python {model: hr.payslip}: |
+ import os
+ import openerp.report
+ from openerp import tools
+ data, format = openerp.report.render_report(cr, uid, [ref('hr_payslip_0')], 'hr_payroll.report_payslip', {}, {})
+ if tools.config['test_report_directory']:
+ file(os.path.join(tools.config['test_report_directory'], 'hr_payroll-payslip.'+format), 'wb+').write(data)
+-
+ I print the payslip details report
+-
+ !python {model: hr.payslip}: |
+ import os
+ import openerp.report
+ from openerp import tools
+ data, format = openerp.report.render_report(cr, uid, [ref('hr_payslip_0')], 'hr_payroll.report_payslipdetails', {}, {})
+ if tools.config['test_report_directory']:
+ file(os.path.join(tools.config['test_report_directory'], 'hr_payroll-payslipdetails.'+format), 'wb+').write(data)
+-
+ I print the contribution register report
+-
+ !python {model: hr.contribution.register}: |
+ ctx={'model': 'hr.contribution.register', 'active_ids': [ref('hr_houserent_register')]}
+ from openerp.tools import test_reports
+ test_reports.try_report_action(cr, uid, 'action_payslip_lines_contribution_register', context=ctx, our_module='hr_payroll')
diff --git a/addons/hr_payroll/views/report_contributionregister.xml b/addons/hr_payroll/views/report_contributionregister.xml
new file mode 100644
index 00000000000..814a6025268
--- /dev/null
+++ b/addons/hr_payroll/views/report_contributionregister.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
PaySlip Lines by Contribution Register
+
+
+
+
+
+
+
+ PaySlip Name
+ Code
+ Name
+ Quantity/Rate
+ Amount
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/hr_payroll/views/report_payslip.xml b/addons/hr_payroll/views/report_payslip.xml
new file mode 100644
index 00000000000..5ac9dfa557e
--- /dev/null
+++ b/addons/hr_payroll/views/report_payslip.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
Pay Slip
+
+
+
+
+ Name
+
+ Designation
+
+
+
+ Address
+
+
+
+
+
+ Email
+
+ Identification No
+
+
+
+ Reference
+
+ Bank Account
+
+
+
+ Date From
+
+ Date To
+
+
+
+
+
+
+
+ Code
+ Name
+ Quantity/rate
+ Amount
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Authorized signature
+
+
+
+
+
+
+
diff --git a/addons/hr_payroll/views/report_payslipdetails.xml b/addons/hr_payroll/views/report_payslipdetails.xml
new file mode 100644
index 00000000000..acd0fbc2610
--- /dev/null
+++ b/addons/hr_payroll/views/report_payslipdetails.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
Pay Slip
+
+
+
+
+ Name
+
+ Designation
+
+
+
+ Address
+
+
+
+
+
+ Email
+
+ Identification No
+
+
+
+ Reference
+
+ Bank Account
+
+
+
+ Date From
+
+ Date To
+
+
+
+
+
Details by Salary Rule Category
+
+
+
+ Code
+ Salary Rule Category
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Payslip Lines by Contribution Register
+
+
+
+ Code
+ Name
+ Quantity/rate
+ Amount
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Authorized signature
+
+
+
+
+
+
+
diff --git a/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py b/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py
index ff078b09a67..20e3c0413aa 100644
--- a/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py
+++ b/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py
@@ -22,9 +22,9 @@
import time
from datetime import datetime
from dateutil import relativedelta
-
from openerp.osv import fields, osv
+
class payslip_lines_contribution_register(osv.osv_memory):
_name = 'payslip.lines.contribution.register'
_description = 'PaySlip Lines by Contribution Registers'
@@ -44,11 +44,8 @@ class payslip_lines_contribution_register(osv.osv_memory):
'model': 'hr.contribution.register',
'form': self.read(cr, uid, ids, [], context=context)[0]
}
- return {
- 'type': 'ir.actions.report.xml',
- 'report_name': 'contribution.register.lines',
- 'datas': datas,
- }
-
+ return self.pool['report'].get_action(
+ cr, uid, [], 'hr_payroll.report_contributionregister', data=datas, context=context
+ )
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py
index ac11ef5e81d..fa51f58d696 100644
--- a/addons/hr_recruitment/hr_recruitment.py
+++ b/addons/hr_recruitment/hr_recruitment.py
@@ -80,6 +80,7 @@ class hr_applicant(osv.Model):
_description = "Applicant"
_order = "id desc"
_inherit = ['mail.thread', 'ir.needaction_mixin']
+
_track = {
'stage_id': {
# this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
@@ -87,6 +88,7 @@ class hr_applicant(osv.Model):
'hr_recruitment.mt_applicant_stage_changed': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence > 1,
},
}
+ _mail_mass_mailing = _('Applicants')
def _get_default_department_id(self, cr, uid, context=None):
""" Gives default department by checking if present in the context """
diff --git a/addons/hr_timesheet_invoice/__openerp__.py b/addons/hr_timesheet_invoice/__openerp__.py
index d15dd3b0e3b..c2857ba4ceb 100644
--- a/addons/hr_timesheet_invoice/__openerp__.py
+++ b/addons/hr_timesheet_invoice/__openerp__.py
@@ -35,7 +35,7 @@ reports.""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'images': ['images/hr_bill_task_work.jpeg','images/hr_type_of_invoicing.jpeg'],
- 'depends': ['account', 'hr_timesheet'],
+ 'depends': ['account', 'hr_timesheet', 'report'],
'data': [
'security/ir.model.access.csv',
'hr_timesheet_invoice_data.xml',
@@ -47,6 +47,7 @@ reports.""",
'wizard/hr_timesheet_analytic_profit_view.xml',
'wizard/hr_timesheet_invoice_create_view.xml',
'wizard/hr_timesheet_invoice_create_final_view.xml',
+ 'views/report_analyticprofit.xml',
],
'demo': ['hr_timesheet_invoice_demo.xml'],
'test': ['test/test_hr_timesheet_invoice.yml',
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml
index 473820d5123..3c6df8ef378 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml
@@ -2,13 +2,12 @@
-
+ name="hr_timesheet_invoice.report_analyticprofit"
+ file="hr_timesheet_invoice.report_analyticprofit"
+ report_type="qweb-pdf"
+ string="Timesheet Profit"
+ />
diff --git a/addons/hr_timesheet_invoice/report/account_analytic_profit.py b/addons/hr_timesheet_invoice/report/account_analytic_profit.py
index bfaf585aff5..3f7108fb665 100644
--- a/addons/hr_timesheet_invoice/report/account_analytic_profit.py
+++ b/addons/hr_timesheet_invoice/report/account_analytic_profit.py
@@ -20,6 +20,8 @@
##############################################################################
from openerp.report import report_sxw
+from openerp.osv import osv
+
class account_analytic_profit(report_sxw.rml_parse):
def __init__(self, cr, uid, name, context):
@@ -30,6 +32,7 @@ class account_analytic_profit(report_sxw.rml_parse):
'journal_ids': self._journal_ids,
'line': self._line,
})
+
def _user_ids(self, lines):
user_obj = self.pool['res.users']
ids=list(set([b.user_id.id for b in lines]))
@@ -116,6 +119,11 @@ class account_analytic_profit(report_sxw.rml_parse):
])
return line_obj.browse(self.cr, self.uid, ids)
-report_sxw.report_sxw('report.account.analytic.profit', 'account.analytic.line', 'addons/hr_timesheet_invoice/report/account_analytic_profit.rml', parser=account_analytic_profit)
+
+class report_account_analytic_profit(osv.AbstractModel):
+ _name = 'report.hr_timesheet_invoice.report_analyticprofit'
+ _inherit = 'report.abstract_report'
+ _template = 'hr_timesheet_invoice.report_analyticprofit'
+ _wrapped_report_class = account_analytic_profit
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_timesheet_invoice/report/account_analytic_profit.rml b/addons/hr_timesheet_invoice/report/account_analytic_profit.rml
deleted file mode 100644
index 57ca806061a..00000000000
--- a/addons/hr_timesheet_invoice/report/account_analytic_profit.rml
+++ /dev/null
@@ -1,341 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Invoice rate by user
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Period from startdate
-
-
- Period to enddate
-
-
- Currency
-
-
-
-
-
-
- [[ formatLang(data['form']['date_from'],date=True) ]]
-
-
- [[ formatLang (data['form']['date_to'] ,date=True)]]
-
-
- [[ company.currency_id.name ]]
-
-
-
-
-
-
-
-
-
-
-
-
- User or Journal Name
-
-
- Units
-
-
- Theorical
-
-
- Income
-
-
- Cost
-
-
- Profit
-
-
- Eff.
-
-
-
-
-
-
-
-
-
- Totals:
-
-
- [[ reduce(lambda x, y: x+y['unit_amount'], line(data['form'], data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]]
-
-
-
-
-
-
-
- [[ reduce(lambda x, y: x+y['amount'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]]
-
-
- [[ reduce(lambda x, y: x+y['cost'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]]
-
-
- [[ reduce(lambda x, y: x+y['profit'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]]
-
-
- [[ reduce(lambda x, y: x+y['cost'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) and round(reduce(lambda x, y: x+y['amount'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0)/reduce(lambda x, y: x+y['cost'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0)* -100, 2)]] %
-
-
-
-
-
- [[ repeatIn(user_ids(lines(data['form'])), 'e') ]]
-
-
-
- [[ e.name ]]
-
-
- [[ repeatIn(journal_ids(data['form'], [e.id]), 'j') ]]
-
-
- [[ reduce(lambda x, y: x+y['unit_amount'], line(data['form'], [j.id], [e.id]), 0) ]]
-
-
- [[ reduce(lambda x, y: x+y['amount_th'], line(data['form'], [j.id], [e.id]), 0) ]]
-
-
- [[ reduce(lambda x, y: x+y['amount'], line(data['form'], [j.id], [e.id]), 0) ]]
-
-
- [[ reduce(lambda x, y: x+y['cost'], line(data['form'], [j.id], [e.id]), 0) ]]
-
-
- [[ reduce(lambda x, y: x+y['profit'], line(data['form'], [j.id], [e.id]), 0) ]]
-
-
- [[reduce(lambda x, y: x+y['cost'], line(data['form'], [j.id], [e.id]), 0) and '%d' % (reduce(lambda x, y: x+y['amount'], line(data['form'], [j.id], [e.id]), 0) / reduce(lambda x, y: x+y['cost'], line(data['form'], [j.id], [e.id]), 0) * 100.0, 2)]] %
-
-
-
-
-
-
- [[ j.name ]]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- [[ repeatIn(line(data['form'], [j.id],[e.id]), 'l') ]]
-
-
-
- [[ l['name'] ]]
-
-
- [[ l['unit_amount'] ]]
-
-
- [[ l['amount_th'] ]]
-
-
- [[ l['amount'] ]]
-
-
- [[ l['cost'] ]]
-
-
- [[ l['profit'] ]]
-
-
- [[ l['eff'] ]] %
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/hr_timesheet_invoice/test/hr_timesheet_invoice_report.yml b/addons/hr_timesheet_invoice/test/hr_timesheet_invoice_report.yml
index 39c3161f244..e2fa5b344a9 100644
--- a/addons/hr_timesheet_invoice/test/hr_timesheet_invoice_report.yml
+++ b/addons/hr_timesheet_invoice/test/hr_timesheet_invoice_report.yml
@@ -6,6 +6,6 @@
import openerp.report
from openerp import tools
data_dict = {'model': 'ir.ui.menu', 'form': {'date_from': time.strftime('%Y-%m-01'), 'employee_ids': [[6,0,[ref('hr.employee_fp'), ref('hr.employee_qdp'),ref('hr.employee_al')]]], 'journal_ids': [[6,0,[ref('hr_timesheet.analytic_journal')]]], 'date_to': time.strftime('%Y-%m-%d')}}
- data, format = openerp.report.render_report(cr, uid, [], 'account.analytic.profit', data_dict, {})
+ data, format = openerp.report.render_report(cr, uid, [], 'hr_timesheet_invoice.report_analyticprofit', data_dict, {})
if tools.config['test_report_directory']:
file(os.path.join(tools.config['test_report_directory'], 'hr_timesheet_invoice-account_analytic_profit_report.'+format), 'wb+').write(data)
diff --git a/addons/hr_timesheet_invoice/views/report_analyticprofit.xml b/addons/hr_timesheet_invoice/views/report_analyticprofit.xml
new file mode 100644
index 00000000000..c067a9b1d2d
--- /dev/null
+++ b/addons/hr_timesheet_invoice/views/report_analyticprofit.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
Invoice rate by user
+
+
+
+
Period from startdate:
+
+
+
+
+
+
+
+
+
+ User or Journal Name
+ Units
+ Theorical
+ Income
+ Cost
+ Profit
+ Eff.
+
+
+
+
+ Totals:
+
+
+
+
+
+ %
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/hr_timesheet_invoice/wizard/hr_timesheet_analytic_profit.py b/addons/hr_timesheet_invoice/wizard/hr_timesheet_analytic_profit.py
index 2675556e8ac..a78bc1c5bf5 100644
--- a/addons/hr_timesheet_invoice/wizard/hr_timesheet_analytic_profit.py
+++ b/addons/hr_timesheet_invoice/wizard/hr_timesheet_analytic_profit.py
@@ -23,6 +23,7 @@ import datetime
from openerp.osv import fields, osv
from openerp.tools.translate import _
+
class account_analytic_profit(osv.osv_memory):
_name = 'hr.timesheet.analytic.profit'
_description = 'Print Timesheet Profit'
@@ -60,15 +61,12 @@ class account_analytic_profit(osv.osv_memory):
data['form']['journal_ids'] = [(6, 0, data['form']['journal_ids'])] # Improve me => Change the rml/sxw so that it can support withou [0][2]
data['form']['employee_ids'] = [(6, 0, data['form']['employee_ids'])]
datas = {
- 'ids': [],
- 'model': 'account.analytic.line',
- 'form': data['form']
- }
- return {
- 'type': 'ir.actions.report.xml',
- 'report_name': 'account.analytic.profit',
- 'datas': datas,
- }
-
+ 'ids': [],
+ 'model': 'account.analytic.line',
+ 'form': data['form']
+ }
+ return self.pool['report'].get_action(
+ cr, uid, [], 'hr_timesheet_invoice.report_analyticprofit', data=datas, context=context
+ )
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml
index cdd7eb12c69..8e52f7e1f7d 100644
--- a/addons/mail/data/mail_data.xml
+++ b/addons/mail/data/mail_data.xml
@@ -53,7 +53,7 @@
- none
+ none
diff --git a/addons/mail/data/mail_demo.xml b/addons/mail/data/mail_demo.xml
index dde8d1d9804..09415ba89e0 100644
--- a/addons/mail/data/mail_demo.xml
+++ b/addons/mail/data/mail_demo.xml
@@ -4,76 +4,76 @@
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
- none
+ none
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index 0059df90530..86246bc2745 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -96,13 +96,7 @@ class mail_notification(osv.Model):
if message.author_id and message.author_id.email == partner.email:
continue
# 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
- if partner.notification_email_send == 'comment' and message.type not in ('email', 'comment'):
- continue
- # Partner wants to receive only emails
- if partner.notification_email_send == 'email' and message.type != 'email':
+ if partner.notify_email == 'none':
continue
notify_pids.append(partner.id)
return notify_pids
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index a33eaaea15d..12903d02236 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -126,7 +126,7 @@ class mail_mail(osv.Model):
_logger.exception("Failed processing mail queue")
return res
- def _postprocess_sent_message(self, cr, uid, mail, context=None):
+ def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
"""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.
@@ -145,9 +145,8 @@ class mail_mail(osv.Model):
#------------------------------------------------------
def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None):
- """ Generate URLs for links in mails:
- - partner is an user and has read access to the document: direct link to document with model, res_id
- """
+ """Generate URLs for links in mails: partner has access (is user):
+ link to action_mail_redirect action that will redirect to doc or Inbox """
if partner and partner.user_ids:
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
# the parameters to encode for the query and fragment part of url
@@ -167,11 +166,10 @@ class mail_mail(osv.Model):
return None
def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
- """ If subject is void and record_name defined: ' posted on '
+ """If subject is void, set the subject as 'Re: ' or
+ 'Re: '
:param boolean force: force the subject replacement
- :param browse_record mail: mail.mail browse_record
- :param browse_record partner: specific recipient partner
"""
if (force or not mail.subject) and mail.record_name:
return 'Re: %s' % (mail.record_name)
@@ -180,12 +178,8 @@ class mail_mail(osv.Model):
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 to add custom content depending on some module.
-
- :param browse_record mail: mail.mail browse_record
- :param browse_record partner: specific recipient partner
- """
+ """Return a specific ir_email body. The main purpose of this method
+ is to be inherited to add custom content depending on some module."""
body = mail.body_html
# generate footer
@@ -194,34 +188,34 @@ class mail_mail(osv.Model):
body = tools.append_content_to_html(body, link, 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
- partner, or generic to the whole recipients given by mail.email_to.
-
- :param browse_record mail: mail.mail browse_record
- :param browse_record partner: specific recipient partner
- """
- 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)
- body_alternative = tools.html2plaintext(body)
-
- # generate email_to, heuristic:
- # 1. if 'partner' is specified and there is a related document: Followers of 'Doc'
- # 2. if 'partner' is specified, but no related document: Partner Name
- # 3; fallback on mail.email_to that we split to have an email addresses list
- if partner and mail.record_name:
+ def send_get_mail_to(self, cr, uid, mail, partner=None, context=None):
+ """Forge the email_to with the following heuristic:
+ - if 'partner' and mail is a notification on a document: followers (Followers of 'Doc' )
+ - elif 'partner', no notificatoin or no doc: recipient specific (Partner Name )
+ - else fallback on mail.email_to splitting """
+ if partner and mail.notification and mail.record_name:
sanitized_record_name = re.sub(r'[^\w+.]+', '-', mail.record_name)
email_to = [_('"Followers of %s" <%s>') % (sanitized_record_name, partner.email)]
elif partner:
email_to = ['%s <%s>' % (partner.name, partner.email)]
else:
email_to = tools.email_split(mail.email_to)
+ return email_to
+ def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
+ """Return a dictionary for specific email values, depending on a
+ partner, or generic to the whole recipients given by mail.email_to.
+
+ :param browse_record mail: mail.mail browse_record
+ :param browse_record partner: specific recipient partner
+ """
+ body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
+ body_alternative = tools.html2plaintext(body)
return {
'body': body,
'body_alternative': body_alternative,
- 'subject': subject,
- 'email_to': email_to,
+ 'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context),
+ 'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context),
}
def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
@@ -240,7 +234,7 @@ class mail_mail(osv.Model):
:return: True
"""
ir_mail_server = self.pool.get('ir.mail_server')
-
+
for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
try:
# handle attachments
@@ -284,7 +278,7 @@ class mail_mail(osv.Model):
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})
mail_sent = True
@@ -294,11 +288,11 @@ class mail_mail(osv.Model):
# /!\ can't use mail.state here, as mail.refresh() will cause an error
# see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
- if mail_sent:
- self._postprocess_sent_message(cr, uid, mail, context=context)
+ self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)
except Exception as e:
_logger.exception('failed sending mail.mail %s', mail.id)
mail.write({'state': 'exception'})
+ self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=False)
if raise_exception:
if isinstance(e, AssertionError):
# get the args of the original error, wrap into a value and throw a MailDeliveryException
@@ -307,6 +301,6 @@ class mail_mail(osv.Model):
raise MailDeliveryException(_("Mail Delivery Failed"), value)
raise
- if auto_commit == True:
+ if auto_commit is True:
cr.commit()
return True
diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml
index cf3dd1e8233..e259d28a2b7 100644
--- a/addons/mail/mail_mail_view.xml
+++ b/addons/mail/mail_mail_view.xml
@@ -36,6 +36,7 @@
+
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 4c05d7a6d1f..5b716a14ef2 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -81,22 +81,6 @@ class mail_message(osv.Model):
context = dict(context, default_type=None)
return super(mail_message, self).default_get(cr, uid, fields, context=context)
- 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_record_name(self, cr, uid, ids, name, arg, context=None):
- """ Return the related document name, using name_get. It is done using
- SUPERUSER_ID, to be sure to have the record name correctly stored. """
- # TDE note: regroup by model/ids, to have less queries to perform
- result = dict.fromkeys(ids, False)
- for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
- if not message.get('model') or not message.get('res_id') or message['model'] not in self.pool:
- continue
- result[message['id']] = self.pool[message['model']].name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1]
- return result
-
def _get_to_read(self, cr, uid, ids, name, arg, context=None):
""" Compute if the message is unread by the current user. """
res = dict((id, False) for id in ids)
@@ -135,16 +119,6 @@ class mail_message(osv.Model):
inversed because we search unread message on a read column. """
return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])]
- 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 = '%s: %s' % (message.subject or '', strip_tags(message.body or '') or '')
- res.append((message.id, self._shorten_name(name.lstrip(' :'))))
- return res
-
_columns = {
'type': fields.selection([
('email', 'Email'),
@@ -172,9 +146,7 @@ class mail_message(osv.Model):
'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='char',
- store=True, string='Message Record Name',
- help="Name get of the related document."),
+ 'record_name': fields.char('Message Record Name', help="Name get of the related document."),
'notification_ids': fields.one2many('mail.notification', 'message_id',
string='Notifications', auto_join=True,
help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
@@ -783,6 +755,13 @@ class mail_message(osv.Model):
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
(self._description, operation))
+ def _get_record_name(self, cr, uid, values, context=None):
+ """ Return the related document name, using name_get. It is done using
+ SUPERUSER_ID, to be sure to have the record name correctly stored. """
+ if not values.get('model') or not values.get('res_id') or values['model'] not in self.pool:
+ return False
+ return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1]
+
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
@@ -841,8 +820,11 @@ class mail_message(osv.Model):
values['message_id'] = self._get_message_id(cr, uid, values, context=context)
if 'reply_to' not in values:
values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
+ if 'record_name' not in values and 'default_record_name' not in context:
+ values['record_name'] = self._get_record_name(cr, uid, values, context=context)
newid = super(mail_message, self).create(cr, uid, values, context)
+
self._notify(cr, uid, newid, context=context,
force_send=context.get('mail_notify_force_send', True),
user_signature=context.get('mail_notify_user_signature', True))
@@ -887,78 +869,6 @@ class mail_message(osv.Model):
# Messaging API
#------------------------------------------------------
- # TDE note: this code is not used currently, will be improved in a future merge, when quoted context
- # will be added to email send for notifications. Currently only WIP.
- MAIL_TEMPLATE = """
- % if message:
- ${display_message(message)}
- % endif
- % for ctx_msg in context_messages:
- ${display_message(ctx_msg)}
- % endfor
- % if add_expandable:
- ${display_expandable()}
- % endif
- ${display_message(header_message)}
-
-
- <%def name="display_message(message)">
-
- Subject: ${message.subject}
- Body: ${message.body}
-
- %def>
-
- <%def name="display_expandable()">
- This is an expandable.
- %def>
- """
-
- def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False):
- """
- 1. message.parent_id = False: new thread, no quote_context
- 2. get the lasts messages in the thread before message
- 3. get the message header
- 4. add an expandable between them
-
- :param dict quote_context: options for quoting
- :return string: html quote
- """
- add_expandable = False
-
- message = self.browse(cr, uid, id, context=context)
- if not message.parent_id:
- return ''
- context_ids = self.search(cr, uid, [
- ('parent_id', '=', message.parent_id.id),
- ('id', '<', message.id),
- ], limit=limit, context=context)
-
- if len(context_ids) >= limit:
- add_expandable = True
- context_ids = context_ids[0:-1]
-
- context_ids.append(message.parent_id.id)
- context_messages = self.browse(cr, uid, context_ids, context=context)
- header_message = context_messages.pop()
-
- try:
- if not add_original:
- message = False
- result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message,
- context_messages=context_messages,
- header_message=header_message,
- add_expandable=add_expandable,
- # context kw would clash with mako internals
- ctx=context,
- format_exceptions=True)
- result = result.strip()
- return result
- except Exception:
- _logger.exception("failed to render mako template for quoting message")
- return ''
- return result
-
def _notify(self, cr, uid, newid, context=None, force_send=False, user_signature=True):
""" Add the related record followers to the destination partner_ids if is not a private message.
Call mail_notification.notify to manage the email sending
@@ -975,9 +885,11 @@ class mail_message(osv.Model):
cr, SUPERUSER_ID, [
('res_model', '=', message.model),
('res_id', '=', message.res_id),
- ('subtype_ids', 'in', message.subtype_id.id)
], context=context)
- partners_to_notify |= set(fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context))
+ partners_to_notify |= set(
+ fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)
+ if message.subtype_id.id in [st.id for st in fo.subtype_ids]
+ )
# remove me from notified partners, unless the message is written on my own wall
if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
partners_to_notify |= set([message.author_id.id])
@@ -1006,25 +918,3 @@ class mail_message(osv.Model):
'partner_id': partner.id,
'read': True,
}, context=context)
-
- #------------------------------------------------------
- # Tools
- #------------------------------------------------------
-
- def check_partners_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,
- }
- }
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 73588207d6a..112dff6bac6 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -31,6 +31,7 @@ except ImportError:
from lxml import etree
import logging
import pytz
+import socket
import time
import xmlrpclib
from email.message import Message
@@ -96,6 +97,9 @@ class mail_thread(osv.AbstractModel):
# :param function lambda: returns whether the tracking should record using this subtype
_track = {}
+ # Mass mailing feature
+ _mail_mass_mailing = False
+
def get_empty_list_help(self, cr, uid, help, context=None):
""" Override of BaseModel.get_empty_list_help() to generate an help message
that adds alias information. """
@@ -584,23 +588,6 @@ class mail_thread(osv.AbstractModel):
model_obj.check_access_rights(cr, uid, check_operation)
model_obj.check_access_rule(cr, uid, mids, check_operation, context=context)
- def _get_formview_action(self, cr, uid, id, model=None, context=None):
- """ Return an action to open the document. This method is meant to be
- overridden in addons that want to give specific view ids for example.
-
- :param int id: id of the document to open
- :param string model: specific model that overrides self._name
- """
- return {
- 'type': 'ir.actions.act_window',
- 'res_model': model or self._name,
- 'view_type': 'form',
- 'view_mode': 'form',
- 'views': [(False, 'form')],
- 'target': 'current',
- 'res_id': id,
- }
-
def _get_inbox_action_xml_id(self, cr, uid, context=None):
""" When redirecting towards the Inbox, choose which action xml_id has
to be fetched. This method is meant to be inherited, at least in portal
@@ -643,10 +630,7 @@ class mail_thread(osv.AbstractModel):
if model_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
try:
model_obj.check_access_rule(cr, uid, [res_id], 'read', context=context)
- if not hasattr(model_obj, '_get_formview_action'):
- action = self.pool.get('mail.thread')._get_formview_action(cr, uid, res_id, model=model, context=context)
- else:
- action = model_obj._get_formview_action(cr, uid, res_id, context=context)
+ action = model_obj.get_formview_action(cr, uid, res_id, context=context)
except (osv.except_osv, orm.except_orm):
pass
action.update({
@@ -661,15 +645,31 @@ class mail_thread(osv.AbstractModel):
# Email specific
#------------------------------------------------------
+ def message_get_default_recipients(self, cr, uid, ids, context=None):
+ if context and context.get('thread_model') and context['thread_model'] in self.pool and context['thread_model'] != self._name:
+ sub_ctx = dict(context)
+ sub_ctx.pop('thread_model')
+ return self.pool[context['thread_model']].message_get_default_recipients(cr, uid, ids, context=sub_ctx)
+ res = {}
+ for record in self.browse(cr, SUPERUSER_ID, ids, context=context):
+ recipient_ids, email_to, email_cc = set(), False, False
+ if 'partner_id' in self._all_columns and record.partner_id:
+ recipient_ids.add(record.partner_id.id)
+ elif 'email_from' in self._all_columns and record.email_from:
+ email_to = record.email_from
+ elif 'email' in self._all_columns:
+ email_to = record.email
+ res[record.id] = {'partner_ids': list(recipient_ids), 'email_to': email_to, 'email_cc': email_cc}
+ return res
+
def message_get_reply_to(self, cr, uid, ids, context=None):
""" Returns the preferred reply-to email address that is basically
the alias of the document, if it exists. """
if not self._inherits.get('mail.alias'):
return [False for id in ids]
- return ["%s@%s" % (record['alias_name'], record['alias_domain'])
- if record.get('alias_domain') and record.get('alias_name')
- else False
- for record in self.read(cr, SUPERUSER_ID, ids, ['alias_name', 'alias_domain'], context=context)]
+ return ["%s@%s" % (record.alias_name, record.alias_domain)
+ if record.alias_domain and record.alias_name else False
+ for record in self.browse(cr, SUPERUSER_ID, ids, context=context)]
#------------------------------------------------------
# Mail gateway
@@ -880,25 +880,30 @@ class mail_thread(osv.AbstractModel):
# 2. message is a reply to an existign thread (6.1 compatibility)
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 fallback_model
- if thread_id and model in self.pool:
- model_obj = self.pool[model]
- compat_mail_msg_ids = mail_msg_obj.search(
- cr, uid, [
- ('message_id', '=', False),
- ('model', '=', model),
- ('res_id', '=', thread_id),
- ], context=context)
- if compat_mail_msg_ids and model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
- _logger.info(
- 'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, model, thread_id, custom_values, uid)
- route = self.message_route_verify(
- cr, uid, message, message_dict,
- (model, thread_id, custom_values, uid, None),
- update_author=True, assert_model=True, create_fallback=True, context=context)
- return route and [route] or []
+ reply_thread_id = int(ref_match.group(1))
+ reply_model = ref_match.group(2) or fallback_model
+ reply_hostname = ref_match.group(3)
+ local_hostname = socket.gethostname()
+ # do not match forwarded emails from another OpenERP system (thread_id collision!)
+ if local_hostname == reply_hostname:
+ thread_id, model = reply_thread_id, reply_model
+ if thread_id and model in self.pool:
+ model_obj = self.pool[model]
+ compat_mail_msg_ids = mail_msg_obj.search(
+ cr, uid, [
+ ('message_id', '=', False),
+ ('model', '=', model),
+ ('res_id', '=', thread_id),
+ ], context=context)
+ if compat_mail_msg_ids and model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
+ _logger.info(
+ 'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
+ email_from, email_to, message_id, model, thread_id, custom_values, uid)
+ route = self.message_route_verify(
+ cr, uid, message, message_dict,
+ (model, thread_id, custom_values, uid, None),
+ update_author=True, assert_model=True, create_fallback=True, context=context)
+ return route and [route] or []
# 2. Reply to a private message
if in_reply_to:
diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py
index 8da2441f3bc..bf801511761 100644
--- a/addons/mail/res_partner.py
+++ b/addons/mail/res_partner.py
@@ -28,23 +28,21 @@ class res_partner_mail(osv.Model):
_name = "res.partner"
_inherit = ['res.partner', 'mail.thread']
_mail_flat_thread = False
+ _mail_mass_mailing = _('Customers')
_columns = {
- 'notification_email_send': fields.selection([
+ 'notify_email': fields.selection([
('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,
+ ('always', 'All Messages'),
+ ], 'Receive Inbox Notifications by Email', required=True,
+ oldname='notification_email_send',
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'
+ 'notify_email': lambda *args: 'always'
}
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
@@ -53,4 +51,5 @@ class res_partner_mail(osv.Model):
self._message_add_suggested_recipient(cr, uid, recipients, partner, partner=partner, reason=_('Partner Profile'))
return recipients
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+ def message_get_default_recipients(self, cr, uid, ids, context=None):
+ return dict((id, {'partner_ids': [id], 'email_to': False, 'email_cc': False}) for id in ids)
diff --git a/addons/mail/res_partner_view.xml b/addons/mail/res_partner_view.xml
index 7df5d27c9d0..5066d85e682 100644
--- a/addons/mail/res_partner_view.xml
+++ b/addons/mail/res_partner_view.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index 42e739d8051..8fe1482b427 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -54,10 +54,10 @@ class res_users(osv.Model):
init_res = super(res_users, self).__init__(pool, cr)
# duplicate list to avoid modifying the original reference
self.SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS)
- self.SELF_WRITEABLE_FIELDS.extend(['notification_email_send', 'display_groups_suggestions'])
+ self.SELF_WRITEABLE_FIELDS.extend(['notify_email', 'display_groups_suggestions'])
# duplicate list to avoid modifying the original reference
self.SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS)
- self.SELF_READABLE_FIELDS.extend(['notification_email_send', 'alias_domain', 'alias_name', 'display_groups_suggestions'])
+ self.SELF_READABLE_FIELDS.extend(['notify_email', 'alias_domain', 'alias_name', 'display_groups_suggestions'])
return init_res
def _auto_init(self, cr, context=None):
diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml
index 2bf54b4bafd..eef2924134f 100644
--- a/addons/mail/res_users_view.xml
+++ b/addons/mail/res_users_view.xml
@@ -10,7 +10,7 @@
-
+
@@ -24,7 +24,7 @@
-
+
-
diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py
index 68a329e043a..d53a8078c96 100644
--- a/addons/mail/tests/common.py
+++ b/addons/mail/tests/common.py
@@ -77,7 +77,7 @@ class TestMail(common.TransactionCase):
'alias_name': 'ernest',
'email': 'e.e@example.com',
'signature': '--\nErnest',
- 'notification_email_send': 'comment',
+ 'notify_email': 'always',
'groups_id': [(6, 0, [self.group_employee_id])]
}, {'no_reset_password': True})
self.user_noone_id = self.res_users.create(cr, uid, {
@@ -86,7 +86,7 @@ class TestMail(common.TransactionCase):
'alias_name': 'noemie',
'email': 'n.n@example.com',
'signature': '--\nNoemie',
- 'notification_email_send': 'comment',
+ 'notify_email': 'always',
'groups_id': [(6, 0, [])]
}, {'no_reset_password': True})
diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py
index c7f1056c59a..0a481c78eca 100644
--- a/addons/mail/tests/test_mail_features.py
+++ b/addons/mail/tests/test_mail_features.py
@@ -210,24 +210,6 @@ class test_mail(TestMail):
self.assertTrue(subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs')
self.assertTrue(subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs')
- def test_10_message_quote_context(self):
- """ Tests designed for message_post. """
- cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
-
- msg1_id = self.mail_message.create(cr, uid, {'body': 'Thread header about Zap Brannigan', 'subject': 'My subject'})
- msg2_id = self.mail_message.create(cr, uid, {'body': 'First answer, should not be displayed', 'subject': 'Re: My subject', 'parent_id': msg1_id})
- msg3_id = self.mail_message.create(cr, uid, {'body': 'Second answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
- msg4_id = self.mail_message.create(cr, uid, {'body': 'Third answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
- msg_new_id = self.mail_message.create(cr, uid, {'body': 'My answer I am propagating', 'subject': 'Re: My subject', 'parent_id': msg1_id})
-
- result = self.mail_message.message_quote_context(cr, uid, msg_new_id, limit=3)
- self.assertIn('Thread header about Zap Brannigan', result, 'Thread header content should be in quote.')
- self.assertIn('Second answer', result, 'Answer should be in quote.')
- self.assertIn('Third answer', result, 'Answer should be in quote.')
- self.assertIn('expandable', result, 'Expandable should be present.')
- self.assertNotIn('First answer, should not be displayed', result, 'Old answer should not be in quote.')
- self.assertNotIn('My answer I am propagating', result, 'Thread header content should be in quote.')
-
def test_11_notification_url(self):
""" Tests designed to test the URL added in notification emails. """
cr, uid, group_pigs = self.cr, self.uid, self.group_pigs
@@ -366,14 +348,14 @@ class test_mail(TestMail):
# Data creation
# --------------------------------------------------
# 0 - Update existing users-partners
- self.res_users.write(cr, uid, [uid], {'email': 'a@a', 'notification_email_send': 'comment'})
+ self.res_users.write(cr, uid, [uid], {'email': 'a@a', 'notify_email': 'always'})
self.res_users.write(cr, uid, [self.user_raoul_id], {'email': 'r@r'})
# 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 receive emails for emails
- p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notification_email_send': 'email'})
+ p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notify_email': 'none'})
# 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', 'email': 'd@d', 'notification_email_send': 'all'})
+ p_d_id = self.res_partner.create(cr, uid, {'name': 'Dédé Grosbedon', 'email': 'd@d', 'notify_email': 'always'})
# 4 - Attachments
attach1_id = self.ir_attachment.create(cr, user_raoul.id, {
'name': 'Attach1', 'datas_fname': 'Attach1',
@@ -600,9 +582,9 @@ class test_mail(TestMail):
# 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 receive emails for emails
- p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notification_email_send': 'email'})
+ p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notify_email': 'always'})
# 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', 'email': 'd@d', 'notification_email_send': 'all'})
+ p_d_id = self.res_partner.create(cr, uid, {'name': 'Dédé Grosbedon', 'email': 'd@d', 'notify_email': 'always'})
# 4 - Create a Bird mail.group, that will be used to test mass mailing
group_bird_id = self.mail_group.create(cr, uid,
{
@@ -674,7 +656,6 @@ class test_mail(TestMail):
'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])]
}, context={
'default_composition_mode': 'reply',
- 'default_model': 'mail.thread',
'default_res_id': self.group_pigs_id,
'default_parent_id': message.id
})
@@ -699,11 +680,10 @@ class test_mail(TestMail):
# --------------------------------------------------
# Do: Compose in mass_mail_mode on pigs and bird
- compose_id = mail_compose.create(cr, user_raoul.id,
- {
+ compose_id = mail_compose.create(
+ cr, user_raoul.id, {
'subject': _subject,
'body': '${object.description}',
- 'post': True,
'partner_ids': [(4, p_c_id), (4, p_d_id)],
}, context={
'default_composition_mode': 'mass_mail',
@@ -718,6 +698,13 @@ class test_mail(TestMail):
'default_res_id': -1,
'active_ids': [self.group_pigs_id, group_bird_id]
})
+ # check mail_mail
+ mail_mail_ids = self.mail_mail.search(cr, uid, [('subject', '=', _subject)])
+ for mail_mail in self.mail_mail.browse(cr, uid, mail_mail_ids):
+ self.assertEqual(set([p.id for p in mail_mail.recipient_ids]), set([p_c_id, p_d_id]),
+ 'compose wizard: mail_mail mass mailing: mail.mail in mass mail incorrect recipients')
+
+ # check logged messages
group_pigs.refresh()
group_bird.refresh()
message1 = group_pigs.message_ids[0]
@@ -733,14 +720,14 @@ class test_mail(TestMail):
'compose wizard: message_post: mail.message in mass mail subject incorrect')
self.assertEqual(message1.body, '%s
' % group_pigs.description,
'compose wizard: message_post: mail.message in mass mail body incorrect')
- self.assertEqual(set([p.id for p in message1.notified_partner_ids]), set([p_c_id, p_d_id]),
- 'compose wizard: message_post: mail.message in mass mail incorrect notified partners')
+ # self.assertEqual(set([p.id for p in message1.notified_partner_ids]), set([p_c_id, p_d_id]),
+ # 'compose wizard: message_post: mail.message in mass mail incorrect notified partners')
self.assertEqual(message2.subject, _subject,
'compose wizard: message_post: mail.message in mass mail subject incorrect')
self.assertEqual(message2.body, '%s
' % group_bird.description,
'compose wizard: message_post: mail.message in mass mail body incorrect')
- self.assertEqual(set([p.id for p in message2.notified_partner_ids]), set([p_c_id, p_d_id]),
- 'compose wizard: message_post: mail.message in mass mail incorrect notified partners')
+ # self.assertEqual(set([p.id for p in message2.notified_partner_ids]), set([p_c_id, p_d_id]),
+ # 'compose wizard: message_post: mail.message in mass mail incorrect notified partners')
# Test: mail.group followers: author not added as follower in mass mail mode
pigs_pids = [p.id for p in group_pigs.message_follower_ids]
@@ -757,7 +744,6 @@ class test_mail(TestMail):
{
'subject': _subject,
'body': '${object.description}',
- 'post': True,
'partner_ids': [(4, p_c_id), (4, p_d_id)],
}, context={
'default_composition_mode': 'mass_mail',
diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py
index cb735ec098e..3fbf3d40405 100644
--- a/addons/mail/tests/test_mail_gateway.py
+++ b/addons/mail/tests/test_mail_gateway.py
@@ -21,6 +21,7 @@
from openerp.addons.mail.tests.common import TestMail
from openerp.tools import mute_logger
+import socket
MAIL_TEMPLATE = """Return-Path:
To: {to}
@@ -400,13 +401,15 @@ class TestMailgateway(TestMail):
to='noone@example.com', subject='spam',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>' % frog_group.id,
msg_id='<1.1.JavaMail.new@agrolait.com>')
- # There are 6.1 messages, activate compat mode
+
+ # When 6.1 messages are present, compat mode is available
+ # Create a fake 6.1 message
tmp_msg_id = self.mail_message.create(cr, uid, {'message_id': False, 'model': 'mail.group', 'res_id': frog_group.id})
# Do: compat mode accepts partial-matching emails
frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other5@gmail.com',
msg_id='<1.2.JavaMail.new@agrolait.com>',
to='noone@example.com>', subject='spam',
- extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>' % frog_group.id)
+ extra='In-Reply-To: <12321321-openerp-%d-mail.group@%s>' % (frog_group.id, socket.gethostname()))
self.mail_message.unlink(cr, uid, [tmp_msg_id])
# Test: no group 'Re: news' created, still only 1 Frogs group
self.assertEqual(len(frog_groups), 0,
@@ -418,6 +421,17 @@ class TestMailgateway(TestMail):
# Test: one new message
self.assertEqual(len(frog_group.message_ids), 4, 'message_process: group should contain 4 messages after reply')
+ # 6.1 compat mode should not work if hostname does not match!
+ tmp_msg_id = self.mail_message.create(cr, uid, {'message_id': False, 'model': 'mail.group', 'res_id': frog_group.id})
+ self.assertRaises(ValueError,
+ format_and_process,
+ MAIL_TEMPLATE, email_from='other5@gmail.com',
+ msg_id='<1.3.JavaMail.new@agrolait.com>',
+ to='noone@example.com>', subject='spam',
+ extra='In-Reply-To: <12321321-openerp-%d-mail.group@neighbor.com>' % frog_group.id)
+ self.mail_message.unlink(cr, uid, [tmp_msg_id])
+
+
# Do: due to some issue, same email goes back into the mailgateway
frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
@@ -445,7 +459,7 @@ class TestMailgateway(TestMail):
# Do: post a new message, with a known partner -> duplicate emails -> partner
format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik ',
- to='erroneous@example.com>', subject='Re: news (2)',
+ subject='Re: news (2)',
msg_id='<1198923581.41972151344608186760.JavaMail.new1@agrolait.com>',
extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
@@ -456,10 +470,9 @@ class TestMailgateway(TestMail):
# Do: post a new message, with a known partner -> duplicate emails -> user
frog_group.message_unsubscribe([extra_partner_id])
- raoul_email = self.user_raoul.email
self.res_users.write(cr, uid, self.user_raoul_id, {'email': 'test_raoul@email.com'})
format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik ',
- to='erroneous@example.com>', subject='Re: news (3)',
+ to='groups@example.com', subject='Re: news (3)',
msg_id='<1198923581.41972151344608186760.JavaMail.new2@agrolait.com>',
extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
@@ -474,7 +487,7 @@ class TestMailgateway(TestMail):
raoul_email = self.user_raoul.email
self.res_users.write(cr, uid, self.user_raoul_id, {'email': 'test_raoul@email.com'})
format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik ',
- to='erroneous@example.com>', subject='Re: news (3)',
+ to='groups@example.com', subject='Re: news (3)',
msg_id='<1198923581.41972151344608186760.JavaMail.new3@agrolait.com>',
extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index e5ff2b51fcd..e14a8f3dfe5 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -38,10 +38,7 @@ class mail_compose_message(osv.TransientModel):
at model and view levels to provide specific features.
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``
+ - 'comment': 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.
@@ -50,6 +47,7 @@ class mail_compose_message(osv.TransientModel):
_inherit = 'mail.message'
_description = 'Email composition wizard'
_log_access = True
+ _batch_size = 500
def default_get(self, cr, uid, fields, context=None):
""" Handle composition mode. Some details about context keys:
@@ -68,28 +66,22 @@ class mail_compose_message(osv.TransientModel):
if context is None:
context = {}
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')
+
+ # v6.1 compatibility mode
+ result['composition_mode'] = result.get('composition_mode', context.get('mail.compose.message.mode'))
+ result['model'] = result.get('model', context.get('active_model'))
+ result['res_id'] = result.get('res_id', context.get('active_id'))
+ result['parent_id'] = result.get('parent_id', context.get('message_id'))
+
+ # default values according to composition mode - NOTE: reply is deprecated, fall back on comment
+ if result['composition_mode'] == 'reply':
+ result['composition_mode'] = 'comment'
+ vals = {}
if 'active_domain' in context: # not context.get() because we want to keep global [] domains
- result['use_active_domain'] = True
- result['active_domain'] = '%s' % context.get('active_domain')
- elif not result.get('active_domain'):
- result['active_domain'] = ''
- # get default values according to the composition mode
- 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}
- else:
- vals = {'model': model, 'res_id': res_id}
- if composition_mode:
- vals['composition_mode'] = composition_mode
+ vals['use_active_domain'] = True
+ vals['active_domain'] = '%s' % context.get('active_domain')
+ if result['composition_mode'] == 'comment':
+ vals.update(self.get_record_data(cr, uid, result, context=context))
for field in vals:
if field in fields:
@@ -102,13 +94,15 @@ class mail_compose_message(osv.TransientModel):
# but when creating the mail.message to create the mail.compose.message
# access rights issues may rise
# We therefore directly change the model and res_id
- if result.get('model') == 'res.users' and result.get('res_id') == uid:
+ if result['model'] == 'res.users' and result['res_id'] == uid:
result['model'] = 'res.partner'
result['res_id'] = self.pool.get('res.users').browse(cr, uid, uid).partner_id.id
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')]
+ return [('comment', 'Post on a document'),
+ ('mass_mail', 'Email Mass Mailing'),
+ ('mass_post', 'Post on Multiple Documents')]
_columns = {
'composition_mode': fields.selection(
@@ -116,19 +110,19 @@ class mail_compose_message(osv.TransientModel):
string='Composition mode'),
'partner_ids': fields.many2many('res.partner',
'mail_compose_message_res_partner_rel',
- 'wizard_id', 'partner_id', 'Additional contacts'),
+ 'wizard_id', 'partner_id', 'Additional Contacts'),
'use_active_domain': fields.boolean('Use active domain'),
'active_domain': fields.char('Active domain', readonly=True),
- 'post': fields.boolean('Post a copy in the document',
- help='Post a copy of the message on the document communication history.'),
- 'notify': fields.boolean('Notify followers',
- help='Notify followers of the document'),
- 'same_thread': fields.boolean('Replies in the document',
- help='Replies to the messages will go into the selected document.'),
'attachment_ids': fields.many2many('ir.attachment',
'mail_compose_message_ir_attachments_rel',
'wizard_id', 'attachment_id', 'Attachments'),
- 'filter_id': fields.many2one('ir.filters', 'Filters'),
+ 'is_log': fields.boolean('Log an Internal Note',
+ help='Whether the message is an internal note (comment mode only)'),
+ # mass mode options
+ 'notify': fields.boolean('Notify followers',
+ help='Notify followers of the document (mass post only)'),
+ 'same_thread': fields.boolean('Replies in the document',
+ help='Replies to the messages will go into the selected document (mass mail only)'),
}
#TODO change same_thread to False in trunk (Require view update)
_defaults = {
@@ -136,8 +130,6 @@ class mail_compose_message(osv.TransientModel):
'body': lambda self, cr, uid, ctx={}: '',
'subject': lambda self, cr, uid, ctx={}: False,
'partner_ids': lambda self, cr, uid, ctx={}: [],
- 'post': False,
- 'notify': False,
'same_thread': True,
}
@@ -169,61 +161,36 @@ class mail_compose_message(osv.TransientModel):
not want that feature in the wizard. """
return
- def get_record_data(self, cr, uid, model, res_id, context=None):
+ def get_record_data(self, cr, uid, values, 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``.
-
- :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
- """
- doc_name_get = self.pool[model].name_get(cr, uid, [res_id], context=context)
- record_name = False
- if doc_name_get:
- record_name = doc_name_get[0][1]
- values = {
- 'model': model,
- 'res_id': res_id,
- 'record_name': record_name,
- }
- if record_name:
- values['subject'] = 'Re: %s' % record_name
- return values
-
- def get_message_data(self, cr, uid, message_id, context=None):
- """ Returns a defaults-like dict with initial values for the composition
- wizard when replying to the given message (e.g. including the quote
- of the initial message, and the correct recipients).
-
- :param int message_id: id of the mail.message to which the user
- is replying.
- """
- if not message_id:
- return {}
+ wizard when sending an email related a previous email (parent_id) or
+ a document (model, res_id). This is based on previously computed default
+ values. """
if context is None:
context = {}
- message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context=context)
+ result, subject = {}, False
+ if values.get('parent_id'):
+ parent = self.pool.get('mail.message').browse(cr, uid, values.get('parent_id'), context=context)
+ result['record_name'] = parent.record_name,
+ subject = tools.ustr(parent.subject or parent.record_name or '')
+ if not values.get('model'):
+ result['model'] = parent.model
+ if not values.get('res_id'):
+ result['res_id'] = parent.res_id
+ partner_ids = values.get('partner_ids', list()) + [partner.id for partner in parent.partner_ids]
+ if context.get('is_private') and parent.author_id: # check message is private then add author also in partner list.
+ partner_ids += [parent.author_id.id]
+ result['partner_ids'] = partner_ids
+ elif values.get('model') and values.get('res_id'):
+ doc_name_get = self.pool[values.get('model')].name_get(cr, uid, [values.get('res_id')], context=context)
+ result['record_name'] = doc_name_get and doc_name_get[0][1] or ''
+ subject = tools.ustr(result['record_name'])
- # create subject
re_prefix = _('Re:')
- reply_subject = tools.ustr(message_data.subject or message_data.record_name or '')
- if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)) and message_data.subject:
- reply_subject = "%s %s" % (re_prefix, reply_subject)
- # get partner_ids from original message
- partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else []
- partner_ids += context.get('default_partner_ids', [])
- if context.get('is_private',False) and message_data.author_id : #check message is private then add author also in partner list.
- partner_ids += [message_data.author_id.id]
- # update the result
- result = {
- 'record_name': message_data.record_name,
- 'model': message_data.model,
- 'res_id': message_data.res_id,
- 'parent_id': message_data.id,
- 'subject': reply_subject,
- 'partner_ids': partner_ids,
- }
+ if subject and not (subject.startswith('Re:') or subject.startswith(re_prefix)):
+ subject = "%s %s" % (re_prefix, subject)
+ result['subject'] = subject
+
return result
#------------------------------------------------------
@@ -235,53 +202,42 @@ class mail_compose_message(osv.TransientModel):
email(s), rendering any template patterns on the fly if needed. """
if context is None:
context = {}
+
# clean the context (hint: mass mailing sets some default values that
# could be wrongly interpreted by mail_mail)
context.pop('default_email_to', None)
context.pop('default_partner_ids', None)
- active_ids = context.get('active_ids')
- is_log = context.get('mail_compose_log', False)
-
for wizard in self.browse(cr, uid, ids, context=context):
- mass_mail_mode = wizard.composition_mode == 'mass_mail'
+ mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post')
active_model_pool = self.pool[wizard.model if wizard.model else 'mail.thread']
if not hasattr(active_model_pool, 'message_post'):
context['thread_model'] = wizard.model
active_model_pool = self.pool['mail.thread']
# wizard works in batch mode: [res_id] or active_ids or active_domain
- if mass_mail_mode and wizard.use_active_domain and wizard.model:
+ if mass_mode and wizard.use_active_domain and wizard.model:
res_ids = self.pool[wizard.model].search(cr, uid, eval(wizard.active_domain), context=context)
- elif mass_mail_mode and wizard.model and active_ids:
- res_ids = active_ids
+ elif mass_mode and wizard.model and context.get('active_ids'):
+ res_ids = context['active_ids']
else:
res_ids = [wizard.res_id]
- all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context)
-
- for res_id, mail_values in all_mail_values.iteritems():
- if mass_mail_mode and not wizard.post:
- m2m_attachment_ids = self.pool['mail.thread']._message_preprocess_attachments(
- cr, uid, mail_values.pop('attachments', []),
- mail_values.pop('attachment_ids', []),
- 'mail.message', 0,
- context=context)
- mail_values['attachment_ids'] = m2m_attachment_ids
- if not mail_values.get('reply_to'):
- mail_values['reply_to'] = mail_values['email_from']
- self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)
- else:
- subtype = 'mail.mt_comment'
- if is_log: # log a note: subtype is False
- subtype = False
- elif mass_mail_mode: # mass mail: is a log pushed to recipients unless specified, author not added
- if not wizard.notify:
+ sliced_res_ids = [res_ids[i:i + self._batch_size] for i in range(0, len(res_ids), self._batch_size)]
+ for res_ids in sliced_res_ids:
+ all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context)
+ for res_id, mail_values in all_mail_values.iteritems():
+ if wizard.composition_mode == 'mass_mail':
+ self.pool['mail.mail'].create(cr, uid, mail_values, context=context)
+ else:
+ subtype = 'mail.mt_comment'
+ if context.get('mail_compose_log') or (wizard.composition_mode == 'mass_post' and not wizard.notify): # log a note: subtype is False
subtype = False
- context = dict(context,
- mail_notify_force_send=False, # do not send emails directly but use the queue instead
- mail_create_nosubscribe=True) # add context key to avoid subscribing the author
- active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values)
+ if wizard.composition_mode == 'mass_post':
+ context = dict(context,
+ mail_notify_force_send=False, # do not send emails directly but use the queue instead
+ mail_create_nosubscribe=True) # add context key to avoid subscribing the author
+ active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values)
return {'type': 'ir.actions.act_window_close'}
@@ -289,6 +245,7 @@ class mail_compose_message(osv.TransientModel):
"""Generate the values that will be used by send_mail to create mail_messages
or mail_mails. """
results = dict.fromkeys(res_ids, False)
+ rendered_values, default_recipients = {}, {}
mass_mail_mode = wizard.composition_mode == 'mass_mail'
# render all template-based value at once
@@ -303,40 +260,46 @@ class mail_compose_message(osv.TransientModel):
'parent_id': wizard.parent_id and wizard.parent_id.id,
'partner_ids': [partner.id for partner in wizard.partner_ids],
'attachment_ids': [attach.id for attach in wizard.attachment_ids],
+ 'author_id': wizard.author_id.id,
+ 'email_from': wizard.email_from,
+ 'record_name': wizard.record_name,
}
# mass mailing: rendering override wizard static values
if mass_mail_mode and wizard.model:
+ # always keep a copy, reset record name (avoid browsing records)
+ mail_values.update(notification=True, model=wizard.model, res_id=res_id, record_name=False)
+ # auto deletion of mail_mail
+ if 'mail_auto_delete' in context:
+ mail_values['auto_delete'] = context.get('mail_auto_delete')
+ # rendered values using template
email_dict = rendered_values[res_id]
mail_values['partner_ids'] += email_dict.pop('partner_ids', [])
+ mail_values.update(email_dict)
+ if wizard.same_thread:
+ mail_values.pop('reply_to')
+ elif not mail_values.get('reply_to'):
+ mail_values['reply_to'] = mail_values['email_from']
+ # mail_mail values: body -> body_html, partner_ids -> recipient_ids
+ mail_values['body_html'] = mail_values.get('body', '')
+ mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])]
+
# process attachments: should not be encoded before being processed by message_post / mail_mail create
- attachments = []
- if email_dict.get('attachments'):
- for name, enc_cont in email_dict.pop('attachments'):
- attachments.append((name, base64.b64decode(enc_cont)))
- mail_values['attachments'] = attachments
+ mail_values['attachments'] = [(name, base64.b64decode(enc_cont)) for name, enc_cont in email_dict.pop('attachments', list())]
attachment_ids = []
for attach_id in mail_values.pop('attachment_ids'):
new_attach_id = self.pool.get('ir.attachment').copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context)
attachment_ids.append(new_attach_id)
- mail_values['attachment_ids'] = attachment_ids
- # email_from: mass mailing only can specify another email_from
- if email_dict.get('email_from'):
- mail_values['email_from'] = email_dict.pop('email_from')
- # replies redirection: mass mailing only
- if wizard.same_thread and wizard.post:
- email_dict.pop('reply_to', None)
- else:
- mail_values['reply_to'] = email_dict.pop('reply_to', None)
- mail_values.update(email_dict)
- # mass mailing without post: mail_mail values
- if mass_mail_mode and not wizard.post:
- if 'mail_auto_delete' in context:
- mail_values['auto_delete'] = context.get('mail_auto_delete')
- mail_values['body_html'] = mail_values.get('body', '')
- mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])]
+ mail_values['attachment_ids'] = self.pool['mail.thread']._message_preprocess_attachments(
+ cr, uid, mail_values.pop('attachments', []),
+ attachment_ids, 'mail.message', 0, context=context)
+
results[res_id] = mail_values
return results
+ #------------------------------------------------------
+ # Template rendering
+ #------------------------------------------------------
+
def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
"""Generate template-based values of wizard, for the document records given
by res_ids. This method is meant to be inherited by email_template that
@@ -346,6 +309,10 @@ class mail_compose_message(osv.TransientModel):
once, and render it multiple times. This is useful for mass mailing where
template rendering represent a significant part of the process.
+ Default recipients are also computed, based on mail_thread method
+ message_get_default_recipients. This allows to ensure a mass mailing has
+ always some recipients specified.
+
:param browse wizard: current mail.compose.message browse record
:param list res_ids: list of record ids
@@ -357,6 +324,9 @@ class mail_compose_message(osv.TransientModel):
emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context=context)
replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context=context)
+ ctx = dict(context, thread_model=wizard.model)
+ default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx)
+
results = dict.fromkeys(res_ids, False)
for res_id in res_ids:
results[res_id] = {
@@ -365,6 +335,7 @@ class mail_compose_message(osv.TransientModel):
'email_from': emails_from[res_id],
'reply_to': replies_to[res_id],
}
+ results[res_id].update(default_recipients.get(res_id, dict()))
return results
def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False):
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index 23931649402..5b6bfc50439 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -11,6 +11,7 @@
+
@@ -28,29 +29,27 @@
-
-
-
-
- Followers of
-
- and
+
+
+
+
+ Email mass mailing on
+ the selected records
+ the current search filter .
+ Followers of the document and
+ context="{'force_email':True, 'show_email':True}"
+ attrs="{'invisible': [('composition_mode', '!=', 'comment')]}"/>
-
-
+
-
-
+ attrs="{'invisible':['|', ('composition_mode', '!=', 'mass_post')]}"/>
+
+
+
diff --git a/addons/marketing/__init__.py b/addons/marketing/__init__.py
index ff0c032da87..4a36da0182f 100644
--- a/addons/marketing/__init__.py
+++ b/addons/marketing/__init__.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2010 Tiny SPRL ().
+# Copyright (C) 2004-TODAY OpenERP SA (http://www.openerp.com)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -15,7 +15,7 @@
# 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 .
+# along with this program. If not, see .
#
##############################################################################
diff --git a/addons/marketing/__openerp__.py b/addons/marketing/__openerp__.py
index e46c58b9959..696a51662ab 100644
--- a/addons/marketing/__openerp__.py
+++ b/addons/marketing/__openerp__.py
@@ -1,29 +1,9 @@
# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2010 Tiny SPRL ().
-#
-# 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 .
-#
-##############################################################################
-
{
'name': 'Marketing',
'version': '1.1',
- 'depends': ['base', 'base_setup', 'crm'],
+ 'depends': ['base', 'base_setup'],
'author': 'OpenERP SA',
'category': 'Hidden/Dependency',
'description': """
@@ -35,7 +15,6 @@ Contains the installer for marketing-related modules.
'website': 'http://www.openerp.com',
'data': [
'security/marketing_security.xml',
- 'security/ir.model.access.csv',
'marketing_view.xml',
'res_config_view.xml',
],
@@ -44,4 +23,3 @@ Contains the installer for marketing-related modules.
'auto_install': False,
'images': ['images/config_marketing.jpeg'],
}
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/marketing/marketing_view.xml b/addons/marketing/marketing_view.xml
index 88ef93916b6..21254a9b578 100644
--- a/addons/marketing/marketing_view.xml
+++ b/addons/marketing/marketing_view.xml
@@ -3,39 +3,12 @@
-
+
-
- crm.lead.inherit.form
- crm.lead
-
-
-
- Marketing
-
-
-
-
-
-
-
-
+
+
-
- crm.lead.inherit.form
- crm.lead
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/marketing/res_config.py b/addons/marketing/res_config.py
index a58a5c56fc7..6e3e1b9d69b 100644
--- a/addons/marketing/res_config.py
+++ b/addons/marketing/res_config.py
@@ -1,40 +1,19 @@
# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Business Applications
-# Copyright (C) 2004-2012 OpenERP S.A. ().
-#
-# 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 openerp.osv import fields, osv
-class marketing_config_settings(osv.osv_memory):
+
+class marketing_config_settings(osv.TransientModel):
_name = 'marketing.config.settings'
_inherit = 'res.config.settings'
_columns = {
- 'module_marketing_campaign': fields.boolean('Marketing campaigns',
+ 'module_mass_mailing': fields.boolean(
+ 'Mass Mailing',
+ help='Provide a way to perform mass mailings.\n'
+ '-This installs the module mass_mailing.'),
+ 'module_marketing_campaign': fields.boolean(
+ 'Marketing campaigns',
help='Provides leads automation through marketing campaigns. '
'Campaigns can in fact be defined on any resource, not just CRM leads.\n'
'-This installs the module marketing_campaign.'),
- 'module_marketing_campaign_crm_demo': fields.boolean('Demo data for marketing campaigns',
- help='Installs demo data like leads, campaigns and segments for Marketing Campaigns.\n'
- '-This installs the module marketing_campaign_crm_demo.'),
- 'module_crm_profiling': fields.boolean('Track customer profile to focus your campaigns',
- help='Allows users to perform segmentation within partners.\n'
- '-This installs the module crm_profiling.'),
}
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/marketing/res_config_view.xml b/addons/marketing/res_config_view.xml
index 78144843f82..d241fa41513 100644
--- a/addons/marketing/res_config_view.xml
+++ b/addons/marketing/res_config_view.xml
@@ -11,24 +11,25 @@
or
-
-
+
-
+
-
+
+
+
+
+
diff --git a/addons/marketing/security/ir.model.access.csv b/addons/marketing/security/ir.model.access.csv
deleted file mode 100644
index 4a08a4aa60d..00000000000
--- a/addons/marketing/security/ir.model.access.csv
+++ /dev/null
@@ -1 +0,0 @@
-id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
diff --git a/addons/hr_expense/report/expense.py b/addons/marketing_crm/__init__.py
similarity index 62%
rename from addons/hr_expense/report/expense.py
rename to addons/marketing_crm/__init__.py
index a6898204741..e2776a1c439 100644
--- a/addons/hr_expense/report/expense.py
+++ b/addons/marketing_crm/__init__.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2010 Tiny SPRL (
).
+# Copyright (C) 2004-TODAY OpenERP SA (http://www.openerp.com)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -15,23 +15,8 @@
# 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 .
+# along with this program. If not, see .
#
##############################################################################
-import datetime
-import time
-
-from openerp.report import report_sxw
-
-class expense(report_sxw.rml_parse):
-
- def __init__(self, cr, uid, name, context):
- super(expense, self).__init__(cr, uid, name, context=context)
- self.localcontext.update({'time': time, })
-
-report_sxw.report_sxw('report.hr.expense', 'hr.expense.expense', 'addons/hr_expense/report/expense.rml',parser=expense)
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
+import models
diff --git a/addons/marketing_crm/__openerp__.py b/addons/marketing_crm/__openerp__.py
new file mode 100644
index 00000000000..c43463855b5
--- /dev/null
+++ b/addons/marketing_crm/__openerp__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Marketing in CRM',
+ 'version': '1.0',
+ 'depends': ['marketing', 'crm'],
+ 'author': 'OpenERP SA',
+ 'category': 'Hidden/Dependency',
+ 'description': """
+Bridge module between marketing and CRM
+ """,
+ 'website': 'http://www.openerp.com',
+ 'data': [
+ 'views/crm.xml',
+ 'views/res_config.xml',
+ ],
+ 'demo': [],
+ 'installable': True,
+ 'auto_install': True,
+}
diff --git a/addons/marketing_crm/models/__init__.py b/addons/marketing_crm/models/__init__.py
new file mode 100644
index 00000000000..7eb689fa199
--- /dev/null
+++ b/addons/marketing_crm/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import res_config
diff --git a/addons/marketing_crm/models/res_config.py b/addons/marketing_crm/models/res_config.py
new file mode 100644
index 00000000000..ffb5a678a96
--- /dev/null
+++ b/addons/marketing_crm/models/res_config.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import fields, osv
+
+
+class CrmMarketingConfig(osv.TransientModel):
+ _name = 'marketing.config.settings'
+ _inherit = 'marketing.config.settings'
+
+ _columns = {
+ 'module_marketing_campaign_crm_demo': fields.boolean(
+ 'Demo data for marketing campaigns',
+ help='Installs demo data like leads, campaigns and segments for Marketing Campaigns.\n'
+ '-This installs the module marketing_campaign_crm_demo.'),
+ 'module_crm_profiling': fields.boolean(
+ 'Track customer profile to focus your campaigns',
+ help='Allows users to perform segmentation within partners.\n'
+ '-This installs the module crm_profiling.'),
+ }
diff --git a/addons/marketing_crm/views/crm.xml b/addons/marketing_crm/views/crm.xml
new file mode 100644
index 00000000000..1e4c7840db4
--- /dev/null
+++ b/addons/marketing_crm/views/crm.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+ crm.lead.inherit.form
+ crm.lead
+
+
+
+ Marketing
+
+
+
+
+
+
+
+
+
+
+ crm.lead.inherit.form
+ crm.lead
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/marketing_crm/views/res_config.xml b/addons/marketing_crm/views/res_config.xml
new file mode 100644
index 00000000000..27f98705232
--- /dev/null
+++ b/addons/marketing_crm/views/res_config.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ marketing.config.settings.crm
+ marketing.config.settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing/__init__.py b/addons/mass_mailing/__init__.py
index 9dadc456842..175383c6644 100644
--- a/addons/mass_mailing/__init__.py
+++ b/addons/mass_mailing/__init__.py
@@ -1,26 +1,5 @@
# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2013-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 mass_mailing
-import mail_mail
-import mail_thread
+import models
import wizard
import controllers
diff --git a/addons/mass_mailing/__openerp__.py b/addons/mass_mailing/__openerp__.py
index 92f8b1fc3e8..91b57ac5b5a 100644
--- a/addons/mass_mailing/__openerp__.py
+++ b/addons/mass_mailing/__openerp__.py
@@ -21,26 +21,33 @@
{
'name': 'Mass Mailing Campaigns',
+ 'summary': 'Design, send and track emails',
'description': """
Easily send mass mailing to your leads, opportunities or customers. Track
marketing campaigns performance to improve conversion rates. Design
professional emails and reuse templates in a few clicks.
""",
- 'version': '1.0',
+ 'version': '2.0',
'author': 'OpenERP',
'website': 'http://www.openerp.com',
'category': 'Marketing',
'depends': [
'mail',
'email_template',
+ 'marketing',
'web_kanban_gauge',
'web_kanban_sparkline',
+ 'website_mail',
],
'data': [
- 'mail_data.xml',
+ 'data/mail_data.xml',
+ 'data/mass_mailing_data.xml',
'wizard/mail_compose_message_view.xml',
- 'wizard/mail_mass_mailing_create_segment.xml',
- 'mass_mailing_view.xml',
+ 'wizard/test_mailing.xml',
+ 'views/mass_mailing.xml',
+ 'views/res_config.xml',
+ 'views/res_partner.xml',
+ 'views/email_template.xml',
'security/ir.model.access.csv',
],
'js': [
@@ -48,10 +55,11 @@ professional emails and reuse templates in a few clicks.
],
'qweb': [],
'css': [
- 'static/src/css/mass_mailing.css'
+ 'static/src/css/mass_mailing.css',
+ 'static/src/css/email_template.css'
],
'demo': [
- 'mass_mailing_demo.xml',
+ 'data/mass_mailing_demo.xml',
],
'installable': True,
'auto_install': False,
diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py
index 046084621c7..7396c02d453 100644
--- a/addons/mass_mailing/controllers/main.py
+++ b/addons/mass_mailing/controllers/main.py
@@ -1,11 +1,42 @@
+import werkzeug
+
from openerp import http, SUPERUSER_ID
from openerp.http import request
+
class MassMailController(http.Controller):
+
@http.route('/mail/track//blank.gif', type='http', auth='none')
- def track_mail_open(self, mail_id):
+ def track_mail_open(self, mail_id, **post):
""" Email tracking. """
mail_mail_stats = request.registry.get('mail.mail.statistics')
mail_mail_stats.set_opened(request.cr, SUPERUSER_ID, mail_mail_ids=[mail_id])
- return "data:image/gif;base64,R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
+ response = werkzeug.wrappers.Response()
+ response.mimetype = 'image/gif'
+ response.set_data('R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='.decode('base64'))
+ return response
+
+ @http.route(['/mail/mailing//unsubscribe'], type='http', auth='none')
+ def mailing(self, mailing_id, email=None, res_id=None, **post):
+ cr, uid, context = request.cr, request.uid, request.context
+ MassMailing = request.registry['mail.mass_mailing']
+ mailing_ids = MassMailing.exists(cr, SUPERUSER_ID, [mailing_id], context=context)
+ if not mailing_ids:
+ return 'KO'
+ mailing = MassMailing.browse(cr, SUPERUSER_ID, mailing_ids[0], context=context)
+ if mailing.mailing_model == 'mail.mass_mailing.contact':
+ list_ids = [l.id for l in mailing.contact_list_ids]
+ record_ids = request.registry[mailing.mailing_model].search(cr, SUPERUSER_ID, [('list_id', 'in', list_ids), ('id', '=', res_id), ('email', 'ilike', email)], context=context)
+ request.registry[mailing.mailing_model].write(cr, SUPERUSER_ID, record_ids, {'opt_out': True}, context=context)
+ else:
+ email_fname = None
+ if 'email_from' in request.registry[mailing.mailing_model]._all_columns:
+ email_fname = 'email_from'
+ elif 'email' in request.registry[mailing.mailing_model]._all_columns:
+ email_fname = 'email'
+ if email_fname:
+ record_ids = request.registry[mailing.mailing_model].search(cr, SUPERUSER_ID, [('id', '=', res_id), (email_fname, 'ilike', email)], context=context)
+ if 'opt_out' in request.registry[mailing.mailing_model]._all_columns:
+ request.registry[mailing.mailing_model].write(cr, SUPERUSER_ID, record_ids, {'opt_out': True}, context=context)
+ return 'OK'
diff --git a/addons/mass_mailing/mail_data.xml b/addons/mass_mailing/data/mail_data.xml
similarity index 100%
rename from addons/mass_mailing/mail_data.xml
rename to addons/mass_mailing/data/mail_data.xml
diff --git a/addons/mass_mailing/data/mass_mailing_data.xml b/addons/mass_mailing/data/mass_mailing_data.xml
new file mode 100644
index 00000000000..6bd1df79a72
--- /dev/null
+++ b/addons/mass_mailing/data/mass_mailing_data.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+ Manage Mass Mailing Campaigns
+
+
+
+
+
+ Schedule
+ 10
+
+
+ Design
+ 20
+
+
+ Sent
+ 30
+
+
+
+
\ No newline at end of file
diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml
new file mode 100644
index 00000000000..569feec9c88
--- /dev/null
+++ b/addons/mass_mailing/data/mass_mailing_demo.xml
@@ -0,0 +1,146 @@
+
+
+
+
+
+ bWlncmF0aW9uIHRlc3Q=
+ SampleDoc.doc
+ SampleDoc.doc
+
+
+
+
+ Imported Contacts
+
+
+
+
+ Aristide Antario
+ aa@example.com
+
+
+
+ Beverly Bridge
+ bb@example.com
+
+
+
+ Carol Cartridge
+ cc@example.com
+
+
+
+
+
+
+ Marketing
+
+
+ Newsletter
+
+
+
+
+
+
+ First Newsletter
+ done
+
+
+ res.partner
+ [('customer', '=', True)]
+ email
+ ]]>
+
+
+
+
+
+ A Punchy Headline
+
+
+
+
+
+
+
+
+
+ A Small Subtitle for ${object.name}
+
+
+
+ Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Feature One
+
+ Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.
+
+
+ Feature Two
+
+ Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.
+
+
+
+
+
]]>
+
+
+
+ Second Newsletter
+ test
+
+ res.partner
+ [('customer', '=', True)]
+ email
+ ]]>
+
+
+
+
+ 1111000@OpenERP.com
+
+
+
+
+
+
+ 1111001@OpenERP.com
+
+
+
+
+
+
+ 1111002@OpenERP.com
+
+
+
+
+
+ 1111003@OpenERP.com
+
+
+
+
+ 1111004@OpenERP.com
+
+
+
+
+
+
diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py
deleted file mode 100644
index e7583d8e739..00000000000
--- a/addons/mass_mailing/mass_mailing.py
+++ /dev/null
@@ -1,369 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2013-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 datetime import datetime
-from dateutil import relativedelta
-
-from openerp import tools
-from openerp.tools.translate import _
-from openerp.osv import osv, fields
-
-
-class MassMailingCampaign(osv.Model):
- """Model of mass mailing campaigns.
- """
- _name = "mail.mass_mailing.campaign"
- _description = 'Mass Mailing Campaign'
- # number of embedded mailings in kanban view
- _kanban_mailing_nbr = 4
-
- def _get_statistics(self, cr, uid, ids, name, arg, context=None):
- """ Compute statistics of the mass mailing campaign """
- results = dict.fromkeys(ids, False)
- for campaign in self.browse(cr, uid, ids, context=context):
- results[campaign.id] = {
- 'sent': len(campaign.statistics_ids),
- # delivered: shouldn't be: all mails - (failed + bounced) ?
- 'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]), # stat.state == 'sent' and
- 'opened': len([stat for stat in campaign.statistics_ids if stat.opened]),
- 'replied': len([stat for stat in campaign.statistics_ids if stat.replied]),
- 'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]),
- }
- return results
-
- def _get_mass_mailing_kanban_ids(self, cr, uid, ids, name, arg, context=None):
- """ Gather data about mass mailings to display them in kanban view as
- nested kanban views is not possible currently. """
- results = dict.fromkeys(ids, '')
- for campaign in self.browse(cr, uid, ids, context=context):
- mass_mailing_results = []
- for mass_mailing in campaign.mass_mailing_ids[:self._kanban_mailing_nbr]:
- mass_mailing_object = {}
- for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']:
- mass_mailing_object[attr] = getattr(mass_mailing, attr)
- mass_mailing_results.append(mass_mailing_object)
- results[campaign.id] = mass_mailing_results
- return results
-
- _columns = {
- 'name': fields.char(
- 'Campaign Name', required=True,
- ),
- 'user_id': fields.many2one(
- 'res.users', 'Responsible',
- required=True,
- ),
- 'mass_mailing_ids': fields.one2many(
- 'mail.mass_mailing', 'mass_mailing_campaign_id',
- 'Mass Mailings',
- ),
- 'mass_mailing_kanban_ids': fields.function(
- _get_mass_mailing_kanban_ids,
- type='text', string='Mass Mailings (kanban data)',
- help='This field has for purpose to gather data about mass mailings '
- 'to display them in kanban view as nested kanban views is not '
- 'possible currently',
- ),
- 'statistics_ids': fields.one2many(
- 'mail.mail.statistics', 'mass_mailing_campaign_id',
- 'Sent Emails',
- ),
- 'color': fields.integer('Color Index'),
- # stat fields
- 'sent': fields.function(
- _get_statistics,
- string='Sent Emails',
- type='integer', multi='_get_statistics'
- ),
- 'delivered': fields.function(
- _get_statistics,
- string='Delivered',
- type='integer', multi='_get_statistics',
- ),
- 'opened': fields.function(
- _get_statistics,
- string='Opened',
- type='integer', multi='_get_statistics',
- ),
- 'replied': fields.function(
- _get_statistics,
- string='Replied',
- type='integer', multi='_get_statistics'
- ),
- 'bounced': fields.function(
- _get_statistics,
- string='Bounced',
- type='integer', multi='_get_statistics'
- ),
- }
-
- _defaults = {
- 'user_id': lambda self, cr, uid, ctx=None: uid,
- }
-
- def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None):
- ctx = dict(context)
- ctx.update({
- 'default_mass_mailing_campaign_id': ids[0],
- })
- return {
- 'name': _('Create a Mass Mailing for the Campaign'),
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'mail.mass_mailing.create',
- 'views': [(False, 'form')],
- 'view_id': False,
- 'target': 'new',
- 'context': ctx,
- }
-
-
-class MassMailing(osv.Model):
- """ MassMailing models a wave of emails for a mass mailign campaign.
- A mass mailing is an occurence of sending emails. """
-
- _name = 'mail.mass_mailing'
- _description = 'Wave of sending emails'
- # number of periods for tracking mail_mail statistics
- _period_number = 6
- _order = 'date DESC'
-
- def __get_bar_values(self, cr, uid, id, obj, domain, read_fields, value_field, groupby_field, context=None):
- """ Generic method to generate data for bar chart values using SparklineBarWidget.
- This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
-
- :param obj: the target model (i.e. crm_lead)
- :param domain: the domain applied to the read_group
- :param list read_fields: the list of fields to read in the read_group
- :param str value_field: the field used to compute the value of the bar slice
- :param str groupby_field: the fields used to group
-
- :return list section_result: a list of dicts: [
- { 'value': (int) bar_column_value,
- 'tootip': (str) bar_column_tooltip,
- }
- ]
- """
- date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
- section_result = [{'value': 0,
- 'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
- } for i in range(0, self._period_number)]
- group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
- field_col_info = obj._all_columns.get(groupby_field.split(':')[0])
- pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
- for group in group_obj:
- group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date()
- timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
- section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
- return section_result
-
- def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
- """ Get the daily statistics of the mass mailing. This is done by a grouping
- on opened and replied fields. Using custom format in context, we obtain
- results for the next 6 days following the mass mailing date. """
- obj = self.pool['mail.mail.statistics']
- res = {}
- for id in ids:
- res[id] = {}
- date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
- date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
- date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
- date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
- domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
- res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened:day', context=context)
- domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
- res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied:day', context=context)
- return res
-
- def _get_statistics(self, cr, uid, ids, name, arg, context=None):
- """ Compute statistics of the mass mailing campaign """
- results = dict.fromkeys(ids, False)
- for mass_mailing in self.browse(cr, uid, ids, context=context):
- results[mass_mailing.id] = {
- 'sent': len(mass_mailing.statistics_ids),
- 'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]), # mail.state == 'sent' and
- 'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]),
- 'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]),
- 'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]),
- }
- return results
-
- _columns = {
- 'name': fields.char('Name', required=True),
- 'mass_mailing_campaign_id': fields.many2one(
- 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
- ondelete='cascade', required=True,
- ),
- 'template_id': fields.many2one(
- 'email.template', 'Email Template',
- ondelete='set null',
- ),
- 'domain': fields.char('Domain'),
- 'date': fields.datetime('Date'),
- 'color': fields.related(
- 'mass_mailing_campaign_id', 'color',
- type='integer', string='Color Index',
- ),
- # statistics data
- 'statistics_ids': fields.one2many(
- 'mail.mail.statistics', 'mass_mailing_id',
- 'Emails Statistics',
- ),
- 'sent': fields.function(
- _get_statistics,
- string='Sent Emails',
- type='integer', multi='_get_statistics'
- ),
- 'delivered': fields.function(
- _get_statistics,
- string='Delivered',
- type='integer', multi='_get_statistics',
- ),
- 'opened': fields.function(
- _get_statistics,
- string='Opened',
- type='integer', multi='_get_statistics',
- ),
- 'replied': fields.function(
- _get_statistics,
- string='Replied',
- type='integer', multi='_get_statistics'
- ),
- 'bounced': fields.function(
- _get_statistics,
- string='Bounce',
- type='integer', multi='_get_statistics'
- ),
- # monthly ratio
- 'opened_monthly': fields.function(
- _get_daily_statistics,
- string='Opened',
- type='char', multi='_get_daily_statistics',
- ),
- 'replied_monthly': fields.function(
- _get_daily_statistics,
- string='Replied',
- type='char', multi='_get_daily_statistics',
- ),
- }
-
- _defaults = {
- 'date': fields.datetime.now,
- }
-
-
-class MailMailStats(osv.Model):
- """ MailMailStats models the statistics collected about emails. Those statistics
- are stored in a separated model and table to avoid bloating the mail_mail table
- with statistics values. This also allows to delete emails send with mass mailing
- without loosing the statistics about them. """
-
- _name = 'mail.mail.statistics'
- _description = 'Email Statistics'
- _rec_name = 'message_id'
- _order = 'message_id'
-
- _columns = {
- 'mail_mail_id': fields.integer(
- 'Mail ID',
- help='ID of the related mail_mail. This field is an integer field because'
- 'the related mail_mail can be deleted separately from its statistics.'
- ),
- 'message_id': fields.char(
- 'Message-ID',
- ),
- 'model': fields.char(
- 'Document model',
- ),
- 'res_id': fields.integer(
- 'Document ID',
- ),
- # campaign / wave data
- 'mass_mailing_id': fields.many2one(
- 'mail.mass_mailing', 'Mass Mailing',
- ondelete='set null',
- ),
- 'mass_mailing_campaign_id': fields.related(
- 'mass_mailing_id', 'mass_mailing_campaign_id',
- type='many2one', ondelete='set null',
- relation='mail.mass_mailing.campaign',
- string='Mass Mailing Campaign',
- store=True, readonly=True,
- ),
- 'template_id': fields.related(
- 'mass_mailing_id', 'template_id',
- type='many2one', ondelete='set null',
- relation='email.template',
- string='Email Template',
- store=True, readonly=True,
- ),
- # Bounce and tracking
- 'opened': fields.datetime(
- 'Opened',
- help='Date when this email has been opened for the first time.'),
- 'replied': fields.datetime(
- 'Replied',
- help='Date when this email has been replied for the first time.'),
- 'bounced': fields.datetime(
- 'Bounced',
- help='Date when this email has bounced.'
- ),
- }
-
- def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
- """ Set as opened """
- if not ids and mail_mail_ids:
- ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
- elif not ids and mail_message_ids:
- ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
- else:
- ids = []
- for stat in self.browse(cr, uid, ids, context=context):
- if not stat.opened:
- self.write(cr, uid, [stat.id], {'opened': fields.datetime.now()}, context=context)
- return ids
-
- def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
- """ Set as replied """
- if not ids and mail_mail_ids:
- ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
- elif not ids and mail_message_ids:
- ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
- else:
- ids = []
- for stat in self.browse(cr, uid, ids, context=context):
- if not stat.replied:
- self.write(cr, uid, [stat.id], {'replied': fields.datetime.now()}, context=context)
- return ids
-
- def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
- """ Set as bounced """
- if not ids and mail_mail_ids:
- ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
- elif not ids and mail_message_ids:
- ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
- else:
- ids = []
- for stat in self.browse(cr, uid, ids, context=context):
- if not stat.bounced:
- self.write(cr, uid, [stat.id], {'bounced': fields.datetime.now()}, context=context)
- return ids
diff --git a/addons/mass_mailing/mass_mailing_demo.xml b/addons/mass_mailing/mass_mailing_demo.xml
deleted file mode 100644
index 5a2388846e4..00000000000
--- a/addons/mass_mailing/mass_mailing_demo.xml
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
-
-
-
- Partner Newsletter 1
-
-
- ${object.id}
- Hello]]>
-
-
- Partner Newsletter 2
-
-
- ${object.id}
- Hello]]>
-
-
-
- Partners Newsletter
-
-
-
-
- First Newsletter
-
-
-
-
-
- Second Newsletter
-
-
-
-
-
-
-
- 1111000@OpenERP.com
-
-
-
-
-
- 1111001@OpenERP.com
-
-
-
-
-
- 1111002@OpenERP.com
-
-
-
-
- 1111003@OpenERP.com
-
-
-
- 1111004@OpenERP.com
-
-
-
-
-
- 1111005@OpenERP.com
-
-
-
-
- 1111006@OpenERP.com
-
-
-
-
- 1111007@OpenERP.com
-
-
-
-
diff --git a/addons/mass_mailing/mass_mailing_view.xml b/addons/mass_mailing/mass_mailing_view.xml
deleted file mode 100644
index e8848fba44f..00000000000
--- a/addons/mass_mailing/mass_mailing_view.xml
+++ /dev/null
@@ -1,379 +0,0 @@
-
-
-
-
-
-
- mail.mass_mailing.search
- mail.mass_mailing
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- mail.mass_mailing.tree
- mail.mass_mailing
- 10
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- mail.mass_mailing.form
- mail.mass_mailing
-
-
-
-
-
-
- mail.mass_mailing.kanban
- mail.mass_mailing
-
-
-
-
-
-
-
-
-
-
-
-
- Sent:
- Campaign:
-
-
-
-
-
- Sent
-
-
-
- Delivered
-
-
-
- Opened
-
-
-
- Replied
-
-
-
-
-
Opened
-
-
-
-
Replied
-
-
-
-
-
-
-
-
-
-
-
-
-
- Mass Mailings
- mail.mass_mailing
- form
- kanban,tree,form
-
-
-
- Mass Mailings
- mail.mass_mailing
- form
- kanban,tree,form
- {
- 'search_default_mass_mailing_campaign_id': [active_id],
- 'default_mass_mailing_campaign_id': active_id,
- }
-
-
-
-
-
- mail.mass_mailing.campaign.search
- mail.mass_mailing.campaign
-
-
-
-
-
-
-
-
-
-
-
-
- mail.mass_mailing.campaign.tree
- mail.mass_mailing.campaign
- 10
-
-
-
-
-
-
-
-
-
- mail.mass_mailing.campaign.form
- mail.mass_mailing.campaign
-
-
-
-
-
-
- mail.mass_mailing.campaign.kanban
- mail.mass_mailing.campaign
-
-
-
-
-
-
-
-
-
-
-
-
- Sent
-
-
-
- Delivered
-
-
-
- Opened
-
-
-
- Replied
-
-
-
-
-
-
-
-
-
-
-
-
-
- Mass Mailing Campaigns
- mail.mass_mailing.campaign
- form
- kanban,tree,form
-
-
- Click to define a new mass mailing campaign.
-
- Create a campaign to structure mass mailing and get analysis from email status.
-
-
-
-
-
-
- mail.mail.statistics.search
- mail.mail.statistics
-
-
-
-
-
-
-
-
-
- mail.mail.statistics.tree
- mail.mail.statistics
-
-
-
-
-
-
-
-
-
-
-
-
- mail.mail.statistics.form
- mail.mail.statistics
-
-
-
-
-
-
- Mail Statistics
- mail.mail.statistics
- form
- tree,form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/mass_mailing/models/__init__.py b/addons/mass_mailing/models/__init__.py
new file mode 100644
index 00000000000..2cffe48642b
--- /dev/null
+++ b/addons/mass_mailing/models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+import mass_mailing
+import mass_mailing_stats
+import mail_mail
+import mail_thread
+import res_config
diff --git a/addons/mass_mailing/mail_mail.py b/addons/mass_mailing/models/mail_mail.py
similarity index 57%
rename from addons/mass_mailing/mail_mail.py
rename to addons/mass_mailing/models/mail_mail.py
index 917334bdda4..a67f88a2a28 100644
--- a/addons/mass_mailing/mail_mail.py
+++ b/addons/mass_mailing/models/mail_mail.py
@@ -19,7 +19,8 @@
#
##############################################################################
-from urlparse import urljoin
+import urllib
+import urlparse
from openerp import tools
from openerp import SUPERUSER_ID
@@ -32,6 +33,7 @@ class MailMail(osv.Model):
_inherit = ['mail.mail']
_columns = {
+ 'mailing_id': fields.many2one('mail.mass_mailing', 'Mass Mailing'),
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mail_mail_id',
string='Statistics',
@@ -50,9 +52,24 @@ class MailMail(osv.Model):
def _get_tracking_url(self, cr, uid, mail, partner=None, context=None):
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
- track_url = urljoin(base_url, 'mail/track/%d/blank.gif' % mail.id)
+ track_url = urlparse.urljoin(
+ base_url, 'mail/track/%(mail_id)s/blank.gif?%(params)s' % {
+ 'mail_id': mail.id,
+ 'params': urllib.urlencode({'db': cr.dbname})
+ }
+ )
return ' ' % track_url
+ def _get_unsubscribe_url(self, cr, uid, mail, email_to, msg=None, context=None):
+ base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
+ url = urlparse.urljoin(
+ base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
+ 'mailing_id': mail.mailing_id.id,
+ 'params': urllib.urlencode({'db': cr.dbname, 'res_id': mail.res_id, 'email': email_to})
+ }
+ )
+ return '%s ' % (url, msg or 'Click to unsubscribe')
+
def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
""" Override to add the tracking URL to the body. """
body = super(MailMail, self).send_get_mail_body(cr, uid, mail, partner=partner, context=context)
@@ -63,3 +80,19 @@ class MailMail(osv.Model):
if tracking_url:
body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
return body
+
+ def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
+ res = super(MailMail, self).send_get_email_dict(cr, uid, mail, partner, context=context)
+ if mail.mailing_id and res.get('body') and res.get('email_to'):
+ email_to = tools.email_split(res.get('email_to')[0])
+ unsubscribe_url = self._get_unsubscribe_url(cr, uid, mail, email_to, context=context)
+ if unsubscribe_url:
+ res['body'] = tools.append_content_to_html(res['body'], unsubscribe_url, plaintext=False, container_tag='p')
+ return res
+
+ def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
+ if mail_sent is True and mail.statistics_ids:
+ self.pool['mail.mail.statistics'].write(cr, uid, [s.id for s in mail.statistics_ids], {'sent': fields.datetime.now()}, context=context)
+ elif mail_sent is False and mail.statistics_ids:
+ self.pool['mail.mail.statistics'].write(cr, uid, [s.id for s in mail.statistics_ids], {'exception': fields.datetime.now()}, context=context)
+ return super(MailMail, self)._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)
diff --git a/addons/mass_mailing/mail_thread.py b/addons/mass_mailing/models/mail_thread.py
similarity index 99%
rename from addons/mass_mailing/mail_thread.py
rename to addons/mass_mailing/models/mail_thread.py
index a4ff15a1a3f..524341f88d7 100644
--- a/addons/mass_mailing/mail_thread.py
+++ b/addons/mass_mailing/models/mail_thread.py
@@ -29,7 +29,7 @@ from openerp.osv import osv
_logger = logging.getLogger(__name__)
-class MailThread(osv.Model):
+class MailThread(osv.AbstractModel):
""" Update MailThread to add the feature of bounced emails and replied emails
in message_process. """
_name = 'mail.thread'
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
new file mode 100644
index 00000000000..bd314f03a3d
--- /dev/null
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -0,0 +1,571 @@
+# -*- coding: utf-8 -*-
+
+from datetime import datetime
+from dateutil import relativedelta
+import json
+import random
+import urllib
+import urlparse
+
+from openerp import tools
+from openerp.tools.safe_eval import safe_eval as eval
+from openerp.tools.translate import _
+from openerp.osv import osv, fields
+
+
+class MassMailingCategory(osv.Model):
+ """Model of categories of mass mailing, i.e. marketing, newsletter, ... """
+ _name = 'mail.mass_mailing.category'
+ _description = 'Mass Mailing Category'
+ _order = 'name'
+
+ _columns = {
+ 'name': fields.char('Name', required=True),
+ }
+
+
+class MassMailingContact(osv.Model):
+ """Model of a contact. This model is different from the partner model
+ because it holds only some basic information: name, email. The purpose is to
+ be able to deal with large contact list to email without bloating the partner
+ base."""
+ _name = 'mail.mass_mailing.contact'
+ _description = 'Mass Mailing Contact'
+ _order = 'email'
+ _rec_name = 'email'
+
+ _columns = {
+ 'name': fields.char('Name'),
+ 'email': fields.char('Email', required=True),
+ 'create_date': fields.datetime('Create Date'),
+ 'list_id': fields.many2one(
+ 'mail.mass_mailing.list', string='Mailing List',
+ ondelete='cascade', required=True,
+ ),
+ 'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
+ }
+
+ def _get_latest_list(self, cr, uid, context={}):
+ lid = self.pool.get('mail.mass_mailing.list').search(cr, uid, [], limit=1, order='id desc', context=context)
+ return lid and lid[0] or False
+
+ _defaults = {
+ 'list_id': _get_latest_list
+ }
+
+ def name_create(self, cr, uid, name, context=None):
+ name, email = self.pool['res.partner']._parse_partner_name(name, context=context)
+ if name and not email:
+ email = name
+ if email and not name:
+ name = email
+ rec_id = self.create(cr, uid, {'name': name, 'email': email}, context=context)
+ return self.name_get(cr, uid, [rec_id], context)[0]
+
+
+class MassMailingList(osv.Model):
+ """Model of a contact list. """
+ _name = 'mail.mass_mailing.list'
+ _order = 'name'
+ _description = 'Mailing List'
+
+ def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None):
+ result = dict.fromkeys(ids, 0)
+ Contacts = self.pool.get('mail.mass_mailing.contact')
+ for group in Contacts.read_group(cr, uid, [('list_id', 'in', ids), ('opt_out', '!=', True)], ['list_id'], ['list_id'], context=context):
+ result[group['list_id'][0]] = group['list_id_count']
+ return result
+
+ _columns = {
+ 'name': fields.char('Mailing List', required=True),
+ 'contact_nbr': fields.function(
+ _get_contact_nbr, type='integer',
+ string='Number of Contacts',
+ ),
+ }
+
+
+class MassMailingStage(osv.Model):
+ """Stage for mass mailing campaigns. """
+ _name = 'mail.mass_mailing.stage'
+ _description = 'Mass Mailing Campaign Stage'
+ _order = 'sequence'
+
+ _columns = {
+ 'name': fields.char('Name', required=True, translate=True),
+ 'sequence': fields.integer('Sequence'),
+ }
+
+ _defaults = {
+ 'sequence': 0,
+ }
+
+
+class MassMailingCampaign(osv.Model):
+ """Model of mass mailing campaigns. """
+ _name = "mail.mass_mailing.campaign"
+ _description = 'Mass Mailing Campaign'
+
+ def _get_statistics(self, cr, uid, ids, name, arg, context=None):
+ """ Compute statistics of the mass mailing campaign """
+ Statistics = self.pool['mail.mail.statistics']
+ results = dict.fromkeys(ids, False)
+ for cid in ids:
+ stat_ids = Statistics.search(cr, uid, [('mass_mailing_campaign_id', '=', cid)], context=context)
+ stats = Statistics.browse(cr, uid, stat_ids, context=context)
+ results[cid] = {
+ 'total': len(stats),
+ 'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]),
+ 'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]),
+ 'sent': len([s for s in stats if not s.sent is False]),
+ 'opened': len([s for s in stats if not s.opened is False]),
+ 'replied': len([s for s in stats if not s.replied is False]),
+ 'bounced': len([s for s in stats if not s.bounced is False]),
+ }
+ results[cid]['delivered'] = results[cid]['sent'] - results[cid]['bounced']
+ results[cid]['received_ratio'] = 100.0 * results[cid]['delivered'] / (results[cid]['total'] or 1)
+ results[cid]['opened_ratio'] = 100.0 * results[cid]['opened'] / (results[cid]['total'] or 1)
+ results[cid]['replied_ratio'] = 100.0 * results[cid]['replied'] / (results[cid]['total'] or 1)
+ return results
+
+ _columns = {
+ 'name': fields.char('Name', required=True),
+ 'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
+ 'user_id': fields.many2one(
+ 'res.users', 'Responsible',
+ required=True,
+ ),
+ 'category_ids': fields.many2many(
+ 'mail.mass_mailing.category', 'mail_mass_mailing_category_rel',
+ 'category_id', 'campaign_id', string='Categories'),
+ 'mass_mailing_ids': fields.one2many(
+ 'mail.mass_mailing', 'mass_mailing_campaign_id',
+ 'Mass Mailings',
+ ),
+ 'unique_ab_testing': fields.boolean(
+ 'AB Testing',
+ help='If checked, recipients will be mailed only once, allowing to send'
+ 'various mailings in a single campaign to test the effectiveness'
+ 'of the mailings.'),
+ 'color': fields.integer('Color Index'),
+ # stat fields
+ 'total': fields.function(
+ _get_statistics, string='Total',
+ type='integer', multi='_get_statistics'
+ ),
+ 'scheduled': fields.function(
+ _get_statistics, string='Scheduled',
+ type='integer', multi='_get_statistics'
+ ),
+ 'failed': fields.function(
+ _get_statistics, string='Failed',
+ type='integer', multi='_get_statistics'
+ ),
+ 'sent': fields.function(
+ _get_statistics, string='Sent Emails',
+ type='integer', multi='_get_statistics'
+ ),
+ 'delivered': fields.function(
+ _get_statistics, string='Delivered',
+ type='integer', multi='_get_statistics',
+ ),
+ 'opened': fields.function(
+ _get_statistics, string='Opened',
+ type='integer', multi='_get_statistics',
+ ),
+ 'replied': fields.function(
+ _get_statistics, string='Replied',
+ type='integer', multi='_get_statistics'
+ ),
+ 'bounced': fields.function(
+ _get_statistics, string='Bounced',
+ type='integer', multi='_get_statistics'
+ ),
+ 'received_ratio': fields.function(
+ _get_statistics, string='Received Ratio',
+ type='integer', multi='_get_statistics',
+ ),
+ 'opened_ratio': fields.function(
+ _get_statistics, string='Opened Ratio',
+ type='integer', multi='_get_statistics',
+ ),
+ 'replied_ratio': fields.function(
+ _get_statistics, string='Replied Ratio',
+ type='integer', multi='_get_statistics',
+ ),
+ }
+
+ def _get_default_stage_id(self, cr, uid, context=None):
+ stage_ids = self.pool['mail.mass_mailing.stage'].search(cr, uid, [], limit=1, context=context)
+ return stage_ids and stage_ids[0] or False
+
+ _defaults = {
+ 'user_id': lambda self, cr, uid, ctx=None: uid,
+ 'stage_id': lambda self, *args: self._get_default_stage_id(*args),
+ }
+
+ def get_recipients(self, cr, uid, ids, model=None, context=None):
+ """Return the recipients of a mailing campaign. This is based on the statistics
+ build for each mailing. """
+ Statistics = self.pool['mail.mail.statistics']
+ res = dict.fromkeys(ids, False)
+ for cid in ids:
+ domain = [('mass_mailing_campaign_id', '=', cid)]
+ if model:
+ domain += [('model', '=', model)]
+ stat_ids = Statistics.search(cr, uid, domain, context=context)
+ res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context))
+ return res
+
+
+class MassMailing(osv.Model):
+ """ MassMailing models a wave of emails for a mass mailign campaign.
+ A mass mailing is an occurence of sending emails. """
+
+ _name = 'mail.mass_mailing'
+ _description = 'Mass Mailing'
+ # number of periods for tracking mail_mail statistics
+ _period_number = 6
+ _order = 'sent_date DESC'
+
+ def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, date_begin, context=None):
+ """ Generic method to generate data for bar chart values using SparklineBarWidget.
+ This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
+
+ :param obj: the target model (i.e. crm_lead)
+ :param domain: the domain applied to the read_group
+ :param list read_fields: the list of fields to read in the read_group
+ :param str value_field: the field used to compute the value of the bar slice
+ :param str groupby_field: the fields used to group
+
+ :return list section_result: a list of dicts: [
+ { 'value': (int) bar_column_value,
+ 'tootip': (str) bar_column_tooltip,
+ }
+ ]
+ """
+ date_begin = date_begin.date()
+ section_result = [{'value': 0,
+ 'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
+ } for i in range(0, self._period_number)]
+ group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
+ field_col_info = obj._all_columns.get(groupby_field.split(':')[0])
+ pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
+ for group in group_obj:
+ group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date()
+ timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
+ section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
+ return section_result
+
+ def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
+ """ Get the daily statistics of the mass mailing. This is done by a grouping
+ on opened and replied fields. Using custom format in context, we obtain
+ results for the next 6 days following the mass mailing date. """
+ obj = self.pool['mail.mail.statistics']
+ res = {}
+ for mailing in self.browse(cr, uid, ids, context=context):
+ res[mailing.id] = {}
+ date = mailing.sent_date if mailing.sent_date else mailing.create_date
+ date_begin = datetime.strptime(date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
+ date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ domain = [('mass_mailing_id', '=', mailing.id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
+ res[mailing.id]['opened_dayly'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['opened'], 'opened_count', 'opened:day', date_begin, context=context))
+ domain = [('mass_mailing_id', '=', mailing.id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
+ res[mailing.id]['replied_dayly'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['replied'], 'replied_count', 'replied:day', date_begin, context=context))
+ return res
+
+ def _get_statistics(self, cr, uid, ids, name, arg, context=None):
+ """ Compute statistics of the mass mailing campaign """
+ Statistics = self.pool['mail.mail.statistics']
+ results = dict.fromkeys(ids, False)
+ for mid in ids:
+ stat_ids = Statistics.search(cr, uid, [('mass_mailing_id', '=', mid)], context=context)
+ stats = Statistics.browse(cr, uid, stat_ids, context=context)
+ results[mid] = {
+ 'total': len(stats),
+ 'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]),
+ 'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]),
+ 'sent': len([s for s in stats if not s.sent is False]),
+ 'opened': len([s for s in stats if not s.opened is False]),
+ 'replied': len([s for s in stats if not s.replied is False]),
+ 'bounced': len([s for s in stats if not s.bounced is False]),
+ }
+ results[mid]['delivered'] = results[mid]['sent'] - results[mid]['bounced']
+ results[mid]['received_ratio'] = 100.0 * results[mid]['delivered'] / (results[mid]['total'] or 1)
+ results[mid]['opened_ratio'] = 100.0 * results[mid]['opened'] / (results[mid]['total'] or 1)
+ results[mid]['replied_ratio'] = 100.0 * results[mid]['replied'] / (results[mid]['total'] or 1)
+ return results
+
+ def _get_mailing_model(self, cr, uid, context=None):
+ res = []
+ for model_name in self.pool:
+ model = self.pool[model_name]
+ if hasattr(model, '_mail_mass_mailing') and getattr(model, '_mail_mass_mailing'):
+ res.append((model._name, getattr(model, '_mail_mass_mailing')))
+ res.append(('mail.mass_mailing.contact', _('Mailing List')))
+ return res
+
+ # indirections for inheritance
+ _mailing_model = lambda self, *args, **kwargs: self._get_mailing_model(*args, **kwargs)
+
+ _columns = {
+ 'name': fields.char('Subject', required=True),
+ 'email_from': fields.char('From', required=True),
+ 'create_date': fields.datetime('Creation Date'),
+ 'sent_date': fields.datetime('Sent Date'),
+ 'body_html': fields.html('Body'),
+ 'attachment_ids': fields.many2many(
+ 'ir.attachment', 'mass_mailing_ir_attachments_rel',
+ 'mass_mailing_id', 'attachment_id', 'Attachments'
+ ),
+ 'mass_mailing_campaign_id': fields.many2one(
+ 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
+ ondelete='set null',
+ ),
+ 'state': fields.selection(
+ [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')],
+ string='Status', required=True,
+ ),
+ 'color': fields.related(
+ 'mass_mailing_campaign_id', 'color',
+ type='integer', string='Color Index',
+ ),
+ # mailing options
+ 'reply_to_mode': fields.selection(
+ [('thread', 'In Document'), ('email', 'Specified Email Address')],
+ string='Reply-To Mode', required=True,
+ ),
+ 'reply_to': fields.char('Reply To', help='Preferred Reply-To Address'),
+ # recipients
+ 'mailing_model': fields.selection(_mailing_model, string='Recipients Model', required=True),
+ 'mailing_domain': fields.char('Domain'),
+ 'contact_list_ids': fields.many2many(
+ 'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
+ string='Mailing Lists',
+ ),
+ 'contact_ab_pc': fields.integer(
+ 'AB Testing percentage',
+ help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
+ ),
+ # statistics data
+ 'statistics_ids': fields.one2many(
+ 'mail.mail.statistics', 'mass_mailing_id',
+ 'Emails Statistics',
+ ),
+ 'total': fields.function(
+ _get_statistics, string='Total',
+ type='integer', multi='_get_statistics',
+ ),
+ 'scheduled': fields.function(
+ _get_statistics, string='Scheduled',
+ type='integer', multi='_get_statistics',
+ ),
+ 'failed': fields.function(
+ _get_statistics, string='Failed',
+ type='integer', multi='_get_statistics',
+ ),
+ 'sent': fields.function(
+ _get_statistics, string='Sent',
+ type='integer', multi='_get_statistics',
+ ),
+ 'delivered': fields.function(
+ _get_statistics, string='Delivered',
+ type='integer', multi='_get_statistics',
+ ),
+ 'opened': fields.function(
+ _get_statistics, string='Opened',
+ type='integer', multi='_get_statistics',
+ ),
+ 'replied': fields.function(
+ _get_statistics, string='Replied',
+ type='integer', multi='_get_statistics',
+ ),
+ 'bounced': fields.function(
+ _get_statistics, string='Bounced',
+ type='integer', multi='_get_statistics',
+ ),
+ 'received_ratio': fields.function(
+ _get_statistics, string='Received Ratio',
+ type='integer', multi='_get_statistics',
+ ),
+ 'opened_ratio': fields.function(
+ _get_statistics, string='Opened Ratio',
+ type='integer', multi='_get_statistics',
+ ),
+ 'replied_ratio': fields.function(
+ _get_statistics, string='Replied Ratio',
+ type='integer', multi='_get_statistics',
+ ),
+ # dayly ratio
+ 'opened_dayly': fields.function(
+ _get_daily_statistics, string='Opened',
+ type='char', multi='_get_daily_statistics',
+ oldname='opened_monthly',
+ ),
+ 'replied_dayly': fields.function(
+ _get_daily_statistics, string='Replied',
+ type='char', multi='_get_daily_statistics',
+ oldname='replied_monthly',
+ )
+ }
+
+ def default_get(self, cr, uid, fields, context=None):
+ res = super(MassMailing, self).default_get(cr, uid, fields, context=context)
+ if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model'):
+ if res['mailing_model'] in ['res.partner', 'mail.mass_mailing.contact']:
+ res['reply_to_mode'] = 'email'
+ else:
+ res['reply_to_mode'] = 'thread'
+ return res
+
+ _defaults = {
+ 'state': 'draft',
+ 'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
+ 'reply_to': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
+ 'mailing_model': 'mail.mass_mailing.contact',
+ 'contact_ab_pc': 100,
+ }
+
+ #------------------------------------------------------
+ # Technical stuff
+ #------------------------------------------------------
+
+ def copy_data(self, cr, uid, id, default=None, context=None):
+ if default is None:
+ default = {}
+ mailing = self.browse(cr, uid, id, context=context)
+ default.update({
+ 'state': 'draft',
+ 'statistics_ids': [],
+ 'name': _('%s (duplicate)') % mailing.name,
+ 'sent_date': False,
+ })
+ return super(MassMailing, self).copy_data(cr, uid, id, default, context=context)
+
+ def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
+ """ Override read_group to always display all states. """
+ if groupby and groupby[0] == "state":
+ # Default result structure
+ # states = self._get_state_list(cr, uid, context=context)
+ states = [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')]
+ read_group_all_states = [{
+ '__context': {'group_by': groupby[1:]},
+ '__domain': domain + [('state', '=', state_value)],
+ 'state': state_value,
+ 'state_count': 0,
+ } for state_value, state_name in states]
+ # Get standard results
+ read_group_res = super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
+ # Update standard results with default results
+ result = []
+ for state_value, state_name in states:
+ res = filter(lambda x: x['state'] == state_value, read_group_res)
+ if not res:
+ res = filter(lambda x: x['state'] == state_value, read_group_all_states)
+ res[0]['state'] = [state_value, state_name]
+ result.append(res[0])
+ return result
+ else:
+ return super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
+
+ #------------------------------------------------------
+ # Views & Actions
+ #------------------------------------------------------
+
+ def on_change_model_and_list(self, cr, uid, ids, mailing_model, list_ids, context=None):
+ value = {}
+ if mailing_model == 'mail.mass_mailing.contact':
+ list_ids = map(lambda item: item if isinstance(item, (int, long)) else [lid for lid in item[2]], list_ids)
+ if list_ids:
+ value['mailing_domain'] = "[('list_id', 'in', %s)]" % list_ids
+ else:
+ value['mailing_domain'] = "[('list_id', '=', False)]"
+ else:
+ value['mailing_domain'] = False
+ return {'value': value}
+
+ def action_duplicate(self, cr, uid, ids, context=None):
+ copy_id = None
+ for mid in ids:
+ copy_id = self.copy(cr, uid, mid, context=context)
+ if copy_id:
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'mail.mass_mailing',
+ 'res_id': copy_id,
+ 'context': context,
+ }
+ return False
+
+ def action_test_mailing(self, cr, uid, ids, context=None):
+ ctx = dict(context, default_mass_mailing_id=ids[0])
+ return {
+ 'name': _('Test Mailing'),
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'mail.mass_mailing.test',
+ 'target': 'new',
+ 'context': ctx,
+ }
+
+ def action_edit_html(self, cr, uid, ids, context=None):
+ if not len(ids) == 1:
+ raise ValueError('One and only one ID allowed for this action')
+ mail = self.browse(cr, uid, ids[0], context=context)
+ url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d&template_model=%s&enable_editor=1' % (ids[0], mail.mailing_model)
+ return {
+ 'name': _('Open with Visual Editor'),
+ 'type': 'ir.actions.act_url',
+ 'url': url,
+ 'target': 'self',
+ }
+
+ #------------------------------------------------------
+ # Email Sending
+ #------------------------------------------------------
+
+ def get_recipients(self, cr, uid, mailing, context=None):
+ domain = eval(mailing.mailing_domain)
+ res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
+
+ # randomly choose a fragment
+ if mailing.contact_ab_pc < 100:
+ contact_nbr = self.pool[mailing.mailing_model].search(cr, uid, domain, count=True, context=context)
+ topick = int(contact_nbr / 100.0 * mailing.contact_ab_pc)
+ if mailing.mass_mailing_campaign_id and mailing.mass_mailing_campaign_id.unique_ab_testing:
+ already_mailed = self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id]
+ else:
+ already_mailed = set([])
+ remaining = set(res_ids).difference(already_mailed)
+ if topick > len(remaining):
+ topick = len(remaining)
+ res_ids = random.sample(remaining, topick)
+ return res_ids
+
+ def send_mail(self, cr, uid, ids, context=None):
+ author_id = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id
+ for mailing in self.browse(cr, uid, ids, context=context):
+ # instantiate an email composer + send emails
+ res_ids = self.get_recipients(cr, uid, mailing, context=context)
+ comp_ctx = dict(context, active_ids=res_ids)
+ composer_values = {
+ 'author_id': author_id,
+ 'body': mailing.body_html,
+ 'subject': mailing.name,
+ 'model': mailing.mailing_model,
+ 'email_from': mailing.email_from,
+ 'record_name': False,
+ 'composition_mode': 'mass_mail',
+ 'mass_mailing_id': mailing.id,
+ 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
+ }
+ if mailing.reply_to_mode == 'email':
+ composer_values['reply_to'] = mailing.reply_to
+ composer_id = self.pool['mail.compose.message'].create(cr, uid, composer_values, context=comp_ctx)
+ self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx)
+ self.write(cr, uid, [mailing.id], {'sent_date': fields.datetime.now(), 'state': 'done'}, context=context)
+ return True
diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py
new file mode 100644
index 00000000000..319a6fde14d
--- /dev/null
+++ b/addons/mass_mailing/models/mass_mailing_stats.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013-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 datetime import datetime
+from dateutil import relativedelta
+import random
+try:
+ import simplejson as json
+except ImportError:
+ import json
+import urllib
+import urlparse
+
+from openerp import tools
+from openerp.exceptions import Warning
+from openerp.tools.safe_eval import safe_eval as eval
+from openerp.tools.translate import _
+from openerp.osv import osv, fields
+
+
+class MailMailStats(osv.Model):
+ """ MailMailStats models the statistics collected about emails. Those statistics
+ are stored in a separated model and table to avoid bloating the mail_mail table
+ with statistics values. This also allows to delete emails send with mass mailing
+ without loosing the statistics about them. """
+
+ _name = 'mail.mail.statistics'
+ _description = 'Email Statistics'
+ _rec_name = 'message_id'
+ _order = 'message_id'
+
+ _columns = {
+ 'mail_mail_id': fields.integer(
+ 'Mail ID',
+ help='ID of the related mail_mail. This field is an integer field because'
+ 'the related mail_mail can be deleted separately from its statistics.'
+ ),
+ 'message_id': fields.char('Message-ID'),
+ 'model': fields.char('Document model'),
+ 'res_id': fields.integer('Document ID'),
+ # campaign / wave data
+ 'mass_mailing_id': fields.many2one(
+ 'mail.mass_mailing', 'Mass Mailing',
+ ondelete='set null',
+ ),
+ 'mass_mailing_campaign_id': fields.related(
+ 'mass_mailing_id', 'mass_mailing_campaign_id',
+ type='many2one', ondelete='set null',
+ relation='mail.mass_mailing.campaign',
+ string='Mass Mailing Campaign',
+ store=True, readonly=True,
+ ),
+ # Bounce and tracking
+ 'scheduled': fields.datetime('Scheduled', help='Date when the email has been created'),
+ 'sent': fields.datetime('Sent', help='Date when the email has been sent'),
+ 'exception': fields.datetime('Exception', help='Date of technical error leading to the email not being sent'),
+ 'opened': fields.datetime('Opened', help='Date when the email has been opened the first time'),
+ 'replied': fields.datetime('Replied', help='Date when this email has been replied for the first time.'),
+ 'bounced': fields.datetime('Bounced', help='Date when this email has bounced.'),
+ }
+
+ _defaults = {
+ 'scheduled': fields.datetime.now,
+ }
+
+ def _get_ids(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, domain=None, context=None):
+ if not ids and mail_mail_ids:
+ base_domain = [('mail_mail_id', 'in', mail_mail_ids)]
+ elif not ids and mail_message_ids:
+ base_domain = [('message_id', 'in', mail_message_ids)]
+ else:
+ base_domain = [('id', 'in', ids or [])]
+ if domain:
+ base_domain = ['&'] + domain + base_domain
+ return self.search(cr, uid, base_domain, context=context)
+
+ def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
+ stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('opened', '=', False)], context)
+ self.write(cr, uid, stat_ids, {'opened': fields.datetime.now()}, context=context)
+ return stat_ids
+
+ def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
+ stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('replied', '=', False)], context)
+ self.write(cr, uid, stat_ids, {'replied': fields.datetime.now()}, context=context)
+ return stat_ids
+
+ def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
+ stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('bounced', '=', False)], context)
+ self.write(cr, uid, stat_ids, {'bounced': fields.datetime.now()}, context=context)
+ return stat_ids
+
diff --git a/addons/mass_mailing/models/res_config.py b/addons/mass_mailing/models/res_config.py
new file mode 100644
index 00000000000..4b3757b7dca
--- /dev/null
+++ b/addons/mass_mailing/models/res_config.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import fields, osv
+
+
+class MassMailingConfiguration(osv.TransientModel):
+ _name = 'marketing.config.settings'
+ _inherit = 'marketing.config.settings'
+
+ _columns = {
+ 'group_mass_mailing_campaign': fields.boolean(
+ 'Manage Mass Mailing using Campaign',
+ implied_group='mass_mailing.group_mass_mailing_campaign',
+ help="""Manage mass mailign using Campaigns"""),
+ }
diff --git a/addons/mass_mailing/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv
index 59cf03c2c68..d5c76b9f69d 100644
--- a/addons/mass_mailing/security/ir.model.access.csv
+++ b/addons/mass_mailing/security/ir.model.access.csv
@@ -1,4 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_mass_mailing_category,mail.mass_mailing.category,model_mail_mass_mailing_category,base.group_user,1,1,1,1
+access_mass_mailing_contact,mail.mass_mailing.contact,model_mail_mass_mailing_contact,base.group_user,1,1,1,1
+access_mass_mailing_list,mail.mass_mailing.list,model_mail_mass_mailing_list,base.group_user,1,1,1,1
+access_mass_mailing_stage,mail.mass_mailing.stage,model_mail_mass_mailing_stage,base.group_user,1,1,1,1
access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,base.group_user,1,1,1,0
access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1
access_mass_mailing,mail.mass_mailing,model_mail_mass_mailing,base.group_user,1,1,1,0
diff --git a/addons/mass_mailing/static/src/css/email_template.css b/addons/mass_mailing/static/src/css/email_template.css
new file mode 100644
index 00000000000..495c201a172
--- /dev/null
+++ b/addons/mass_mailing/static/src/css/email_template.css
@@ -0,0 +1,17 @@
+.openerp .oe_kanban_email_template {
+ width: 360px;
+ min-height: 270px !important;
+}
+
+.kanban_html_preview {
+ width: 600px;
+ height: 600px;
+ -webkit-transform: scale(.50);
+ -ms-transform: scale(.50);
+ transform: scale(.50);
+ -webkit-transform-origin: 0 0;
+ -ms-transform-origin: 0 0;
+ transform-origin: 0 0;
+ margin: 0 0px -300px 0;
+ overflow: hidden !important;
+}
diff --git a/addons/mass_mailing/static/src/css/mass_mailing.css b/addons/mass_mailing/static/src/css/mass_mailing.css
index e54ce46c025..cc91e8acf58 100644
--- a/addons/mass_mailing/static/src/css/mass_mailing.css
+++ b/addons/mass_mailing/static/src/css/mass_mailing.css
@@ -1,61 +1,13 @@
-.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_campaign {
- /* Customize to manage content */
- width: 552px;
- min-height: 278px !important;
- /* End of customize */
+.openerp .oe_kanban_view .oe_kanban_mass_mailing_campaign {
+ width: 280px;
+ min-height: 141px;
}
-.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_segment {
- /* Customize to manage content */
- width: 282px;
- min-height: 246px !important;
- /* End of customize */
+.openerp .oe_kanban_view .oe_kanban_mass_mailing_campaign .oe_kanban_header_right {
+ float: right;
}
-.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_mail_stats {
- width: 122px; /* Manage space in between stats */
- display: inline-block;
- margin: 2px 5px 0px 5px;
- text-align: center;
- border: 1px solid rgba(0, 0, 0, 0.16);
- -webkit-border-radius: 2px;
- border-radius: 2px;
- background-color: #FFFFFF;
+.openerp .oe_kanban_view .oe_kanban_mass_mailing {
+ width: 280px;
+ min-height: 141px;
}
-
-.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_mail_result {
- font-weight: bold;
-}
-
-.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_gauge {
- width: 120px;
- height: 120px;
- display: inline-block;
-}
-
-.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_kanban_content div.oe_sparkline_container {
- height: 60px;
- width: 120px;
- display: inline-block;
- margin: 8px 5px 0px 5px;
-}
-
-.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_sparkline_bar_title {
- text-align: center;
- display: inline;
-}
-
-.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_sparkline_bar {
- width: 100px;
- height: 60px;
- display: inline-block;
-}
-
-/*
- * Campaign related CSS
- */
-
-
-/*
- * Segment related CSS
- */
diff --git a/addons/mass_mailing/static/src/js/mass_mailing.js b/addons/mass_mailing/static/src/js/mass_mailing.js
index 93f63f01ad1..ef456c40bbf 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing.js
@@ -1,13 +1,13 @@
-openerp.mass_mailing = function(openerp) {
+openerp.mass_mailing = function (instance) {
+ var _t = instance.web._t;
openerp.web_kanban.KanbanRecord.include({
on_card_clicked: function (event) {
if (this.view.dataset.model === 'mail.mass_mailing.campaign') {
- this.$('.oe_mass_mailings a').first().click();
+ this.$('.oe_mailings').click();
} else {
this._super.apply(this, arguments);
}
},
});
-
};
diff --git a/addons/mass_mailing/views/email_template.xml b/addons/mass_mailing/views/email_template.xml
new file mode 100644
index 00000000000..cde5685abaf
--- /dev/null
+++ b/addons/mass_mailing/views/email_template.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+ email.template.form.minimal
+ email.template
+ 32
+
+
+
+
+
+
+ email.template.kanban
+ email.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Templates
+ email.template
+ form
+ kanban,tree,form
+ {
+ 'form_view_ref': 'mass_mailing.email_template_form_minimal',
+ 'default_use_default_to': True,
+}
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml
new file mode 100644
index 00000000000..7892feacacc
--- /dev/null
+++ b/addons/mass_mailing/views/mass_mailing.xml
@@ -0,0 +1,626 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.mass_mailing.contact.search
+ mail.mass_mailing.contact
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.mass_mailing.contact.tree
+ mail.mass_mailing.contact
+ 10
+
+
+
+
+
+
+
+
+
+
+
+ Mailing List Subscribers
+ mail.mass_mailing.contact
+ form
+ tree
+ {'search_default_not_opt_out': 1}
+
+
+
+ Recipients
+ mail.mass_mailing.contact
+ form
+ tree
+ {'search_default_list_id': active_id, 'search_default_not_opt_out': 1}
+
+
+ Click to create a recipient.
+
+
+
+
+
+
+
+
+ mail.mass_mailing.list.search
+ mail.mass_mailing.list
+
+
+
+
+
+
+
+
+ mail.mass_mailing.list.tree
+ mail.mass_mailing.list
+ 10
+
+
+
+
+
+
+
+
+
+ mail.mass_mailing.list.form
+ mail.mass_mailing.list
+
+
+
+
+
+
+ Contact Lists
+ mail.mass_mailing.list
+ form
+ tree,form
+
+
+ Click here to create a new mailing list.
+
+ Mailing lists allows you to to manage customers and
+ contacts easily and to send to mailings in a single click.
+
+
+
+
+
+
+
+ mail.mass_mailing.search
+ mail.mass_mailing
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.mass_mailing.tree
+ mail.mass_mailing
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.mass_mailing.form
+ mail.mass_mailing
+
+
+
+
+
+
+ mail.mass_mailing.kanban
+ mail.mass_mailing
+
+
+
+
+
+
+
+
+ i
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ Opened %
+ Replied %
+
+
+
+
+
+
+
+
+
+
+
+
+ Mass Mailings
+ mail.mass_mailing
+ form
+ kanban,tree,form
+
+
+ Click here to create a new mailing.
+
+ Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
+
+
+
+
+ Mass Mailings
+ mail.mass_mailing
+ form
+ kanban,tree,form
+ {
+ 'search_default_mass_mailing_campaign_id': [active_id],
+ 'default_mass_mailing_campaign_id': active_id,
+ }
+
+
+
+ Click here to create a new mailing.
+
+ Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
+
+
+
+
+
+
+
+ mail.mass_mailing.stage.search
+ mail.mass_mailing.stage
+
+
+
+
+
+
+
+
+ mail.mass_mailing.stage.tree
+ mail.mass_mailing.stage
+ 10
+
+
+
+
+
+
+
+
+ Mass Mailing Stages
+ mail.mass_mailing.stage
+ form
+ tree,form
+
+
+
+
+
+
+ mail.mass_mailing.campaign.search
+ mail.mass_mailing.campaign
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.mass_mailing.campaign.tree
+ mail.mass_mailing.campaign
+ 10
+
+
+
+
+
+
+
+
+
+
+
+ mail.mass_mailing.campaign.form
+ mail.mass_mailing.campaign
+
+
+
+
+
+
+ mail.mass_mailing.campaign.kanban
+ mail.mass_mailing.campaign
+
+
+
+
+
+
+
+
+
+
+ i
+
+
+
+
+
+
+
+
+
+
+ Opened %
+ Replied %
+
+
+
+
+
+
+
+
+
+
+
+
+ Mass Mailing Campaigns
+ mail.mass_mailing.campaign
+ form
+ kanban,tree,form
+
+
+ Click to define a new mass mailing campaign.
+
+ Create a campaign to structure mass mailing and get analysis from email status.
+
+
+
+
+
+
+
+
+ mail.mail.statistics.search
+ mail.mail.statistics
+
+
+
+
+
+
+
+
+
+ mail.mail.statistics.tree
+ mail.mail.statistics
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.mail.statistics.form
+ mail.mail.statistics
+
+
+
+
+
+
+ Mail Statistics
+ mail.mail.statistics
+ form
+ tree,form
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing/views/res_config.xml b/addons/mass_mailing/views/res_config.xml
new file mode 100644
index 00000000000..0f61086e2cc
--- /dev/null
+++ b/addons/mass_mailing/views/res_config.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ marketing.config.settings.mass.mailing
+ marketing.config.settings
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing/views/res_partner.xml b/addons/mass_mailing/views/res_partner.xml
new file mode 100644
index 00000000000..14710cf8f37
--- /dev/null
+++ b/addons/mass_mailing/views/res_partner.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/addons/mass_mailing/wizard/__init__.py b/addons/mass_mailing/wizard/__init__.py
index 669d12289c2..aa8074b7488 100644
--- a/addons/mass_mailing/wizard/__init__.py
+++ b/addons/mass_mailing/wizard/__init__.py
@@ -1,23 +1,4 @@
# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2013-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 test_mailing
import mail_compose_message
-import mail_mass_mailing_create_segment
diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py
index 4b3f0d6b9bd..4aebff2d04d 100644
--- a/addons/mass_mailing/wizard/mail_compose_message.py
+++ b/addons/mass_mailing/wizard/mail_compose_message.py
@@ -1,23 +1,4 @@
# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2013-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 openerp.osv import osv, fields
@@ -29,11 +10,14 @@ class MailComposeMessage(osv.TransientModel):
_columns = {
'mass_mailing_campaign_id': fields.many2one(
- 'mail.mass_mailing.campaign', 'Mass mailing campaign',
+ 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
),
'mass_mailing_id': fields.many2one(
- 'mail.mass_mailing', 'Mass mailing',
- domain="[('mass_mailing_campaign_id', '=', mass_mailing_campaign_id)]",
+ 'mail.mass_mailing', 'Mass Mailing'
+ ),
+ 'mass_mailing_name': fields.char('Mass Mailing'),
+ 'mailing_list_ids': fields.many2many(
+ 'mail.mass_mailing.list', string='Mailing List'
),
}
@@ -42,23 +26,27 @@ class MailComposeMessage(osv.TransientModel):
mail.mail.statistics values in the o2m of mail_mail, when doing pure
email mass mailing. """
res = super(MailComposeMessage, self).get_mail_values(cr, uid, wizard, res_ids, context=context)
- if wizard.composition_mode == 'mass_mail' and wizard.mass_mailing_campaign_id: # TODO: which kind of mass mailing ?
- if wizard.mass_mailing_id:
- mass_mailing_id = wizard.mass_mailing_id.id
- else:
- current_date = fields.datetime.now()
+ # use only for allowed models in mass mailing
+ if wizard.composition_mode == 'mass_mail' and \
+ (wizard.mass_mailing_name or wizard.mass_mailing_id) and \
+ wizard.model in [item[0] for item in self.pool['mail.mass_mailing']._get_mailing_model(cr, uid, context=context)]:
+ mass_mailing = wizard.mass_mailing_id
+ if not mass_mailing:
mass_mailing_id = self.pool['mail.mass_mailing'].create(
cr, uid, {
- 'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id,
- 'name': '%s-%s' % (wizard.mass_mailing_campaign_id.name, current_date),
- 'date': current_date,
- 'domain': wizard.active_domain,
+ 'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id and wizard.mass_mailing_campaign_id.id or False,
+ 'name': wizard.mass_mailing_name,
'template_id': wizard.template_id and wizard.template_id.id or False,
+ 'state': 'done',
+ 'mailing_type': wizard.model,
+ 'mailing_domain': wizard.active_domain,
}, context=context)
+ mass_mailing = self.pool['mail.mass_mailing'].browse(cr, uid, mass_mailing_id, context=context)
for res_id in res_ids:
+ res[res_id]['mailing_id'] = mass_mailing.id
res[res_id]['statistics_ids'] = [(0, 0, {
'model': wizard.model,
'res_id': res_id,
- 'mass_mailing_id': mass_mailing_id,
+ 'mass_mailing_id': mass_mailing.id,
})]
return res
diff --git a/addons/mass_mailing/wizard/mail_compose_message_view.xml b/addons/mass_mailing/wizard/mail_compose_message_view.xml
index f5dbf57a808..61cfe823006 100644
--- a/addons/mass_mailing/wizard/mail_compose_message_view.xml
+++ b/addons/mass_mailing/wizard/mail_compose_message_view.xml
@@ -9,7 +9,9 @@
-
+
diff --git a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py
deleted file mode 100644
index 0944e89f1de..00000000000
--- a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2013-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 openerp.osv import osv, fields
-
-from openerp.tools.translate import _
-
-
-class MailMassMailingCreate(osv.TransientModel):
- """Wizard to help creating mass mailing waves for a campaign. """
-
- _name = 'mail.mass_mailing.create'
- _description = 'Mass mailing creation'
-
- _columns = {
- 'mass_mailing_campaign_id': fields.many2one(
- 'mail.mass_mailing.campaign', 'Mass mailing campaign',
- required=True,
- ),
- 'model_id': fields.many2one(
- 'ir.model', 'Document Type',
- required=True,
- help='Document on which the mass mailing will run. This must be a '
- 'valid OpenERP model.',
- ),
- 'model_model': fields.related(
- 'model_id', 'name',
- type='char', string='Model Name'
- ),
- 'filter_id': fields.many2one(
- 'ir.filters', 'Filter',
- required=True,
- domain="[('model_id', '=', model_model)]",
- help='Filter to be applied on the document to find the records to be '
- 'mailed.',
- ),
- 'domain': fields.related(
- 'filter_id', 'domain',
- type='char', string='Domain',
- ),
- 'template_id': fields.many2one(
- 'email.template', 'Template', required=True,
- domain="[('model_id', '=', model_id)]",
- ),
- 'name': fields.char(
- 'Mailing Name', required=True,
- help='Name of the mass mailing.',
- ),
- 'mass_mailing_id': fields.many2one(
- 'mail.mass_mailing', 'Mass Mailing',
- ),
- }
-
- def _get_default_model_id(self, cr, uid, context=None):
- model_ids = self.pool['ir.model'].search(cr, uid, [('model', '=', 'res.partner')], context=context)
- return model_ids and model_ids[0] or False
-
- _defaults = {
- 'model_id': lambda self, cr, uid, ctx=None: self._get_default_model_id(cr, uid, context=ctx),
- }
-
- def on_change_model_id(self, cr, uid, ids, model_id, context=None):
- if model_id:
- model_model = self.pool['ir.model'].browse(cr, uid, model_id, context=context).model
- else:
- model_model = False
- return {'value': {'model_model': model_model}}
-
- def on_change_filter_id(self, cr, uid, ids, filter_id, context=None):
- if filter_id:
- domain = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context).domain
- else:
- domain = False
- return {'value': {'domain': domain}}
-
- def create_mass_mailing(self, cr, uid, ids, context=None):
- """ Create a mass mailing based on wizard data, and update the wizard """
- for wizard in self.browse(cr, uid, ids, context=context):
- mass_mailing_values = {
- 'name': wizard.name,
- 'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id,
- 'domain': wizard.domain,
- 'template_id': wizard.template_id.id,
- }
- mass_mailing_id = self.pool['mail.mass_mailing'].create(cr, uid, mass_mailing_values, context=context)
- self.write(cr, uid, [wizard.id], {'mass_mailing_id': mass_mailing_id}, context=context)
- return True
-
- def launch_composer(self, cr, uid, ids, context=None):
- """ Main wizard action: create a new mailing and launch the mail.compose.message
- email composer with wizard data. """
- self.create_mass_mailing(cr, uid, ids, context=context)
-
- wizard = self.browse(cr, uid, ids[0], context=context)
- ctx = dict(context)
- ctx.update({
- 'default_composition_mode': 'mass_mail',
- 'default_template_id': wizard.template_id.id,
- 'default_use_mass_mailing_campaign': True,
- 'default_use_active_domain': True,
- 'default_model': wizard.model_id.model,
- 'default_res_id': False,
- 'default_active_domain': wizard.domain,
- 'default_mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id,
- 'default_mass_mailing_id': wizard.mass_mailing_id.id,
- })
- return {
- 'name': _('Compose Email for Mass Mailing'),
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'mail.compose.message',
- 'views': [(False, 'form')],
- 'view_id': False,
- 'target': 'new',
- 'context': ctx,
- }
diff --git a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml
deleted file mode 100644
index b2b48fb8618..00000000000
--- a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
-
-
-
- mail.mass_mailing.create.form
- mail.mass_mailing.create
-
-
-
-
-
-
- Create Mass Mailing
- mail.mass_mailing.create
- mail.mass_mailing.campaign
- ir.actions.act_window
- form
- form
- new
- {'default_mass_mailing_campaign_id': active_id}
-
-
-
-
diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py
new file mode 100644
index 00000000000..39883d941ae
--- /dev/null
+++ b/addons/mass_mailing/wizard/test_mailing.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+
+from openerp import tools
+from openerp.osv import osv, fields
+
+
+class TestMassMailing(osv.TransientModel):
+ _name = 'mail.mass_mailing.test'
+ _description = 'Sample Mail Wizard'
+
+ _columns = {
+ 'email_to': fields.char('Recipients', required=True,
+ help='Comma-separated list of email addresses.'),
+ 'mass_mailing_id': fields.many2one('mail.mass_mailing', 'Mailing', required=True),
+ }
+
+ _defaults = {
+ 'email_to': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
+ }
+
+ def send_mail_test(self, cr, uid, ids, context=None):
+ Mail = self.pool['mail.mail']
+ for wizard in self.browse(cr, uid, ids, context=context):
+ mailing = wizard.mass_mailing_id
+ test_emails = tools.email_split(wizard.email_to)
+ mail_ids = []
+ for test_mail in test_emails:
+ body = mailing.body_html
+ unsubscribe_url = self.pool['mail.mass_mailing'].get_unsubscribe_url(cr, uid, mailing.id, 0, email=test_mail, context=context)
+ body = tools.append_content_to_html(body, unsubscribe_url, plaintext=False, container_tag='p')
+ mail_values = {
+ 'email_from': mailing.email_from,
+ 'reply_to': mailing.reply_to,
+ 'email_to': test_mail,
+ 'subject': mailing.name,
+ 'body_html': body,
+ 'auto_delete': True,
+ }
+ mail_ids.append(Mail.create(cr, uid, mail_values, context=context))
+ Mail.send(cr, uid, mail_ids, context=context)
+ self.pool['mail.mass_mailing'].write(cr, uid, [mailing.id], {'state': 'test'}, context=context)
+ return True
diff --git a/addons/mass_mailing/wizard/test_mailing.xml b/addons/mass_mailing/wizard/test_mailing.xml
new file mode 100644
index 00000000000..61ea8817bc4
--- /dev/null
+++ b/addons/mass_mailing/wizard/test_mailing.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ mail.mass_mailing.test.form
+ mail.mass_mailing.test
+
+
+
+
+
+
+ Mailing Test
+ mail.mass_mailing.test
+ form
+ form
+ new
+
+
+
+
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index 464cb0c8f5c..2d466219140 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -69,9 +69,9 @@ class PaymentAcquirer(osv.Model):
string='Process Method',
help='Static payments are payments like transfer, that require manual steps.'),
'view_template_id': fields.many2one('ir.ui.view', 'Form Button Template', required=True),
- 'env': fields.selection(
+ 'environment': fields.selection(
[('test', 'Test'), ('prod', 'Production')],
- string='Environment'),
+ string='Environment', oldname='env'),
'website_published': fields.boolean(
'Visible in Portal / Website',
help="Make this payment acquirer available (Customer invoices, etc.)"),
@@ -85,7 +85,7 @@ class PaymentAcquirer(osv.Model):
_defaults = {
'company_id': lambda self, cr, uid, obj, ctx=None: self.pool['res.users'].browse(cr, uid, uid).company_id.id,
- 'env': 'test',
+ 'environment': 'test',
'validation': 'automatic',
'website_published': True,
}
diff --git a/addons/payment/views/payment_acquirer.xml b/addons/payment/views/payment_acquirer.xml
index 325bb774ee6..0228bd7d17d 100644
--- a/addons/payment/views/payment_acquirer.xml
+++ b/addons/payment/views/payment_acquirer.xml
@@ -19,7 +19,7 @@
-
+
@@ -70,7 +70,7 @@
-
+
diff --git a/addons/payment_adyen/data/adyen.xml b/addons/payment_adyen/data/adyen.xml
index 5ca0f4d71e9..ec1808f5b7f 100644
--- a/addons/payment_adyen/data/adyen.xml
+++ b/addons/payment_adyen/data/adyen.xml
@@ -7,7 +7,7 @@
adyen
- test
+ test
You will be redirected to the Adyen website after cliking on the payment button.]]>
dummy
diff --git a/addons/payment_adyen/models/adyen.py b/addons/payment_adyen/models/adyen.py
index 607d65738c5..ab210fed890 100644
--- a/addons/payment_adyen/models/adyen.py
+++ b/addons/payment_adyen/models/adyen.py
@@ -21,13 +21,13 @@ _logger = logging.getLogger(__name__)
class AcquirerAdyen(osv.Model):
_inherit = 'payment.acquirer'
- def _get_adyen_urls(self, cr, uid, env, context=None):
+ def _get_adyen_urls(self, cr, uid, environment, context=None):
""" Adyen URLs
- yhpp: hosted payment page: pay.shtml for single, select.shtml for multiple
"""
return {
- 'adyen_form_url': 'https://%s.adyen.com/hpp/pay.shtml' % env,
+ 'adyen_form_url': 'https://%s.adyen.com/hpp/pay.shtml' % environment,
}
def _get_providers(self, cr, uid, context=None):
@@ -97,7 +97,7 @@ class AcquirerAdyen(osv.Model):
def adyen_get_form_action_url(self, cr, uid, id, context=None):
acquirer = self.browse(cr, uid, id, context=context)
- return self._get_adyen_urls(cr, uid, acquirer.env, context=context)['adyen_form_url']
+ return self._get_adyen_urls(cr, uid, acquirer.environment, context=context)['adyen_form_url']
class TxAdyen(osv.Model):
diff --git a/addons/payment_adyen/tests/test_adyen.py b/addons/payment_adyen/tests/test_adyen.py
index c071e682a3d..c4e9356136c 100644
--- a/addons/payment_adyen/tests/test_adyen.py
+++ b/addons/payment_adyen/tests/test_adyen.py
@@ -49,7 +49,7 @@ class AdyenForm(AdyenCommon):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid things
adyen = self.payment_acquirer.browse(self.cr, self.uid, self.adyen_id, None)
- self.assertEqual(adyen.env, 'test', 'test without test env')
+ self.assertEqual(adyen.environment, 'test', 'test without test environment')
# ----------------------------------------
# Test: button direct rendering
diff --git a/addons/payment_ogone/data/ogone.xml b/addons/payment_ogone/data/ogone.xml
index ade39cfb79c..e24aace7ffd 100644
--- a/addons/payment_ogone/data/ogone.xml
+++ b/addons/payment_ogone/data/ogone.xml
@@ -7,7 +7,7 @@
ogone
- test
+ test
You will be redirected to the Ogone website after cliking on the payment button.]]>
dummy
diff --git a/addons/payment_ogone/models/ogone.py b/addons/payment_ogone/models/ogone.py
index f6cab44f0b5..80b5e0d567e 100644
--- a/addons/payment_ogone/models/ogone.py
+++ b/addons/payment_ogone/models/ogone.py
@@ -22,7 +22,7 @@ _logger = logging.getLogger(__name__)
class PaymentAcquirerOgone(osv.Model):
_inherit = 'payment.acquirer'
- def _get_ogone_urls(self, cr, uid, env, context=None):
+ def _get_ogone_urls(self, cr, uid, environment, context=None):
""" Ogone URLS:
- standard order: POST address for form-based
@@ -30,10 +30,10 @@ class PaymentAcquirerOgone(osv.Model):
@TDETODO: complete me
"""
return {
- 'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard_utf8.asp' % (env,),
- 'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect_utf8.asp' % (env,),
- 'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect_utf8.asp' % (env,),
- 'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % (env,),
+ 'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard_utf8.asp' % (environment,),
+ 'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect_utf8.asp' % (environment,),
+ 'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect_utf8.asp' % (environment,),
+ 'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % (environment,),
}
def _get_providers(self, cr, uid, context=None):
@@ -110,7 +110,7 @@ class PaymentAcquirerOgone(osv.Model):
def ogone_get_form_action_url(self, cr, uid, id, context=None):
acquirer = self.browse(cr, uid, id, context=context)
- return self._get_ogone_urls(cr, uid, acquirer.env, context=context)['ogone_standard_order_url']
+ return self._get_ogone_urls(cr, uid, acquirer.environment, context=context)['ogone_standard_order_url']
class PaymentTxOgone(osv.Model):
@@ -351,7 +351,7 @@ class PaymentTxOgone(osv.Model):
PSWD=tx.acquirer_id.ogone_password,
ID=payid,
)
- query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % (tx.acquirer_id.env,)
+ query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % (tx.acquirer_id.environment,)
tries = 2
tx_done = False
diff --git a/addons/payment_ogone/tests/test_ogone.py b/addons/payment_ogone/tests/test_ogone.py
index cc76dd4eed6..0db68214bd4 100644
--- a/addons/payment_ogone/tests/test_ogone.py
+++ b/addons/payment_ogone/tests/test_ogone.py
@@ -24,7 +24,7 @@ class OgonePayment(PaymentAcquirerCommon):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid thing
ogone = self.payment_acquirer.browse(self.cr, self.uid, self.ogone_id, None)
- self.assertEqual(ogone.env, 'test', 'test without test env')
+ self.assertEqual(ogone.environment, 'test', 'test without test environment')
# ----------------------------------------
# Test: button direct rendering + shasign
@@ -110,7 +110,7 @@ class OgonePayment(PaymentAcquirerCommon):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid thing
ogone = self.payment_acquirer.browse(self.cr, self.uid, self.ogone_id, None)
- self.assertEqual(ogone.env, 'test', 'test without test env')
+ self.assertEqual(ogone.environment, 'test', 'test without test environment')
# typical data posted by ogone after client has successfully paid
ogone_post_data = {
@@ -174,7 +174,7 @@ class OgonePayment(PaymentAcquirerCommon):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid thing
ogone = self.payment_acquirer.browse(self.cr, self.uid, self.ogone_id, None)
- self.assertEqual(ogone.env, 'test', 'test without test env')
+ self.assertEqual(ogone.environment, 'test', 'test without test environment')
# create a new draft tx
tx_id = self.payment_transaction.create(
diff --git a/addons/payment_paypal/data/paypal.xml b/addons/payment_paypal/data/paypal.xml
index 0ea6cd5a848..d66dee8a3b3 100644
--- a/addons/payment_paypal/data/paypal.xml
+++ b/addons/payment_paypal/data/paypal.xml
@@ -7,7 +7,7 @@
paypal
- test
+ test
You will be redirected to the Paypal website after cliking on the payment button.]]>
dummy
diff --git a/addons/payment_paypal/models/paypal.py b/addons/payment_paypal/models/paypal.py
index de852702880..d71e3d0620b 100644
--- a/addons/payment_paypal/models/paypal.py
+++ b/addons/payment_paypal/models/paypal.py
@@ -21,9 +21,9 @@ _logger = logging.getLogger(__name__)
class AcquirerPaypal(osv.Model):
_inherit = 'payment.acquirer'
- def _get_paypal_urls(self, cr, uid, env, context=None):
+ def _get_paypal_urls(self, cr, uid, environment, context=None):
""" Paypal URLS """
- if env == 'prod':
+ if environment == 'prod':
return {
'paypal_form_url': 'https://www.paypal.com/cgi-bin/webscr',
'paypal_rest_url': 'https://api.paypal.com/v1/oauth2/token',
@@ -131,7 +131,7 @@ class AcquirerPaypal(osv.Model):
def paypal_get_form_action_url(self, cr, uid, id, context=None):
acquirer = self.browse(cr, uid, id, context=context)
- return self._get_paypal_urls(cr, uid, acquirer.env, context=context)['paypal_form_url']
+ return self._get_paypal_urls(cr, uid, acquirer.environment, context=context)['paypal_form_url']
def _paypal_s2s_get_access_token(self, cr, uid, ids, context=None):
"""
@@ -143,7 +143,7 @@ class AcquirerPaypal(osv.Model):
parameters = werkzeug.url_encode({'grant_type': 'client_credentials'})
for acquirer in self.browse(cr, uid, ids, context=context):
- tx_url = self._get_paypal_urls(cr, uid, acquirer.env)['paypal_rest_url']
+ tx_url = self._get_paypal_urls(cr, uid, acquirer.environment)['paypal_rest_url']
request = urllib2.Request(tx_url, parameters)
# add other headers (https://developer.paypal.com/webapps/developer/docs/integration/direct/make-your-first-call/)
diff --git a/addons/payment_paypal/tests/test_paypal.py b/addons/payment_paypal/tests/test_paypal.py
index 01f40ddd094..ccac9b6fdef 100644
--- a/addons/payment_paypal/tests/test_paypal.py
+++ b/addons/payment_paypal/tests/test_paypal.py
@@ -39,7 +39,7 @@ class PaypalServer2Server(PaypalCommon):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid things
paypal = self.payment_acquirer.browse(self.cr, self.uid, self.paypal_id, None)
- self.assertEqual(paypal.env, 'test', 'test without test env')
+ self.assertEqual(paypal.environment, 'test', 'test without test environment')
res = self.payment_acquirer._paypal_s2s_get_access_token(cr, uid, [self.paypal_id], context=context)
self.assertTrue(res[self.paypal_id] is not False, 'paypal: did not generate access token')
@@ -73,7 +73,7 @@ class PaypalForm(PaypalCommon):
# be sure not to do stupid things
self.payment_acquirer.write(cr, uid, self.paypal_id, {'fees_active': False}, context)
paypal = self.payment_acquirer.browse(cr, uid, self.paypal_id, context)
- self.assertEqual(paypal.env, 'test', 'test without test env')
+ self.assertEqual(paypal.environment, 'test', 'test without test environment')
# ----------------------------------------
# Test: button direct rendering
@@ -122,7 +122,7 @@ class PaypalForm(PaypalCommon):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid things
paypal = self.payment_acquirer.browse(self.cr, self.uid, self.paypal_id, None)
- self.assertEqual(paypal.env, 'test', 'test without test env')
+ self.assertEqual(paypal.environment, 'test', 'test without test environment')
# update acquirer: compute fees
self.payment_acquirer.write(cr, uid, self.paypal_id, {
@@ -156,7 +156,7 @@ class PaypalForm(PaypalCommon):
cr, uid, context = self.cr, self.uid, {}
# be sure not to do stupid things
paypal = self.payment_acquirer.browse(cr, uid, self.paypal_id, context)
- self.assertEqual(paypal.env, 'test', 'test without test env')
+ self.assertEqual(paypal.environment, 'test', 'test without test environment')
# typical data posted by paypal after client has successfully paid
paypal_post_data = {
diff --git a/addons/payment_transfer/data/transfer.xml b/addons/payment_transfer/data/transfer.xml
index 5eabe29a17f..13dd1f241c7 100644
--- a/addons/payment_transfer/data/transfer.xml
+++ b/addons/payment_transfer/data/transfer.xml
@@ -8,7 +8,7 @@
manual
- test
+ test
Transfer information will be provided after choosing the payment mode.]]>
diff --git a/addons/portal/portal_demo.xml b/addons/portal/portal_demo.xml
index f6e46708bbc..ab4876639ef 100644
--- a/addons/portal/portal_demo.xml
+++ b/addons/portal/portal_demo.xml
@@ -8,7 +8,7 @@
demo.portal@yourcompany.example.com
- none
+ none
Vivegnis
4683
diff --git a/addons/portal_sale/portal_sale.py b/addons/portal_sale/portal_sale.py
index aef730ab0a2..f45acf45523 100644
--- a/addons/portal_sale/portal_sale.py
+++ b/addons/portal_sale/portal_sale.py
@@ -126,8 +126,8 @@ class account_invoice(osv.Model):
class mail_mail(osv.osv):
_inherit = 'mail.mail'
- def _postprocess_sent_message(self, cr, uid, mail, context=None):
- if mail.model == 'sale.order':
+ def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
+ if mail_sent and mail.model == 'sale.order':
so_obj = self.pool.get('sale.order')
order = so_obj.browse(cr, uid, mail.res_id, context=context)
partner = order.partner_id
@@ -138,4 +138,4 @@ class mail_mail(osv.osv):
for p in mail.partner_ids:
if p.id not in order.message_follower_ids:
so_obj.message_subscribe(cr, uid, [mail.res_id], [p.id], context=context)
- return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context)
+ return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)
diff --git a/addons/project/res_config.py b/addons/project/res_config.py
index 101a868d574..e7699a61ec8 100644
--- a/addons/project/res_config.py
+++ b/addons/project/res_config.py
@@ -41,9 +41,6 @@ class project_configuration(osv.osv_memory):
'the timesheet line entries for particular date and user, with the effect of creating, '
'editing and deleting either ways.\n'
'-This installs the module project_timesheet.'),
- 'module_project_long_term': fields.boolean("Manage resources planning on gantt view",
- help='A long term project management module that tracks planning, scheduling, and resource allocation.\n'
- '-This installs the module project_long_term.'),
'module_project_issue': fields.boolean("Track issues and bugs",
help='Provides management of issues/bugs in projects.\n'
'-This installs the module project_issue.'),
diff --git a/addons/project/res_config_view.xml b/addons/project/res_config_view.xml
index 207f35da72e..e1d00e56e20 100644
--- a/addons/project/res_config_view.xml
+++ b/addons/project/res_config_view.xml
@@ -61,10 +61,6 @@
-
-
-
-
diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py
index ed131670737..e7b83b4894e 100644
--- a/addons/purchase/purchase.py
+++ b/addons/purchase/purchase.py
@@ -133,10 +133,7 @@ class purchase_order(osv.osv):
def _invoiced(self, cursor, user, ids, name, arg, context=None):
res = {}
for purchase in self.browse(cursor, user, ids, context=context):
- invoiced = False
- if purchase.invoiced_rate == 100.00:
- invoiced = True
- res[purchase.id] = invoiced
+ res[purchase.id] = all(line.invoiced for line in purchase.order_line)
return res
def _get_journal(self, cr, uid, context=None):
@@ -542,7 +539,7 @@ class purchase_order(osv.osv):
inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
inv_lines.append(inv_line_id)
- po_line.write({'invoiced': True, 'invoice_lines': [(4, inv_line_id)]}, context=context)
+ po_line.write({'invoice_lines': [(4, inv_line_id)]}, context=context)
# get invoice data and create invoice
inv_data = {
@@ -1248,10 +1245,10 @@ class mail_mail(osv.Model):
_name = 'mail.mail'
_inherit = 'mail.mail'
- def _postprocess_sent_message(self, cr, uid, mail, context=None):
- if mail.model == 'purchase.order':
+ def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
+ if mail_sent and mail.model == 'purchase.order':
self.pool.get('purchase.order').signal_send_rfq(cr, uid, [mail.res_id])
- return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context)
+ return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)
class product_template(osv.Model):
@@ -1290,9 +1287,15 @@ class account_invoice(osv.Model):
else:
user_id = uid
po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
- for po_id in po_ids:
- purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice received"), context=context)
- workflow.trg_write(uid, 'purchase.order', po_id, cr)
+ for order in purchase_order_obj.browse(cr, uid, po_ids, context=context):
+ purchase_order_obj.message_post(cr, user_id, order.id, body=_("Invoice received"), context=context)
+ invoiced = []
+ for po_line in order.order_line:
+ if any(line.invoice_id.state not in ['draft', 'cancel'] for line in po_line.invoice_lines):
+ invoiced.append(po_line.id)
+ if invoiced:
+ self.pool['purchase.order.line'].write(cr, uid, invoiced, {'invoiced': True})
+ workflow.trg_write(uid, 'purchase.order', order.id, cr)
return res
def confirm_paid(self, cr, uid, ids, context=None):
diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py
index 7671c4b3d86..2d5dea89d35 100644
--- a/addons/purchase/stock.py
+++ b/addons/purchase/stock.py
@@ -110,7 +110,6 @@ class stock_picking(osv.osv):
invoice_line_obj = self.pool.get('account.invoice.line')
purchase_line_obj = self.pool.get('purchase.order.line')
purchase_line_obj.write(cursor, user, [move_line.purchase_line_id.id], {
- 'invoiced': True,
'invoice_lines': [(4, invoice_line_id)],
})
return super(stock_picking, self)._invoice_line_hook(cursor, user, move_line, invoice_line_id)
diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml
index c9ffb94b985..4b0bd8b9bf5 100644
--- a/addons/sale/sale_view.xml
+++ b/addons/sale/sale_view.xml
@@ -358,7 +358,7 @@
sale.order.line.tree
sale.order.line
-
+
@@ -376,7 +376,7 @@
sale.order.line.form2
sale.order.line
-
@@ -178,7 +178,7 @@