diff --git a/addons/account/res_config.py b/addons/account/res_config.py
index 8ab93c2fcbb..09df82e38fd 100644
--- a/addons/account/res_config.py
+++ b/addons/account/res_config.py
@@ -81,31 +81,31 @@ class account_config_settings(osv.osv_memory):
'purchase_refund_sequence_next': fields.related('purchase_refund_journal_id', 'sequence_id', 'number_next', type='integer', string='Next supplier credit note number'),
'module_account_check_writing': fields.boolean('Pay your suppliers by check',
- help="""This allows you to check writing and printing.
- This installs the module account_check_writing."""),
+ help='This allows you to check writing and printing.\n'
+ '-This installs the module account_check_writing.'),
'module_account_accountant': fields.boolean('Full accounting features: journals, legal statements, chart of accounts, etc.',
help="""If you do not check this box, you will be able to do invoicing & payments, but not accounting (Journal Items, Chart of Accounts, ...)"""),
'module_account_asset': fields.boolean('Assets management',
- help="""This allows you to manage the assets owned by a company or a person.
- It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.
- This installs the module account_asset. If you do not check this box, you will be able to do invoicing & payments,
- but not accounting (Journal Items, Chart of Accounts, ...)"""),
+ help='This allows you to manage the assets owned by a company or a person.\n'
+ 'It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.\n'
+ '-This installs the module account_asset. If you do not check this box, you will be able to do invoicing & payments, '
+ 'but not accounting (Journal Items, Chart of Accounts, ...)'),
'module_account_budget': fields.boolean('Budget management',
- help="""This allows accountants to manage analytic and crossovered budgets.
- Once the master budgets and the budgets are defined,
- the project managers can set the planned amount on each analytic account.
- This installs the module account_budget."""),
+ help='This allows accountants to manage analytic and crossovered budgets. '
+ 'Once the master budgets and the budgets are defined, '
+ 'the project managers can set the planned amount on each analytic account.\n'
+ '-This installs the module account_budget.'),
'module_account_payment': fields.boolean('Manage payment orders',
- help="""This allows you to create and manage your payment orders, with purposes to
- * serve as base for an easy plug-in of various automated payment mechanisms, and
- * provide a more efficient way to manage invoice payments.
- This installs the module account_payment."""),
+ help='This allows you to create and manage your payment orders, with purposes to \n'
+ '* serve as base for an easy plug-in of various automated payment mechanisms, and \n'
+ '* provide a more efficient way to manage invoice payments.\n'
+ '-This installs the module account_payment.' ),
'module_account_voucher': fields.boolean('Manage customer payments',
- help="""This includes all the basic requirements of voucher entries for bank, cash, sales, purchase, expense, contra, etc.
- This installs the module account_voucher."""),
+ help='This includes all the basic requirements of voucher entries for bank, cash, sales, purchase, expense, contra, etc.\n'
+ '-This installs the module account_voucher.'),
'module_account_followup': fields.boolean('Manage customer payment follow-ups',
- help="""This allows to automate letters for unpaid invoices, with multi-level recalls.
- This installs the module account_followup."""),
+ help='This allows to automate letters for unpaid invoices, with multi-level recalls.\n'
+ '-This installs the module account_followup.'),
'group_proforma_invoices': fields.boolean('Allow pro-forma invoices',
implied_group='account.group_proforma_invoices',
help="Allows you to put invoices in pro-forma state."),
diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py
index 59be9cc4e3d..1a14f3e54da 100644
--- a/addons/base_setup/res_config.py
+++ b/addons/base_setup/res_config.py
@@ -32,8 +32,8 @@ class base_config_settings(osv.osv_memory):
_columns = {
'module_multi_company': fields.boolean('Manage multiple companies',
- help="""Work in multi-company environments, with appropriate security access between companies.
- This installs the module multi_company."""),
+ help='Work in multi-company environments, with appropriate security access between companies.\n'
+ '-This installs the module multi_company.'),
'module_share': fields.boolean('Allow documents sharing',
help="""Share or embbed any screen of openerp."""),
'module_portal': fields.boolean('Activate the customer portal',
@@ -88,18 +88,20 @@ class sale_config_settings(osv.osv_memory):
'module_crm': fields.boolean('CRM'),
'module_sale' : fields.boolean('SALE'),
'module_plugin_thunderbird': fields.boolean('Enable Thunderbird plug-in',
- help="""The plugin allows you archive email and its attachments to the selected
- OpenERP objects. You can select a partner, or a lead and
- attach the selected mail as a .eml file in
- the attachment of a selected record. You can create documents for CRM Lead,
- Partner from the selected emails.
- This installs the module plugin_thunderbird."""),
+ help='The plugin allows you archive email and its attachments to the selected '
+ 'OpenERP objects. You can select a partner, or a lead and '
+ 'attach the selected mail as a .eml file in '
+ 'the attachment of a selected record. You can create documents for CRM Lead, '
+ 'Partner from the selected emails.\n'
+ '-This installs the module plugin_thunderbird.'),
'module_plugin_outlook': fields.boolean('Enable Outlook plug-in',
- help="""The Outlook plugin allows you to select an object that you would like to add
- to your email and its attachments from MS Outlook. You can select a partner,
- or a lead object and archive a selected
- email into an OpenERP mail message with attachments.
- This installs the module plugin_outlook."""),
+ help='The Outlook plugin allows you to select an object that you would like to add '
+ 'to your email and its attachments from MS Outlook. You can select a partner, '
+ 'or a lead object and archive a selected email into an OpenERP mail message with attachments.\n'
+ '-This installs the module plugin_outlook.'),
+ 'module_mass_mailing': fields.boolean(
+ 'Manage mass mailing campaigns',
+ help='Get access to statistics with your mass mailing, manage campaigns.'),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/base_setup/res_config_view.xml b/addons/base_setup/res_config_view.xml
index 8004d444ed9..7f15a79429f 100644
--- a/addons/base_setup/res_config_view.xml
+++ b/addons/base_setup/res_config_view.xml
@@ -131,6 +131,10 @@
+
+
+
+
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index e49ae818cae..9d17526c19f 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -253,6 +253,8 @@ class crm_lead(format_address, osv.osv):
multi='day_close', type="float", store=True),
'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
+ # Messaging and marketing
+ 'message_bounce': fields.integer('Bounce'),
# Only used for type opportunity
'probability': fields.float('Success Rate (%)', group_operator="avg"),
'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml
index d885627a2c2..c0eefcc8d4c 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -169,16 +169,17 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/addons/crm/res_config.py b/addons/crm/res_config.py
index ea37c24fe06..5f3ffe272e7 100644
--- a/addons/crm/res_config.py
+++ b/addons/crm/res_config.py
@@ -58,10 +58,11 @@ class crm_configuration(osv.TransientModel):
implied_group='crm.group_fund_raising',
help="""Allows you to trace and manage your activities for fund raising."""),
'module_crm_claim': fields.boolean("Manage Customer Claims",
- help="""Allows you to track your customers/suppliers claims and grievances.
- This installs the module crm_claim."""),
+ help='Allows you to track your customers/suppliers claims and grievances.\n'
+ '-This installs the module crm_claim.'),
'module_crm_helpdesk': fields.boolean("Manage Helpdesk and Support",
- help="""Allows you to communicate with Customer, process Customer query, and provide better help and support. This installs the module crm_helpdesk."""),
+ help='Allows you to communicate with Customer, process Customer query, and provide better help and support.\n'
+ '-This installs the module crm_helpdesk.'),
'group_multi_salesteams': fields.boolean("Organize Sales activities into multiple Sales Teams",
implied_group='base.group_multi_salesteams',
help="""Allows you to use Sales Teams to manage your leads and opportunities."""),
diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py
index 5f264cbbc7c..ed35349f420 100644
--- a/addons/email_template/email_template.py
+++ b/addons/email_template/email_template.py
@@ -75,7 +75,7 @@ class email_template(osv.osv):
_description = 'Email Templates'
_order = 'name'
- def render_template(self, cr, uid, template, model, res_id, context=None):
+ def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
"""Render the given template text, replace mako expressions ``${expr}``
with the result of evaluating these expressions with
an evaluation context containing:
@@ -87,46 +87,60 @@ class email_template(osv.osv):
:param str template: the template text to render
:param str model: model name of the document record this mail is related to.
- :param int res_id: id of the document record this mail is related to.
+ :param int res_ids: list of ids of document records those mails are related to.
"""
- if not template:
- return u""
if context is None:
context = {}
- try:
- template = tools.ustr(template)
- record = None
- if res_id:
- record = self.pool[model].browse(cr, uid, res_id, context=context)
- user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
- variables = {
- 'object': record,
- 'user': user,
- 'ctx': context, # context kw would clash with mako internals
- }
- result = mako_template_env.from_string(template).render(variables)
- if result == u"False":
- result = u""
- return result
- except Exception:
- _logger.exception("failed to render mako template value %r", template)
- return u""
+ results = dict.fromkeys(res_ids, u"")
- def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
+ # try to load the template
+ try:
+ template = mako_template_env.from_string(tools.ustr(template))
+ except Exception:
+ _logger.exception("Failed to load template %r", template)
+ return results
+
+ # prepare template variables
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ records = self.pool[model].browse(cr, uid, res_ids, context=context) or [None]
+ variables = {
+ 'user': user,
+ 'ctx': context, # context kw would clash with mako internals
+ }
+ for record in records:
+ res_id = record.id if record else None
+ variables['object'] = record
+ try:
+ render_result = template.render(variables)
+ except Exception:
+ _logger.exception("Failed to render template %r using values %r" % (template, variables))
+ render_result = u""
+ if render_result == u"False":
+ render_result = u""
+ results[res_id] = render_result
+ return results
+
+ def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None):
if context is None:
context = {}
+ if res_ids is None:
+ res_ids = [None]
+ results = dict.fromkeys(res_ids, False)
+
if not template_id:
- return False
+ return results
template = self.browse(cr, uid, template_id, context)
- lang = self.render_template(cr, uid, template.lang, template.model, record_id, context)
- if lang:
- # Use translated template if necessary
- ctx = context.copy()
- ctx['lang'] = lang
- template = self.browse(cr, uid, template.id, ctx)
- else:
- template = self.browse(cr, uid, int(template_id), context)
- return template
+ langs = self.render_template_batch(cr, uid, template.lang, template.model, res_ids, context)
+ for res_id, lang in langs.iteritems():
+ if lang:
+ # Use translated template if necessary
+ ctx = context.copy()
+ ctx['lang'] = lang
+ template = self.browse(cr, uid, template.id, ctx)
+ else:
+ template = self.browse(cr, uid, int(template_id), context)
+ results[res_id] = template
+ return results
def onchange_model_id(self, cr, uid, ids, model_id, context=None):
mod_name = False
@@ -308,64 +322,75 @@ class email_template(osv.osv):
})
return {'value': result}
- def generate_email(self, cr, uid, template_id, res_id, context=None):
- """Generates an email from the template for given (model, res_id) pair.
+ 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.
- :param template_id: id of the template to render.
- :param res_id: id of the record to use for rendering the template (model
- is taken from template definition)
- :returns: a dict containing all relevant fields for creating a new
- mail.mail entry, with one extra key ``attachments``, in the
- format expected by :py:meth:`mail_thread.message_post`.
+ :param template_id: id of the template to render.
+ :param res_id: id of the record to use for rendering the template (model
+ is taken from template definition)
+ :returns: a dict containing all relevant fields for creating a new
+ mail.mail entry, with one extra key ``attachments``, in the
+ format expected by :py:meth:`mail_thread.message_post`.
"""
if context is None:
context = {}
+ if fields is None:
+ fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']
+
report_xml_pool = self.pool.get('ir.actions.report.xml')
- template = self.get_email_template(cr, uid, template_id, res_id, context)
- values = {}
- for field in ['subject', 'body_html', 'email_from',
- 'email_to', 'partner_to', 'email_cc', 'reply_to']:
- values[field] = self.render_template(cr, uid, getattr(template, field),
- template.model, res_id, context=context) \
- or False
- if 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)
+ res_ids_to_templates = self.get_email_template_batch(cr, uid, template_id, res_ids, context)
- if values['body_html']:
- values['body'] = tools.html_sanitize(values['body_html'])
+ # templates: res_id -> template; template -> res_ids
+ templates_to_res_ids = {}
+ for res_id, template in res_ids_to_templates.iteritems():
+ templates_to_res_ids.setdefault(template, []).append(res_id)
- values.update(mail_server_id=template.mail_server_id.id or False,
- auto_delete=template.auto_delete,
- model=template.model,
- res_id=res_id or False)
+ results = dict()
+ for template, template_res_ids in templates_to_res_ids.iteritems():
+ # generate fields value for all res_ids linked to the current template
+ for field in ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']:
+ generated_field_values = self.render_template_batch(cr, uid, getattr(template, field), template.model, template_res_ids, context=context)
+ for res_id, field_value in generated_field_values.iteritems():
+ results.setdefault(res_id, dict())[field] = field_value
+ # update values for all res_ids
+ for res_id in template_res_ids:
+ values = results[res_id]
+ if 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['body_html']:
+ values['body'] = tools.html_sanitize(values['body_html'])
+ values.update(
+ mail_server_id=template.mail_server_id.id or False,
+ auto_delete=template.auto_delete,
+ model=template.model,
+ res_id=res_id or False,
+ attachment_ids=[attach.id for attach in template.attachment_ids],
+ )
- attachments = []
- # Add report in attachments
- if template.report_template:
- report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context)
- report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
- # Ensure report is rendered using template's language
- ctx = context.copy()
- if template.lang:
- ctx['lang'] = self.render_template(cr, uid, template.lang, template.model, res_id, context)
- result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)
- result = base64.b64encode(result)
- if not report_name:
- report_name = 'report.' + report_service
- ext = "." + format
- if not report_name.endswith(ext):
- report_name += ext
- attachments.append((report_name, result))
+ # Add report in attachments
+ if template.report_template:
+ for res_id in template_res_ids:
+ attachments = []
+ report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context)
+ report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
+ # Ensure report is rendered using template's language
+ ctx = context.copy()
+ if template.lang:
+ ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, res_id, context) # take 0 ?
+ result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)
+ result = base64.b64encode(result)
+ if not report_name:
+ report_name = 'report.' + report_service
+ ext = "." + format
+ if not report_name.endswith(ext):
+ report_name += ext
+ attachments.append((report_name, result))
- attachment_ids = []
- # Add template attachments
- for attach in template.attachment_ids:
- attachment_ids.append(attach.id)
+ values['attachments'] = attachments
- values['attachments'] = attachments
- values['attachment_ids'] = attachment_ids
- return values
+ return results
def send_mail(self, cr, uid, template_id, res_id, force_send=False, raise_exception=False, context=None):
"""Generates a new mail message for the given template and record,
@@ -412,4 +437,14 @@ class email_template(osv.osv):
mail_mail.send(cr, uid, [msg_id], raise_exception=raise_exception, context=context)
return msg_id
+ # Compatibility method
+ def render_template(self, cr, uid, template, model, res_id, context=None):
+ return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id]
+
+ def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
+ return self.get_email_template_batch(cr, uid, template_id, [record_id], context)[record_id]
+
+ def generate_email(self, cr, uid, template_id, res_id, context=None):
+ return self.generate_email_batch(cr, uid, template_id, [res_id], context)[res_id]
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py
index d5feea9ebc4..ccc8a49b875 100644
--- a/addons/email_template/tests/test_mail.py
+++ b/addons/email_template/tests/test_mail.py
@@ -20,10 +20,10 @@
##############################################################################
import base64
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
-class test_message_compose(TestMailBase):
+class test_message_compose(TestMail):
def setUp(self):
super(test_message_compose, self).setUp()
@@ -73,7 +73,7 @@ class test_message_compose(TestMailBase):
# 1. Comment on pigs
compose_id = mail_compose.create(cr, uid,
- {'subject': 'Forget me subject', 'body': '
Dummy body
'},
+ {'subject': 'Forget me subject', 'body': '
Dummy body
', 'post': True},
{'default_composition_mode': 'comment',
'default_model': 'mail.group',
'default_res_id': self.group_pigs_id,
@@ -101,7 +101,7 @@ class test_message_compose(TestMailBase):
'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'}, context)
+ compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, 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', [])]
@@ -145,7 +145,7 @@ class test_message_compose(TestMailBase):
'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'}, context)
+ compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, 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', [])]
diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py
index 0aab765efbf..bae7a0d8f9c 100644
--- a/addons/email_template/wizard/mail_compose_message.py
+++ b/addons/email_template/wizard/mail_compose_message.py
@@ -62,6 +62,7 @@ class mail_compose_message(osv.TransientModel):
for wizard in self.browse(cr, uid, ids, context=context):
if wizard.template_id:
wizard_context['mail_notify_user_signature'] = False # template user_signature is added when generating body_html
+ wizard_context['mail_auto_delete'] = wizard.template_id.auto_delete # mass mailing: use template auto_delete value -> note, for emails mass mailing only
if not wizard.attachment_ids or wizard.composition_mode == 'mass_mail' or not wizard.template_id:
continue
new_attachment_ids = []
@@ -81,7 +82,7 @@ class mail_compose_message(osv.TransientModel):
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(cr, uid, template_id, res_id, context=context)
+ 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', [])
@@ -147,45 +148,55 @@ class mail_compose_message(osv.TransientModel):
partner_ids.append(int(partner_id))
return partner_ids
- def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):
+ def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None):
""" Call email_template.generate_email(), get fields relevant for
mail.compose.message, transform email_cc and email_to into partner_ids """
- template_values = self.pool.get('email.template').generate_email(cr, uid, template_id, res_id, context=context)
# filter template values
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments', 'mail_server_id']
- values = dict((field, template_values[field]) for field in fields if template_values.get(field))
- values['body'] = values.pop('body_html', '')
+ values = dict.fromkeys(res_ids, False)
- # 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, 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:
- values['partner_ids'] = list(partner_ids)
+ template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, context=context)
+ for res_id in res_ids:
+ res_id_values = dict((field, template_values[res_id][field]) for field in 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(self, cr, uid, wizard, res_id, context=None):
+ def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
""" Override to handle templates. """
- # generate the composer email
+ # generate template-based values
if wizard.template_id:
- values = self.generate_email_for_composer(cr, uid, wizard.template_id.id, res_id, context=context)
+ template_values = self.generate_email_for_composer_batch(cr, uid, wizard.template_id.id, res_ids, context=context)
else:
- values = {}
- # remove attachments as they should not be rendered
- values.pop('attachment_ids', None)
- # get values to return
- email_dict = super(mail_compose_message, self).render_message(cr, uid, wizard, res_id, context)
- # those values are not managed; they are readonly
- email_dict.pop('email_to', None)
- email_dict.pop('email_cc', None)
- email_dict.pop('partner_to', None)
- # update template values by wizard values
- values.update(email_dict)
- return values
+ 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)
- def render_template(self, cr, uid, template, model, res_id, context=None):
- return self.pool.get('email.template').render_template(cr, uid, template, model, res_id, context=context)
+ 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)
+ # update template values by composer values
+ template_values[res_id].update(composer_values[res_id])
+ return template_values
+
+ def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
+ return self.pool.get('email.template').render_template_batch(cr, uid, template, model, res_ids, context=context)
+
+ # Compatibility methods
+ def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):
+ return self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context)[res_id]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/email_template/wizard/mail_compose_message_view.xml b/addons/email_template/wizard/mail_compose_message_view.xml
index 3cb83896ca2..b7c57af2679 100644
--- a/addons/email_template/wizard/mail_compose_message_view.xml
+++ b/addons/email_template/wizard/mail_compose_message_view.xml
@@ -11,7 +11,7 @@
-
diff --git a/addons/hr_recruitment/res_config.py b/addons/hr_recruitment/res_config.py
index ac53bf74ff3..d18ca7de477 100644
--- a/addons/hr_recruitment/res_config.py
+++ b/addons/hr_recruitment/res_config.py
@@ -27,11 +27,11 @@ class hr_applicant_settings(osv.osv_memory):
_columns = {
'module_document_ftp': fields.boolean('Allow the automatic indexation of resumes',
- help="""Manage your CV's and motivation letter related to all applicants.
- This installs the module document_ftp. This will install the knowledge management module in order to allow you to search using specific keywords through the content of all documents (PDF, .DOCx...)"""),
+ help='Manage your CV\'s and motivation letter related to all applicants.\n'
+ '-This installs the module document_ftp. This will install the knowledge management module in order to allow you to search using specific keywords through the content of all documents (PDF, .DOCx...)'),
'fetchmail_applicants': fields.boolean('Create applicants from an incoming email account',
fetchmail_model='hr.applicant', fetchmail_name='Incoming HR Applications',
- help ="""Allow applicants to send their job application to an email address (jobs@mycompany.com),
- and create automatically application documents in the system."""),
+ help='Allow applicants to send their job application to an email address (jobs@mycompany.com), '
+ 'and create automatically application documents in the system.'),
}
diff --git a/addons/hr_timesheet_sheet/res_config.py b/addons/hr_timesheet_sheet/res_config.py
index e767b4da8fa..6e63b2badc9 100644
--- a/addons/hr_timesheet_sheet/res_config.py
+++ b/addons/hr_timesheet_sheet/res_config.py
@@ -28,8 +28,8 @@ class hr_timesheet_settings(osv.osv_memory):
'timesheet_range': fields.selection([('day','Day'),('week','Week'),('month','Month')],
'Validate timesheets every', help="Periodicity on which you validate your timesheets."),
'timesheet_max_difference': fields.float('Allow a difference of time between timesheets and attendances of (in hours)',
- help="""Allowed difference in hours between the sign in/out and the timesheet
- computation for one sheet. Set this to 0 if you do not want any control."""),
+ help='Allowed difference in hours between the sign in/out and the timesheet '
+ 'computation for one sheet. Set this to 0 if you do not want any control.'),
}
def get_default_timesheet(self, cr, uid, fields, context=None):
diff --git a/addons/knowledge/res_config.py b/addons/knowledge/res_config.py
index fddbb5ebdc3..c1e0bc1e1d3 100644
--- a/addons/knowledge/res_config.py
+++ b/addons/knowledge/res_config.py
@@ -28,15 +28,15 @@ class knowledge_config_settings(osv.osv_memory):
'module_document_page': fields.boolean('Create static web pages',
help="""This installs the module document_page."""),
'module_document': fields.boolean('Manage documents',
- help="""This is a complete document management system, with: user authentication,
- full document search (but pptx and docx are not supported), and a document dashboard.
- This installs the module document."""),
+ help='This is a complete document management system, with: user authentication, '
+ 'full document search (but pptx and docx are not supported), and a document dashboard.\n'
+ '-This installs the module document.'),
'module_document_ftp': fields.boolean('Share repositories (FTP)',
- help="""Access your documents in OpenERP through an FTP interface.
- This installs the module document_ftp."""),
+ help='Access your documents in OpenERP through an FTP interface.\n'
+ '-This installs the module document_ftp.'),
'module_document_webdav': fields.boolean('Share repositories (WebDAV)',
- help="""Access your documents in OpenERP through WebDAV.
- This installs the module document_webdav."""),
+ help='Access your documents in OpenERP through WebDAV.\n'
+ '-This installs the module document_webdav.'),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py
index ef3a60987d6..959877ad098 100644
--- a/addons/mail/controllers/main.py
+++ b/addons/mail/controllers/main.py
@@ -1,17 +1,16 @@
import base64
+import psycopg2
import openerp
from openerp import SUPERUSER_ID
-import openerp.addons.web.http as oeweb
+import openerp.addons.web.http as http
from openerp.addons.web.controllers.main import content_disposition
-#----------------------------------------------------------
-# Controller
-#----------------------------------------------------------
-class MailController(oeweb.Controller):
+
+class MailController(http.Controller):
_cp_path = '/mail'
- @oeweb.httprequest
+ @http.httprequest
def download_attachment(self, req, model, id, method, attachment_id, **kw):
Model = req.session.model(model)
res = getattr(Model, method)(int(id), int(attachment_id))
@@ -24,7 +23,7 @@ class MailController(oeweb.Controller):
('Content-Disposition', content_disposition(filename, req))])
return req.not_found()
- @oeweb.jsonrequest
+ @http.jsonrequest
def receive(self, req):
""" End-point to receive mail from an external SMTP server. """
dbs = req.jsonrequest.get('databases')
diff --git a/addons/mail/doc/changelog.rst b/addons/mail/doc/changelog.rst
index a7a2cfcf2b5..5eee5f136a6 100644
--- a/addons/mail/doc/changelog.rst
+++ b/addons/mail/doc/changelog.rst
@@ -6,6 +6,15 @@ Changelog
`trunk (saas-2)`
----------------
+ - ``mass_mailing_campaign`` update
+
+ - ``mail_mail`: moved ``reply_to`` computation from ``mail_mail`` to ``mail_message``
+ where it belongs, as the field is located onto the ``mail_message`` model.
+ - ``mail_compose_message``: template rendering is now done in batch. Each template
+ is rendered for all res_ids, instead of all templates one id at a time.
+ - ``mail_thread``: to ease inheritance, processing of routes is now done in
+ message_route_process, called in message_route
+
- added support of ``active_domain`` form context, coming from the list view.
When checking the header hook, the mass mailing will be done on all records
matching the ``active_domain``.
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index ac350810aa6..0059df90530 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -77,7 +77,7 @@ class mail_notification(osv.Model):
if not cr.fetchone():
cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)')
- def get_partners_to_notify(self, cr, uid, message, partners_to_notify=None, context=None):
+ def get_partners_to_email(self, cr, uid, ids, message, context=None):
""" Return the list of partners to notify, based on their preferences.
:param browse_record message: mail.message to notify
@@ -85,13 +85,10 @@ class mail_notification(osv.Model):
the notifications to process
"""
notify_pids = []
- for notification in message.notification_ids:
+ for notification in self.browse(cr, uid, ids, context=context):
if notification.read:
continue
partner = notification.partner_id
- # If partners_to_notify specified: restrict to them
- if partners_to_notify is not None and partner.id not in partners_to_notify:
- continue
# Do not send to partners without email address defined
if not partner.email:
continue
@@ -143,14 +140,62 @@ class mail_notification(osv.Model):
company = user.company_id.name
sent_by = _('Sent by %(company)s using %(openerp)s.')
signature_company = '%s' % (sent_by % {
- 'company': company,
- 'openerp': "OpenERP"
- })
+ 'company': company,
+ 'openerp': "OpenERP"
+ })
footer = tools.append_content_to_html(footer, signature_company, plaintext=False, container_tag='div')
return footer
- def _notify(self, cr, uid, msg_id, partners_to_notify=None, context=None,
+ def update_message_notification(self, cr, uid, ids, message_id, partner_ids, context=None):
+ existing_pids = set()
+ new_pids = set()
+ new_notif_ids = []
+
+ for notification in self.browse(cr, uid, ids, context=context):
+ existing_pids.add(notification.partner_id.id)
+
+ # update existing notifications
+ self.write(cr, uid, ids, {'read': False}, context=context)
+
+ # create new notifications
+ new_pids = set(partner_ids) - existing_pids
+ for new_pid in new_pids:
+ new_notif_ids.append(self.create(cr, uid, {'message_id': message_id, 'partner_id': new_pid, 'read': False}, context=context))
+ return new_notif_ids
+
+ def _notify_email(self, cr, uid, ids, message_id, force_send=False, user_signature=True, context=None):
+ message = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context)
+
+ # compute partners
+ email_pids = self.get_partners_to_email(cr, uid, ids, message, context=None)
+ if not email_pids:
+ return True
+
+ # compute email body (signature, company data)
+ body_html = message.body
+ user_id = message.author_id and message.author_id.user_ids and message.author_id.user_ids[0] and message.author_id.user_ids[0].id or None
+ if user_signature:
+ signature_company = self.get_signature_footer(cr, uid, user_id, res_model=message.model, res_id=message.res_id, context=context)
+ body_html = tools.append_content_to_html(body_html, signature_company, plaintext=False, container_tag='div')
+
+ # compute email references
+ references = message.parent_id.message_id if message.parent_id else False
+
+ # create email values
+ mail_values = {
+ 'mail_message_id': message.id,
+ 'auto_delete': True,
+ 'body_html': body_html,
+ 'recipient_ids': [(4, id) for id in email_pids],
+ 'references': references,
+ }
+ email_notif_id = self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)
+ if force_send:
+ self.pool.get('mail.mail').send(cr, uid, [email_notif_id], context=context)
+ return True
+
+ def _notify(self, cr, uid, message_id, partners_to_notify=None, context=None,
force_send=False, user_signature=True):
""" Send by email the notification depending on the user preferences
@@ -162,57 +207,14 @@ class mail_notification(osv.Model):
:param bool user_signature: if True, the generated mail.mail body is
the body of the related mail.message with the author's signature
"""
- if context is None:
- context = {}
- mail_message_obj = self.pool.get('mail.message')
+ notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', message_id), ('partner_id', 'in', partners_to_notify)], context=context)
- # optional list of partners to notify: subscribe them if not already done or update the notification
- if partners_to_notify:
- notifications_to_update = []
- notified_partners = []
- notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', msg_id), ('partner_id', 'in', partners_to_notify)], context=context)
- for notification in self.browse(cr, SUPERUSER_ID, notif_ids, context=context):
- notified_partners.append(notification.partner_id.id)
- notifications_to_update.append(notification.id)
- partners_to_notify = filter(lambda item: item not in notified_partners, partners_to_notify)
- if notifications_to_update:
- self.write(cr, SUPERUSER_ID, notifications_to_update, {'read': False}, context=context)
- mail_message_obj.write(cr, uid, msg_id, {'notified_partner_ids': [(4, id) for id in partners_to_notify]}, context=context)
+ # update or create notifications
+ new_notif_ids = self.update_message_notification(cr, SUPERUSER_ID, notif_ids, message_id, partners_to_notify, context=context)
# mail_notify_noemail (do not send email) or no partner_ids: do not send, return
- if context.get('mail_notify_noemail'):
+ if context and context.get('mail_notify_noemail'):
return True
+
# browse as SUPERUSER_ID because of access to res_partner not necessarily allowed
- msg = self.pool.get('mail.message').browse(cr, SUPERUSER_ID, msg_id, context=context)
- notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, partners_to_notify=partners_to_notify, context=context)
- if not notify_partner_ids:
- return True
-
- # add the context in the email
- # TDE FIXME: commented, to be improved in a future branch
- # quote_context = self.pool.get('mail.message').message_quote_context(cr, uid, msg_id, context=context)
-
- # add signature
- body_html = msg.body
- user_id = msg.author_id and msg.author_id.user_ids and msg.author_id.user_ids[0] and msg.author_id.user_ids[0].id or None
- if user_signature:
- signature_company = self.get_signature_footer(cr, uid, user_id, res_model=msg.model, res_id=msg.res_id, context=context)
- body_html = tools.append_content_to_html(body_html, signature_company, plaintext=False, container_tag='div')
-
- references = False
- if msg.parent_id:
- references = msg.parent_id.message_id
-
- mail_values = {
- 'mail_message_id': msg.id,
- 'auto_delete': True,
- 'body_html': body_html,
- 'recipient_ids': [(4, id) for id in notify_partner_ids],
- 'references': references,
- }
- mail_mail = self.pool.get('mail.mail')
- email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
-
- if force_send:
- mail_mail.send(cr, uid, [email_notif_id], context=context)
- return True
+ self._notify_email(cr, SUPERUSER_ID, new_notif_ids, message_id, force_send, user_signature, context=context)
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index 9fa80b9be46..91799b0ac63 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -61,15 +61,9 @@ class mail_mail(osv.Model):
# Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
# and during unlink() we will not cascade delete the parent and its attachments
'notification': fields.boolean('Is Notification',
- help='Mail has been created to notify people of an existing mail.message')
+ help='Mail has been created to notify people of an existing mail.message'),
}
- def _get_default_from(self, cr, uid, context=None):
- """ Kept for compatibility
- TDE TODO: remove me in 8.0
- """
- return self.pool['mail.message']._get_default_from(cr, uid, context=context)
-
_defaults = {
'state': 'outgoing',
}
@@ -81,74 +75,11 @@ class mail_mail(osv.Model):
context = dict(context, default_type=None)
return super(mail_mail, self).default_get(cr, uid, fields, context=context)
- def _get_reply_to(self, cr, uid, values, context=None):
- """ Return a specific reply_to: alias of the document through message_get_reply_to
- or take the email_from
- """
- # if value specified: directly return it
- if values.get('reply_to'):
- return values.get('reply_to')
- format_name = True # whether to use a 'Followers of Pigs catchall alias
- if not email_reply_to:
- catchall_alias = ir_config_parameter.get_param(cr, uid, "mail.catchall.alias", context=context)
- if catchall_domain and catchall_alias:
- email_reply_to = '%s@%s' % (catchall_alias, catchall_domain)
-
- # still no reply_to -> reply_to will be the email_from
- if not email_reply_to and email_from:
- email_reply_to = email_from
-
- # format 'Document name '
- if email_reply_to and model and res_id and format_name:
- emails = tools.email_split(email_reply_to)
- if emails:
- email_reply_to = emails[0]
- document_name = self.pool[model].name_get(cr, SUPERUSER_ID, [res_id], context=context)[0]
- if document_name:
- # sanitize document name
- sanitized_doc_name = re.sub(r'[^\w+.]+', '-', document_name[1])
- # generate reply to
- email_reply_to = _('"Followers of %s" <%s>') % (sanitized_doc_name, email_reply_to)
-
- return email_reply_to
-
def create(self, cr, uid, values, context=None):
# notification field: if not set, set if mail comes from an existing mail.message
if 'notification' not in values and values.get('mail_message_id'):
values['notification'] = True
- mail_id = super(mail_mail, self).create(cr, uid, values, context=context)
-
- # reply_to: if not set, set with default values that require creation values
- # but delegate after creation because of mail_message.message_id automatic
- # creation using existence of reply_to
- if not values.get('reply_to'):
- reply_to = self._get_reply_to(cr, uid, values, context=context)
- if reply_to:
- self.write(cr, uid, [mail_id], {'reply_to': reply_to}, context=context)
- return mail_id
+ return super(mail_mail, self).create(cr, uid, values, context=context)
def unlink(self, cr, uid, ids, context=None):
# cascade-delete the parent message for all mails that are not created for a notification
@@ -213,11 +144,6 @@ class mail_mail(osv.Model):
# mail_mail formatting, tools and send mechanism
#------------------------------------------------------
- # TODO in 8.0(+): maybe factorize this to enable in modules link generation
- # independently of mail_mail model
- # TODO in 8.0(+): factorize doc name sanitized and 'Followers of ...' formatting
- # because it begins to appear everywhere
-
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
@@ -232,8 +158,8 @@ class mail_mail(osv.Model):
}
if mail.notification:
fragment.update({
- 'message_id': mail.mail_message_id.id,
- })
+ 'message_id': mail.mail_message_id.id,
+ })
url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
return _("""Access your messages and documents in OpenERP""") % url
else:
@@ -325,26 +251,37 @@ class mail_mail(osv.Model):
email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
for partner in mail.recipient_ids:
email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
+ # headers
+ headers = {}
+ bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
+ catchall_domain = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.catchall.domain", context=context)
+ if bounce_alias and catchall_domain:
+ if mail.model and mail.res_id:
+ headers['Return-Path'] = '%s-%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
+ else:
+ headers['Return-Path'] = '%s-%d@%s' % (bounce_alias, mail.id, catchall_domain)
# build an RFC2822 email.message.Message object and send it without queuing
res = None
for email in email_list:
msg = ir_mail_server.build_email(
- email_from = mail.email_from,
- email_to = email.get('email_to'),
- subject = email.get('subject'),
- body = email.get('body'),
- body_alternative = email.get('body_alternative'),
- email_cc = tools.email_split(mail.email_cc),
- reply_to = mail.reply_to,
- attachments = attachments,
- message_id = mail.message_id,
- references = mail.references,
- object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
- subtype = 'html',
- subtype_alternative = 'plain')
+ email_from=mail.email_from,
+ email_to=email.get('email_to'),
+ subject=email.get('subject'),
+ body=email.get('body'),
+ body_alternative=email.get('body_alternative'),
+ email_cc=tools.email_split(mail.email_cc),
+ reply_to=mail.reply_to,
+ attachments=attachments,
+ message_id=mail.message_id,
+ references=mail.references,
+ object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
+ subtype='html',
+ subtype_alternative='plain',
+ headers=headers)
res = ir_mail_server.send_email(cr, uid, msg,
- mail_server_id=mail.mail_server_id.id, context=context)
+ mail_server_id=mail.mail_server_id.id,
+ context=context)
if res:
mail.write({'state': 'sent', 'message_id': res})
mail_sent = True
diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml
index f3728d4653a..dd6a78fd8a4 100644
--- a/addons/mail/mail_mail_view.xml
+++ b/addons/mail/mail_mail_view.xml
@@ -14,7 +14,7 @@
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 4bff9413a4a..3719dbc1c63 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -20,6 +20,8 @@
##############################################################################
import logging
+import re
+
from openerp import tools
from email.header import decode_header
@@ -98,7 +100,7 @@ class mail_message(osv.Model):
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)
- partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
notif_obj = self.pool.get('mail.notification')
notif_ids = notif_obj.search(cr, uid, [
('partner_id', 'in', [partner_id]),
@@ -117,7 +119,7 @@ class mail_message(osv.Model):
def _get_starred(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)
- partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
notif_obj = self.pool.get('mail.notification')
notif_ids = notif_obj.search(cr, uid, [
('partner_id', 'in', [partner_id]),
@@ -206,11 +208,11 @@ class mail_message(osv.Model):
raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
def _get_default_author(self, cr, uid, context=None):
- return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
_defaults = {
'type': 'email',
- 'date': lambda *a: fields.datetime.now(),
+ 'date': fields.datetime.now(),
'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx),
'body': '',
'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
@@ -264,7 +266,7 @@ class mail_message(osv.Model):
:return number of message mark as read
"""
notification_obj = self.pool.get('mail.notification')
- user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
if not create_missing:
domain += [('read', '=', not read)]
@@ -292,7 +294,7 @@ class mail_message(osv.Model):
(i.e. when acting on displayed messages not notified)
"""
notification_obj = self.pool.get('mail.notification')
- user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
if not create_missing:
domain += [('starred', '=', not starred)]
@@ -330,7 +332,7 @@ class mail_message(osv.Model):
"""
res_partner_obj = self.pool.get('res.partner')
ir_attachment_obj = self.pool.get('ir.attachment')
- pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
+ pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
# 1. Aggregate partners (author_id and partner_ids) and attachments
partner_ids = set()
@@ -646,7 +648,7 @@ class mail_message(osv.Model):
elif not ids:
return ids
- pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
+ pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
model_ids = {}
@@ -706,7 +708,7 @@ class mail_message(osv.Model):
ids = [ids]
not_obj = self.pool.get('mail.notification')
fol_obj = self.pool.get('mail.followers')
- partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
+ partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id
# Read mail_message.ids to have their values
message_values = dict.fromkeys(ids)
@@ -775,17 +777,66 @@ 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_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
+ """
+ email_reply_to = None
+
+ ir_config_parameter = self.pool.get("ir.config_parameter")
+ catchall_domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)
+
+ # model, res_id, email_from: comes from values OR related message
+ model, res_id, email_from = values.get('model'), values.get('res_id'), values.get('email_from')
+
+ # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
+ if not email_reply_to and model and res_id and catchall_domain and hasattr(self.pool[model], 'message_get_reply_to'):
+ email_reply_to = self.pool[model].message_get_reply_to(cr, uid, [res_id], context=context)[0]
+ # no alias reply_to -> catchall alias
+ if not email_reply_to and catchall_domain:
+ catchall_alias = ir_config_parameter.get_param(cr, uid, "mail.catchall.alias", context=context)
+ if catchall_alias:
+ email_reply_to = '%s@%s' % (catchall_alias, catchall_domain)
+ # still no reply_to -> reply_to will be the email_from
+ if not email_reply_to and email_from:
+ email_reply_to = email_from
+
+ # format 'Document name '
+ if email_reply_to and model and res_id:
+ emails = tools.email_split(email_reply_to)
+ if emails:
+ email_reply_to = emails[0]
+ document_name = self.pool[model].name_get(cr, SUPERUSER_ID, [res_id], context=context)[0]
+ if document_name:
+ # sanitize document name
+ sanitized_doc_name = re.sub(r'[^\w+.]+', '-', document_name[1])
+ # generate reply to
+ email_reply_to = _('"Followers of %s" <%s>') % (sanitized_doc_name, email_reply_to)
+
+ return email_reply_to
+
+ def _get_message_id(self, cr, uid, values, context=None):
+ message_id = None
+ if not values.get('message_id') and values.get('reply_to'):
+ message_id = tools.generate_tracking_message_id('reply_to')
+ elif not values.get('message_id') and values.get('res_id') and values.get('model'):
+ message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
+ elif not values.get('message_id'):
+ message_id = tools.generate_tracking_message_id('private')
+ return message_id
+
def create(self, cr, uid, values, context=None):
if context is None:
context = {}
default_starred = context.pop('default_starred', False)
- # generate message_id, to redirect answers to the right discussion thread
- if not values.get('message_id') and values.get('reply_to'):
- values['message_id'] = tools.generate_tracking_message_id('reply_to')
- elif not values.get('message_id') and values.get('res_id') and values.get('model'):
- values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
- elif not values.get('message_id'):
- values['message_id'] = tools.generate_tracking_message_id('private')
+
+ if 'email_from' not in values: # needed to compute reply_to
+ values['email_from'] = self._get_default_from(cr, uid, context=context)
+ if not values.get('message_id'):
+ 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)
+
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),
@@ -915,26 +966,28 @@ class mail_message(osv.Model):
if message.subtype_id and message.model and message.res_id:
fol_obj = self.pool.get("mail.followers")
# browse as SUPERUSER because rules could restrict the search results
- fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
- ('res_model', '=', message.model),
- ('res_id', '=', message.res_id),
- ('subtype_ids', 'in', message.subtype_id.id)
+ fol_ids = fol_obj.search(
+ 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 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))
# 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])
+ partners_to_notify |= set([message.author_id.id])
elif message.author_id:
- partners_to_notify -= set([message.author_id])
+ partners_to_notify -= set([message.author_id.id])
# all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!)
if message.partner_ids:
- partners_to_notify |= set(message.partner_ids)
+ partners_to_notify |= set([p.id for p in message.partner_ids])
# notify
- if partners_to_notify:
- notification_obj._notify(cr, uid, newid, partners_to_notify=[p.id for p in partners_to_notify], context=context,
- force_send=force_send, user_signature=user_signature)
+ notification_obj._notify(
+ cr, uid, newid, partners_to_notify=list(partners_to_notify), context=context,
+ force_send=force_send, user_signature=user_signature
+ )
message.refresh()
# An error appear when a user receive a notification without notifying
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml
index e4442e366e3..e2d1deb028b 100644
--- a/addons/mail/mail_message_view.xml
+++ b/addons/mail/mail_message_view.xml
@@ -56,7 +56,8 @@
25
-
+
+
@@ -66,23 +67,13 @@
-
-
-
-
-
+
+
+
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index ebb7a40c719..d72c24f6a99 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -874,6 +874,40 @@ class mail_thread(osv.AbstractModel):
"No possible route found for incoming message from %s to %s (Message-Id %s:)." \
"Create an appropriate mail.alias or force the destination model." % (email_from, email_to, message_id)
+ def message_route_process(self, cr, uid, message, message_dict, routes, context=None):
+ # postpone setting message_dict.partner_ids after message_post, to avoid double notifications
+ partner_ids = message_dict.pop('partner_ids', [])
+ thread_id = False
+ for model, thread_id, custom_values, user_id, alias in routes:
+ if self._name == 'mail.thread':
+ context.update({'thread_model': model})
+ if model:
+ model_pool = self.pool[model]
+ assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
+ "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
+ (message_dict['message_id'], model)
+
+ # disabled subscriptions during message_new/update to avoid having the system user running the
+ # email gateway become a follower of all inbound messages
+ nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True)
+ if thread_id and hasattr(model_pool, 'message_update'):
+ model_pool.message_update(cr, user_id, [thread_id], message_dict, context=nosub_ctx)
+ else:
+ thread_id = model_pool.message_new(cr, user_id, message_dict, custom_values, context=nosub_ctx)
+ else:
+ assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
+ model_pool = self.pool.get('mail.thread')
+ if not hasattr(model_pool, 'message_post'):
+ context['thread_model'] = model
+ model_pool = self.pool['mail.thread']
+ new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **message_dict)
+
+ if partner_ids:
+ # postponed after message_post, because this is an external message and we don't want to create
+ # duplicate emails due to notifications
+ self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
+ return thread_id
+
def message_process(self, cr, uid, model, message, custom_values=None,
save_original=False, strip_attachments=False,
thread_id=None, context=None):
@@ -926,8 +960,7 @@ class mail_thread(osv.AbstractModel):
msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context)
if strip_attachments:
msg.pop('attachments', None)
- # postpone setting msg.partner_ids after message_post, to avoid double notifications
- partner_ids = msg.pop('partner_ids', [])
+
if msg.get('message_id'): # should always be True as message_parse generate one if missing
existing_msg_ids = self.pool.get('mail.message').search(cr, SUPERUSER_ID, [
('message_id', '=', msg.get('message_id')),
@@ -939,36 +972,7 @@ class mail_thread(osv.AbstractModel):
# find possible routes for the message
routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context)
- thread_id = False
- for model, thread_id, custom_values, user_id, alias in routes:
- if self._name == 'mail.thread':
- context.update({'thread_model': model})
- if model:
- model_pool = self.pool[model]
- assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
- "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
- (msg['message_id'], model)
-
- # disabled subscriptions during message_new/update to avoid having the system user running the
- # email gateway become a follower of all inbound messages
- nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True)
- if thread_id and hasattr(model_pool, 'message_update'):
- model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
- else:
- thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
- else:
- assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
- model_pool = self.pool.get('mail.thread')
- if not hasattr(model_pool, 'message_post'):
- context['thread_model'] = model
- model_pool = self.pool['mail.thread']
- new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
-
- if partner_ids:
- # postponed after message_post, because this is an external message and we don't want to create
- # duplicate emails due to notifications
- self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
-
+ thread_id = self.message_route_process(cr, uid, msg_txt, msg, routes, context=context)
return thread_id
def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
@@ -1274,8 +1278,8 @@ class mail_thread(osv.AbstractModel):
return result
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
- subtype=None, parent_id=False, attachments=None, context=None,
- content_subtype='html', **kwargs):
+ subtype=None, parent_id=False, attachments=None, context=None,
+ content_subtype='html', **kwargs):
""" Post a new message in an existing thread, returning the new
mail.message ID.
diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py
index ff1080b0580..242beb60bf1 100644
--- a/addons/mail/tests/__init__.py
+++ b/addons/mail/tests/__init__.py
@@ -19,9 +19,10 @@
#
##############################################################################
-from . import test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite
+from . import test_mail_group, test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite
checks = [
+ test_mail_group,
test_mail_message,
test_mail_features,
test_mail_gateway,
diff --git a/addons/mail/tests/test_mail_base.py b/addons/mail/tests/common.py
similarity index 66%
rename from addons/mail/tests/test_mail_base.py
rename to addons/mail/tests/common.py
index b0f9f72b6f7..68a329e043a 100644
--- a/addons/mail/tests/test_mail_base.py
+++ b/addons/mail/tests/common.py
@@ -22,7 +22,7 @@
from openerp.tests import common
-class TestMailBase(common.TransactionCase):
+class TestMail(common.TransactionCase):
def _mock_smtp_gateway(self, *args, **kwargs):
return args[2]['Message-Id']
@@ -39,7 +39,7 @@ class TestMailBase(common.TransactionCase):
return self._build_email(*args, **kwargs)
def setUp(self):
- super(TestMailBase, self).setUp()
+ super(TestMail, self).setUp()
cr, uid = self.cr, self.uid
# Install mock SMTP gateway
@@ -68,12 +68,46 @@ class TestMailBase(common.TransactionCase):
group_employee_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user')
self.group_employee_id = group_employee_ref and group_employee_ref[1] or False
+ # Partner Data
+
+ # User Data: employee, noone
+ self.user_employee_id = self.res_users.create(cr, uid, {
+ 'name': 'Ernest Employee',
+ 'login': 'ernest',
+ 'alias_name': 'ernest',
+ 'email': 'e.e@example.com',
+ 'signature': '--\nErnest',
+ 'notification_email_send': 'comment',
+ 'groups_id': [(6, 0, [self.group_employee_id])]
+ }, {'no_reset_password': True})
+ self.user_noone_id = self.res_users.create(cr, uid, {
+ 'name': 'Noemie NoOne',
+ 'login': 'noemie',
+ 'alias_name': 'noemie',
+ 'email': 'n.n@example.com',
+ 'signature': '--\nNoemie',
+ 'notification_email_send': 'comment',
+ 'groups_id': [(6, 0, [])]
+ }, {'no_reset_password': True})
+
# Test users to use through the various tests
self.res_users.write(cr, uid, uid, {'name': 'Administrator'})
- self.user_raoul_id = self.res_users.create(cr, uid,
- {'name': 'Raoul Grosbedon', 'signature': 'SignRaoul', 'email': 'raoul@raoul.fr', 'login': 'raoul', 'alias_name': 'raoul', 'groups_id': [(6, 0, [self.group_employee_id])]})
- self.user_bert_id = self.res_users.create(cr, uid,
- {'name': 'Bert Tartignole', 'signature': 'SignBert', 'email': 'bert@bert.fr', 'login': 'bert', 'alias_name': 'bert', 'groups_id': [(6, 0, [])]})
+ self.user_raoul_id = self.res_users.create(cr, uid, {
+ 'name': 'Raoul Grosbedon',
+ 'signature': 'SignRaoul',
+ 'email': 'raoul@raoul.fr',
+ 'login': 'raoul',
+ 'alias_name': 'raoul',
+ 'groups_id': [(6, 0, [self.group_employee_id])]
+ })
+ self.user_bert_id = self.res_users.create(cr, uid, {
+ 'name': 'Bert Tartignole',
+ 'signature': 'SignBert',
+ 'email': 'bert@bert.fr',
+ 'login': 'bert',
+ 'alias_name': 'bert',
+ 'groups_id': [(6, 0, [])]
+ })
self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id)
self.user_bert = self.res_users.browse(cr, uid, self.user_bert_id)
self.user_admin = self.res_users.browse(cr, uid, uid)
@@ -82,13 +116,19 @@ class TestMailBase(common.TransactionCase):
self.partner_bert_id = self.user_bert.partner_id.id
# Test 'pigs' group to use through the various tests
- self.group_pigs_id = self.mail_group.create(cr, uid,
+ self.group_pigs_id = self.mail_group.create(
+ cr, uid,
{'name': 'Pigs', 'description': 'Fans of Pigs, unite !', 'alias_name': 'group+pigs'},
- {'mail_create_nolog': True})
+ {'mail_create_nolog': True}
+ )
self.group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ # Test mail.group: public to provide access to everyone
+ self.group_jobs_id = self.mail_group.create(cr, uid, {'name': 'Jobs', 'public': 'public'})
+ # Test mail.group: private to restrict access
+ self.group_priv_id = self.mail_group.create(cr, uid, {'name': 'Private', 'public': 'private'})
def tearDown(self):
# Remove mocks
self.registry('ir.mail_server').build_email = self._build_email
self.registry('ir.mail_server').send_email = self._send_email
- super(TestMailBase, self).tearDown()
+ super(TestMail, self).tearDown()
diff --git a/addons/mail/tests/test_invite.py b/addons/mail/tests/test_invite.py
index faa9fd55793..c4484d254bb 100644
--- a/addons/mail/tests/test_invite.py
+++ b/addons/mail/tests/test_invite.py
@@ -19,10 +19,10 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
-class test_invite(TestMailBase):
+class test_invite(TestMail):
def test_00_basic_invite(self):
cr, uid = self.cr, self.uid
diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py
index e9563e19246..0688bf02606 100644
--- a/addons/mail/tests/test_mail_features.py
+++ b/addons/mail/tests/test_mail_features.py
@@ -21,12 +21,12 @@
from openerp.addons.mail.mail_mail import mail_mail
from openerp.addons.mail.mail_thread import mail_thread
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
from openerp.tools import mute_logger, email_split
from openerp.tools.mail import html_sanitize
-class test_mail(TestMailBase):
+class test_mail(TestMail):
def test_000_alias_setup(self):
""" Test basic mail.alias setup works, before trying to use them for routing """
@@ -631,6 +631,7 @@ class test_mail(TestMailBase):
{
'subject': _subject,
'body': '${object.description}',
+ 'post': True,
'partner_ids': [(4, p_c_id), (4, p_d_id)],
}, context={
'default_composition_mode': 'mass_mail',
@@ -684,6 +685,7 @@ class test_mail(TestMailBase):
{
'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 3fd8ebd3101..b2a1c3250aa 100644
--- a/addons/mail/tests/test_mail_gateway.py
+++ b/addons/mail/tests/test_mail_gateway.py
@@ -19,7 +19,7 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
from openerp.tools import mute_logger
MAIL_TEMPLATE = """Return-Path:
@@ -143,173 +143,9 @@ dGVzdAo=
--089e01536c4ed4d17204e49b8e96--"""
-class TestMailgateway(TestMailBase):
+class TestMailgateway(TestMail):
- def test_00_partner_find_from_email(self):
- """ Tests designed for partner fetch based on emails. """
- cr, uid, user_raoul, group_pigs = self.cr, self.uid, self.user_raoul, self.group_pigs
-
- # --------------------------------------------------
- # Data creation
- # --------------------------------------------------
- # 1 - Partner ARaoul
- p_a_id = self.res_partner.create(cr, uid, {'name': 'ARaoul', 'email': 'test@test.fr'})
-
- # --------------------------------------------------
- # CASE1: without object
- # --------------------------------------------------
-
- # Do: find partner with email -> first partner should be found
- partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0]
- self.assertEqual(partner_info['full_name'], 'Maybe Raoul ',
- 'mail_thread: message_partner_info_from_emails did not handle email')
- self.assertEqual(partner_info['partner_id'], p_a_id,
- 'mail_thread: message_partner_info_from_emails wrong partner found')
-
- # Data: add some data about partners
- # 2 - User BRaoul
- p_b_id = self.res_partner.create(cr, uid, {'name': 'BRaoul', 'email': 'test@test.fr', 'user_ids': [(4, user_raoul.id)]})
-
- # Do: find partner with email -> first user should be found
- partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0]
- self.assertEqual(partner_info['partner_id'], p_b_id,
- 'mail_thread: message_partner_info_from_emails wrong partner found')
-
- # --------------------------------------------------
- # CASE1: with object
- # --------------------------------------------------
-
- # Do: find partner in group where there is a follower with the email -> should be taken
- self.mail_group.message_subscribe(cr, uid, [group_pigs.id], [p_b_id])
- partner_info = self.mail_group.message_partner_info_from_emails(cr, uid, group_pigs.id, ['Maybe Raoul '], link_mail=False)[0]
- self.assertEqual(partner_info['partner_id'], p_b_id,
- 'mail_thread: message_partner_info_from_emails wrong partner found')
-
- def test_05_mail_message_mail_mail(self):
- """ Tests designed for testing email values based on mail.message, aliases, ... """
- cr, uid, user_raoul_id = self.cr, self.uid, self.user_raoul_id
-
- # Data: update + generic variables
- reply_to1 = '_reply_to1@example.com'
- reply_to2 = '_reply_to2@example.com'
- email_from1 = 'from@example.com'
- alias_domain = 'schlouby.fr'
- raoul_from = 'Raoul Grosbedon '
- raoul_from_alias = 'Raoul Grosbedon '
- raoul_reply = '"Followers of Pigs" '
- raoul_reply_alias = '"Followers of Pigs" '
- # Data: remove alias_domain to see emails with alias
- param_ids = self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.domain')])
- self.registry('ir.config_parameter').unlink(cr, uid, param_ids)
-
- # Do: free message; specified values > default values
- msg_id = self.mail_message.create(cr, user_raoul_id, {'reply_to': reply_to1, 'email_from': email_from1})
- msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
- # Test: message content
- self.assertIn('reply_to', msg.message_id,
- 'mail_message: message_id should be specific to a mail_message with a given reply_to')
- self.assertEqual(msg.reply_to, reply_to1,
- 'mail_message: incorrect reply_to: should come from values')
- self.assertEqual(msg.email_from, email_from1,
- 'mail_message: incorrect email_from: should come from values')
- # Do: create a mail_mail with the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, reply_to1,
- 'mail_mail: incorrect reply_to: should come from mail.message')
- self.assertEqual(mail.email_from, email_from1,
- 'mail_mail: incorrect email_from: should come from mail.message')
- # Do: create a mail_mail with the previous mail_message + specified reply_to
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel', 'reply_to': reply_to2})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, reply_to2,
- 'mail_mail: incorrect reply_to: should come from values')
- self.assertEqual(mail.email_from, email_from1,
- 'mail_mail: incorrect email_from: should come from mail.message')
-
- # Do: mail_message attached to a document
- msg_id = self.mail_message.create(cr, user_raoul_id, {'model': 'mail.group', 'res_id': self.group_pigs_id})
- msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
- # Test: message content
- self.assertIn('mail.group', msg.message_id,
- 'mail_message: message_id should contain model')
- self.assertIn('%s' % self.group_pigs_id, msg.message_id,
- 'mail_message: message_id should contain res_id')
- self.assertFalse(msg.reply_to,
- 'mail_message: incorrect reply_to: should not be generated if not specified')
- self.assertEqual(msg.email_from, raoul_from,
- 'mail_message: incorrect email_from: should be Raoul')
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, raoul_reply,
- 'mail_mail: incorrect reply_to: should be Raoul')
-
- # Data: set catchall domain
- self.registry('ir.config_parameter').set_param(cr, uid, 'mail.catchall.domain', alias_domain)
- self.registry('ir.config_parameter').unlink(cr, uid, self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.alias')]))
-
- # Update message
- self.mail_message.write(cr, user_raoul_id, [msg_id], {'email_from': False, 'reply_to': False})
- msg.refresh()
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, raoul_reply_alias,
- 'mail_mail: incorrect reply_to: should be Pigs alias')
-
- # Update message: test alias on email_from
- msg_id = self.mail_message.create(cr, user_raoul_id, {})
- msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, raoul_from_alias,
- 'mail_mail: incorrect reply_to: should be message email_from using Raoul alias')
-
- # Update message
- self.mail_message.write(cr, user_raoul_id, [msg_id], {'res_id': False, 'email_from': 'someone@schlouby.fr', 'reply_to': False})
- msg.refresh()
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, msg.email_from,
- 'mail_mail: incorrect reply_to: should be message email_from')
-
- # Data: set catchall alias
- self.registry('ir.config_parameter').set_param(self.cr, self.uid, 'mail.catchall.alias', 'gateway')
-
- # Update message
- self.mail_message.write(cr, uid, [msg_id], {'email_from': False, 'reply_to': False})
- msg.refresh()
- # Do: create a mail_mail based on the previous mail_message
- mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'})
- mail = self.mail_mail.browse(cr, uid, mail_id)
- # Test: mail_mail Content-Type
- self.assertEqual(mail.reply_to, 'gateway@schlouby.fr',
- 'mail_mail: reply_to should equal the catchall email alias')
-
- # Do: create a mail_mail
- mail_id = self.mail_mail.create(cr, uid, {'state': 'cancel'})
- mail = self.mail_mail.browse(cr, uid, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, 'gateway@schlouby.fr',
- 'mail_mail: reply_to should equal the catchall email alias')
-
- # Do: create a mail_mail
- mail_id = self.mail_mail.create(cr, uid, {'state': 'cancel', 'reply_to': 'someone@example.com'})
- mail = self.mail_mail.browse(cr, uid, mail_id)
- # Test: mail_mail content
- self.assertEqual(mail.reply_to, 'someone@example.com',
- 'mail_mail: reply_to should equal the rpely_to given to create')
-
- def test_09_message_parse(self):
+ def test_00_message_parse(self):
""" Testing incoming emails parsing """
cr, uid = self.cr, self.uid
@@ -738,9 +574,7 @@ class TestMailgateway(TestMailBase):
'message_post: private discussion: incorrect notified recipients')
self.assertEqual(msg.model, False,
'message_post: private discussion: context key "thread_model" not correctly ignored when having no res_id')
- # Test: message reply_to and message-id
- self.assertFalse(msg.reply_to,
- 'message_post: private discussion: initial message should not have any reply_to specified')
+ # Test: message-id
self.assertIn('openerp-private', msg.message_id,
'message_post: private discussion: message-id should contain the private keyword')
diff --git a/addons/mail/tests/test_mail_group.py b/addons/mail/tests/test_mail_group.py
new file mode 100644
index 00000000000..c1512441b13
--- /dev/null
+++ b/addons/mail/tests/test_mail_group.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2012-TODAY 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.addons.mail.tests.common import TestMail
+from openerp.osv.orm import except_orm
+from openerp.tools import mute_logger
+
+
+class TestMailGroup(TestMail):
+
+ @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
+ def test_00_mail_group_access_rights(self):
+ """ Testing mail_group access rights and basic mail_thread features """
+ cr, uid, user_noone_id, user_employee_id = self.cr, self.uid, self.user_noone_id, self.user_employee_id
+
+ # Do: Bert reads Jobs -> ok, public
+ self.mail_group.read(cr, user_noone_id, [self.group_jobs_id])
+ # Do: Bert read Pigs -> ko, restricted to employees
+ with self.assertRaises(except_orm):
+ self.mail_group.read(cr, user_noone_id, [self.group_pigs_id])
+ # Do: Raoul read Pigs -> ok, belong to employees
+ self.mail_group.read(cr, user_employee_id, [self.group_pigs_id])
+
+ # Do: Bert creates a group -> ko, no access rights
+ with self.assertRaises(except_orm):
+ self.mail_group.create(cr, user_noone_id, {'name': 'Test'})
+ # Do: Raoul creates a restricted group -> ok
+ new_group_id = self.mail_group.create(cr, user_employee_id, {'name': 'Test'})
+ # Do: Bert added in followers, read -> ok, in followers
+ self.mail_group.message_subscribe_users(cr, uid, [new_group_id], [user_noone_id])
+ self.mail_group.read(cr, user_noone_id, [new_group_id])
+
+ # Do: Raoul reads Priv -> ko, private
+ with self.assertRaises(except_orm):
+ self.mail_group.read(cr, user_employee_id, [self.group_priv_id])
+ # Do: Raoul added in follower, read -> ok, in followers
+ self.mail_group.message_subscribe_users(cr, uid, [self.group_priv_id], [user_employee_id])
+ self.mail_group.read(cr, user_employee_id, [self.group_priv_id])
+
+ # Do: Raoul write on Jobs -> ok
+ self.mail_group.write(cr, user_employee_id, [self.group_priv_id], {'name': 'modified'})
+ # Do: Bert cannot write on Private -> ko (read but no write)
+ with self.assertRaises(except_orm):
+ self.mail_group.write(cr, user_noone_id, [self.group_priv_id], {'name': 're-modified'})
+ # Test: Bert cannot unlink the group
+ with self.assertRaises(except_orm):
+ self.mail_group.unlink(cr, user_noone_id, [self.group_priv_id])
+ # Do: Raoul unlinks the group, there are no followers and messages left
+ self.mail_group.unlink(cr, user_employee_id, [self.group_priv_id])
+ fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_priv_id)])
+ self.assertFalse(fol_ids, 'unlinked document should not have any followers left')
+ msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_priv_id)])
+ self.assertFalse(msg_ids, 'unlinked document should not have any followers left')
diff --git a/addons/mail/tests/test_mail_message.py b/addons/mail/tests/test_mail_message.py
index f422219000c..5dc5e9e247d 100644
--- a/addons/mail/tests/test_mail_message.py
+++ b/addons/mail/tests/test_mail_message.py
@@ -19,66 +19,147 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
from openerp.osv.orm import except_orm
from openerp.tools import mute_logger
-class test_mail_access_rights(TestMailBase):
+class TestMailMail(TestMail):
- def setUp(self):
- super(test_mail_access_rights, self).setUp()
- cr, uid = self.cr, self.uid
+ def test_00_partner_find_from_email(self):
+ """ Tests designed for partner fetch based on emails. """
+ cr, uid, user_raoul, group_pigs = self.cr, self.uid, self.user_raoul, self.group_pigs
- # Test mail.group: public to provide access to everyone
- self.group_jobs_id = self.mail_group.create(cr, uid, {'name': 'Jobs', 'public': 'public'})
- # Test mail.group: private to restrict access
- self.group_priv_id = self.mail_group.create(cr, uid, {'name': 'Private', 'public': 'private'})
+ # --------------------------------------------------
+ # Data creation
+ # --------------------------------------------------
+ # 1 - Partner ARaoul
+ p_a_id = self.res_partner.create(cr, uid, {'name': 'ARaoul', 'email': 'test@test.fr'})
- @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
- def test_00_mail_group_access_rights(self):
- """ Testing mail_group access rights and basic mail_thread features """
- cr, uid, user_bert_id, user_raoul_id = self.cr, self.uid, self.user_bert_id, self.user_raoul_id
+ # --------------------------------------------------
+ # CASE1: without object
+ # --------------------------------------------------
- # Do: Bert reads Jobs -> ok, public
- self.mail_group.read(cr, user_bert_id, [self.group_jobs_id])
- # Do: Bert read Pigs -> ko, restricted to employees
- self.assertRaises(except_orm, self.mail_group.read,
- cr, user_bert_id, [self.group_pigs_id])
- # Do: Raoul read Pigs -> ok, belong to employees
- self.mail_group.read(cr, user_raoul_id, [self.group_pigs_id])
+ # Do: find partner with email -> first partner should be found
+ partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0]
+ self.assertEqual(partner_info['full_name'], 'Maybe Raoul ',
+ 'mail_thread: message_partner_info_from_emails did not handle email')
+ self.assertEqual(partner_info['partner_id'], p_a_id,
+ 'mail_thread: message_partner_info_from_emails wrong partner found')
- # Do: Bert creates a group -> ko, no access rights
- self.assertRaises(except_orm, self.mail_group.create,
- cr, user_bert_id, {'name': 'Test'})
- # Do: Raoul creates a restricted group -> ok
- new_group_id = self.mail_group.create(cr, user_raoul_id, {'name': 'Test'})
- # Do: Bert added in followers, read -> ok, in followers
- self.mail_group.message_subscribe_users(cr, uid, [new_group_id], [user_bert_id])
- self.mail_group.read(cr, user_bert_id, [new_group_id])
+ # Data: add some data about partners
+ # 2 - User BRaoul
+ p_b_id = self.res_partner.create(cr, uid, {'name': 'BRaoul', 'email': 'test@test.fr', 'user_ids': [(4, user_raoul.id)]})
- # Do: Raoul reads Priv -> ko, private
- self.assertRaises(except_orm, self.mail_group.read,
- cr, user_raoul_id, [self.group_priv_id])
- # Do: Raoul added in follower, read -> ok, in followers
- self.mail_group.message_subscribe_users(cr, uid, [self.group_priv_id], [user_raoul_id])
- self.mail_group.read(cr, user_raoul_id, [self.group_priv_id])
+ # Do: find partner with email -> first user should be found
+ partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0]
+ self.assertEqual(partner_info['partner_id'], p_b_id,
+ 'mail_thread: message_partner_info_from_emails wrong partner found')
- # Do: Raoul write on Jobs -> ok
- self.mail_group.write(cr, user_raoul_id, [self.group_priv_id], {'name': 'modified'})
- # Do: Bert cannot write on Private -> ko (read but no write)
- self.assertRaises(except_orm, self.mail_group.write,
- cr, user_bert_id, [self.group_priv_id], {'name': 're-modified'})
- # Test: Bert cannot unlink the group
- self.assertRaises(except_orm,
- self.mail_group.unlink,
- cr, user_bert_id, [self.group_priv_id])
- # Do: Raoul unlinks the group, there are no followers and messages left
- self.mail_group.unlink(cr, user_raoul_id, [self.group_priv_id])
- fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_priv_id)])
- self.assertFalse(fol_ids, 'unlinked document should not have any followers left')
- msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_priv_id)])
- self.assertFalse(msg_ids, 'unlinked document should not have any followers left')
+ # --------------------------------------------------
+ # CASE1: with object
+ # --------------------------------------------------
+
+ # Do: find partner in group where there is a follower with the email -> should be taken
+ self.mail_group.message_subscribe(cr, uid, [group_pigs.id], [p_b_id])
+ partner_info = self.mail_group.message_partner_info_from_emails(cr, uid, group_pigs.id, ['Maybe Raoul '], link_mail=False)[0]
+ self.assertEqual(partner_info['partner_id'], p_b_id,
+ 'mail_thread: message_partner_info_from_emails wrong partner found')
+
+
+class TestMailMessage(TestMail):
+
+ def test_00_mail_message_values(self):
+ """ Tests designed for testing email values based on mail.message, aliases, ... """
+ cr, uid, user_raoul_id = self.cr, self.uid, self.user_raoul_id
+
+ # Data: update + generic variables
+ reply_to1 = '_reply_to1@example.com'
+ reply_to2 = '_reply_to2@example.com'
+ email_from1 = 'from@example.com'
+ alias_domain = 'schlouby.fr'
+ raoul_from = 'Raoul Grosbedon '
+ raoul_from_alias = 'Raoul Grosbedon '
+ raoul_reply = '"Followers of Pigs" '
+ raoul_reply_alias = '"Followers of Pigs" '
+
+ # --------------------------------------------------
+ # Case1: without alias_domain
+ # --------------------------------------------------
+ param_ids = self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.domain')])
+ self.registry('ir.config_parameter').unlink(cr, uid, param_ids)
+
+ # Do: free message; specified values > default values
+ msg_id = self.mail_message.create(cr, user_raoul_id, {'reply_to': reply_to1, 'email_from': email_from1})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: message content
+ self.assertIn('reply_to', msg.message_id,
+ 'mail_message: message_id should be specific to a mail_message with a given reply_to')
+ self.assertEqual(msg.reply_to, reply_to1,
+ 'mail_message: incorrect reply_to: should come from values')
+ self.assertEqual(msg.email_from, email_from1,
+ 'mail_message: incorrect email_from: should come from values')
+
+ # Do: create a mail_mail with the previous mail_message + specified reply_to
+ mail_id = self.mail_mail.create(cr, user_raoul_id, {'mail_message_id': msg_id, 'state': 'cancel', 'reply_to': reply_to2})
+ mail = self.mail_mail.browse(cr, user_raoul_id, mail_id)
+ # Test: mail_mail content
+ self.assertEqual(mail.reply_to, reply_to2,
+ 'mail_mail: incorrect reply_to: should come from values')
+ self.assertEqual(mail.email_from, email_from1,
+ 'mail_mail: incorrect email_from: should come from mail.message')
+
+ # Do: mail_message attached to a document
+ msg_id = self.mail_message.create(cr, user_raoul_id, {'model': 'mail.group', 'res_id': self.group_pigs_id})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: message content
+ self.assertIn('mail.group', msg.message_id,
+ 'mail_message: message_id should contain model')
+ self.assertIn('%s' % self.group_pigs_id, msg.message_id,
+ 'mail_message: message_id should contain res_id')
+ self.assertEqual(msg.reply_to, raoul_reply,
+ 'mail_message: incorrect reply_to: should be Raoul')
+ self.assertEqual(msg.email_from, raoul_from,
+ 'mail_message: incorrect email_from: should be Raoul')
+
+ # --------------------------------------------------
+ # Case2: with alias_domain, without catchall alias
+ # --------------------------------------------------
+ self.registry('ir.config_parameter').set_param(cr, uid, 'mail.catchall.domain', alias_domain)
+ self.registry('ir.config_parameter').unlink(cr, uid, self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.alias')]))
+
+ # Update message
+ msg_id = self.mail_message.create(cr, user_raoul_id, {'model': 'mail.group', 'res_id': self.group_pigs_id})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: generated reply_to
+ self.assertEqual(msg.reply_to, raoul_reply_alias,
+ 'mail_mail: incorrect reply_to: should be Pigs alias')
+
+ # Update message: test alias on email_from
+ msg_id = self.mail_message.create(cr, user_raoul_id, {})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: generated reply_to
+ self.assertEqual(msg.reply_to, raoul_from_alias,
+ 'mail_mail: incorrect reply_to: should be message email_from using Raoul alias')
+
+ # --------------------------------------------------
+ # Case2: with alias_domain and catchall alias
+ # --------------------------------------------------
+ self.registry('ir.config_parameter').set_param(self.cr, self.uid, 'mail.catchall.alias', 'gateway')
+
+ # Update message
+ msg_id = self.mail_message.create(cr, user_raoul_id, {})
+ msg = self.mail_message.browse(cr, user_raoul_id, msg_id)
+ # Test: generated reply_to
+ self.assertEqual(msg.reply_to, 'gateway@schlouby.fr',
+ 'mail_mail: reply_to should equal the catchall email alias')
+
+ # Do: create a mail_mail
+ mail_id = self.mail_mail.create(cr, uid, {'state': 'cancel', 'reply_to': 'someone@example.com'})
+ mail = self.mail_mail.browse(cr, uid, mail_id)
+ # Test: mail_mail content
+ self.assertEqual(mail.reply_to, 'someone@example.com',
+ 'mail_mail: reply_to should equal the rpely_to given to create')
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
def test_10_mail_message_search_access_rights(self):
diff --git a/addons/mail/tests/test_message_read.py b/addons/mail/tests/test_message_read.py
index a4ff3685788..c02e9a32278 100644
--- a/addons/mail/tests/test_message_read.py
+++ b/addons/mail/tests/test_message_read.py
@@ -19,10 +19,10 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
-class test_mail_access_rights(TestMailBase):
+class test_mail_access_rights(TestMail):
def test_00_message_read(self):
""" Tests for message_read and expandables. """
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 88c8abe74c0..f129e5bf0f9 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -75,7 +75,7 @@ class mail_compose_message(osv.TransientModel):
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')
- else:
+ elif not result.get('active_domain'):
result['active_domain'] = ''
# get default values according to the composition mode
if composition_mode == 'reply':
@@ -134,8 +134,9 @@ 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': lambda self, cr, uid, ctx={}: True,
- 'same_thread': lambda self, cr, uid, ctx={}: True,
+ 'post': False,
+ 'notify': False,
+ 'same_thread': True,
}
def check_access_rule(self, cr, uid, ids, operation, context=None):
@@ -232,7 +233,11 @@ class mail_compose_message(osv.TransientModel):
email(s), rendering any template patterns on the fly if needed. """
if context is None:
context = {}
- ir_attachment_obj = self.pool.get('ir.attachment')
+ # 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)
@@ -251,43 +256,11 @@ class mail_compose_message(osv.TransientModel):
else:
res_ids = [wizard.res_id]
- for res_id in res_ids:
- # mail.message values, according to the wizard options
- post_values = {
- 'subject': wizard.subject,
- 'body': wizard.body,
- '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],
- }
- # mass mailing: render and override default values
- if mass_mail_mode and wizard.model:
- email_dict = self.render_message(cr, uid, wizard, res_id, context=context)
- post_values['partner_ids'] += email_dict.pop('partner_ids', [])
- post_values['attachments'] = email_dict.pop('attachments', [])
- attachment_ids = []
- for attach_id in post_values.pop('attachment_ids'):
- new_attach_id = ir_attachment_obj.copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context)
- attachment_ids.append(new_attach_id)
- post_values['attachment_ids'] = attachment_ids
- # email_from: mass mailing only can specify another email_from
- if email_dict.get('email_from'):
- post_values['email_from'] = email_dict.pop('email_from')
- # replies redirection: mass mailing only
- if not wizard.same_thread:
- post_values['reply_to'] = email_dict.pop('reply_to')
- else:
- email_dict.pop('reply_to')
- post_values.update(email_dict)
- # 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)
- # post the message
+ 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:
- post_values['body_html'] = post_values.get('body', '')
- post_values['recipient_ids'] = [(4, id) for id in post_values.pop('partner_ids', [])]
- self.pool.get('mail.mail').create(cr, uid, post_values, context=context)
+ 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
@@ -296,46 +269,122 @@ class mail_compose_message(osv.TransientModel):
if not wizard.notify:
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, **post_values)
+ 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'}
- def render_message(self, cr, uid, wizard, res_id, context=None):
- """ Generate an email from the template for given (wizard.model, res_id)
- pair. This method is meant to be inherited by email_template that
- will produce a more complete dictionary. """
- return {
- 'subject': self.render_template(cr, uid, wizard.subject, wizard.model, res_id, context),
- 'body': self.render_template(cr, uid, wizard.body, wizard.model, res_id, context),
- 'email_from': self.render_template(cr, uid, wizard.email_from, wizard.model, res_id, context),
- 'reply_to': self.render_template(cr, uid, wizard.reply_to, wizard.model, res_id, context),
- }
+ def get_mail_values(self, cr, uid, wizard, res_ids, context=None):
+ """Generate the values that will be used by send_mail to create mail_messages
+ or mail_mails. """
+ results = dict.fromkeys(res_ids, False)
+ mass_mail_mode = wizard.composition_mode == 'mass_mail'
- def render_template(self, cr, uid, template, model, res_id, context=None):
+ # render all template-based value at once
+ if mass_mail_mode and wizard.model:
+ rendered_values = self.render_message_batch(cr, uid, wizard, res_ids, context=context)
+
+ for res_id in res_ids:
+ # static wizard (mail.message) values
+ mail_values = {
+ 'subject': wizard.subject,
+ 'body': wizard.body,
+ '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],
+ }
+ # mass mailing: rendering override wizard static values
+ if mass_mail_mode and wizard.model:
+ email_dict = rendered_values[res_id]
+ mail_values['partner_ids'] += email_dict.pop('partner_ids', [])
+ mail_values['attachments'] = email_dict.pop('attachments', [])
+ 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 not wizard.same_thread:
+ mail_values['reply_to'] = email_dict.pop('reply_to')
+ else:
+ email_dict.pop('reply_to')
+ 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', [])]
+ results[res_id] = mail_values
+ return results
+
+ 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
+ will produce a more complete dictionary, using Jinja2 templates.
+
+ Each template is generated for all res_ids, allowing to parse the template
+ once, and render it multiple times. This is useful for mass mailing where
+ template rendering represent a significant part of the process.
+
+ :param browse wizard: current mail.compose.message browse record
+ :param list res_ids: list of record ids
+
+ :return dict results: for each res_id, the generated template values for
+ subject, body, email_from and reply_to
+ """
+ subjects = self.render_template_batch(cr, uid, wizard.subject, wizard.model, res_ids, context)
+ bodies = self.render_template_batch(cr, uid, wizard.body, wizard.model, res_ids, context)
+ emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context)
+ replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context)
+
+ results = dict.fromkeys(res_ids, False)
+ for res_id in res_ids:
+ results[res_id] = {
+ 'subject': subjects[res_id],
+ 'body': bodies[res_id],
+ 'email_from': emails_from[res_id],
+ 'reply_to': replies_to[res_id],
+ }
+ return results
+
+ def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
""" Render the given template text, replace mako-like expressions ``${expr}``
- with the result of evaluating these expressions with an evaluation context
- containing:
+ with the result of evaluating these expressions with an evaluation context
+ containing:
- * ``user``: browse_record of the current user
- * ``object``: browse_record of the document record this mail is
- related to
- * ``context``: the context passed to the mail composition wizard
+ * ``user``: browse_record of the current user
+ * ``object``: browse_record of the document record this mail is
+ related to
+ * ``context``: the context passed to the mail composition wizard
- :param str template: the template text to render
- :param str model: model name of the document record this mail is related to.
- :param int res_id: id of the document record this mail is related to.
+ :param str template: the template text to render
+ :param str model: model name of the document record this mail is related to
+ :param list res_ids: list of record ids
"""
if context is None:
context = {}
+ results = dict.fromkeys(res_ids, False)
- def merge(match):
- exp = str(match.group()[2:-1]).strip()
- result = eval(exp, {
- 'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
- 'object': self.pool[model].browse(cr, uid, res_id, context=context),
- 'context': dict(context), # copy context to prevent side-effects of eval
+ for res_id in res_ids:
+ def merge(match):
+ exp = str(match.group()[2:-1]).strip()
+ result = eval(exp, {
+ 'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
+ 'object': self.pool[model].browse(cr, uid, res_id, context=context),
+ 'context': dict(context), # copy context to prevent side-effects of eval
})
- return result and tools.ustr(result) or ''
- return template and EXPRESSION_PATTERN.sub(merge, template)
+ return result and tools.ustr(result) or ''
+ results[res_id] = template and EXPRESSION_PATTERN.sub(merge, template)
+ return results
+
+ # Compatibility methods
+ def render_template(self, cr, uid, template, model, res_id, context=None):
+ return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id]
+
+ def render_message(self, cr, uid, wizard, res_id, context=None):
+ return self.render_message_batch(cr, uid, wizard, [res_id], context)[res_id]
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index ed1a211ce8c..0c86bae7e5f 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -19,15 +19,7 @@
-
-
-
-
+
+
+
+
+
+
diff --git a/addons/marketing/res_config.py b/addons/marketing/res_config.py
index 3c161c4d0d6..a58a5c56fc7 100644
--- a/addons/marketing/res_config.py
+++ b/addons/marketing/res_config.py
@@ -26,15 +26,15 @@ class marketing_config_settings(osv.osv_memory):
_inherit = 'res.config.settings'
_columns = {
'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.
- This installs the module marketing_campaign."""),
+ 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.
- This installs the module marketing_campaign_crm_demo."""),
+ 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.
- This installs the module crm_profiling."""),
+ 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_campaign/i18n/th.po b/addons/marketing_campaign/i18n/th.po
new file mode 100644
index 00000000000..1db67eba8ac
--- /dev/null
+++ b/addons/marketing_campaign/i18n/th.po
@@ -0,0 +1,1056 @@
+# Thai translation for openobject-addons
+# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2013.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-12-21 17:05+0000\n"
+"PO-Revision-Date: 2013-09-18 00:30+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Thai
\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2013-09-19 04:42+0000\n"
+"X-Generator: Launchpad (build 16765)\n"
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+msgid "Manual Mode"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.transition,activity_from_id:0
+msgid "Previous Activity"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:819
+#, python-format
+msgid "The current step for this item has no email or report to preview."
+msgstr ""
+
+#. module: marketing_campaign
+#: constraint:marketing.campaign.transition:0
+msgid "The To/From Activity of transition must be of the same Campaign "
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.transition,trigger:0
+msgid "Time"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.activity,type:0
+msgid "Custom Action"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: view:marketing.campaign:0
+#: view:marketing.campaign.segment:0
+#: view:marketing.campaign.workitem:0
+msgid "Group By..."
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,revenue:0
+msgid ""
+"Set an expected revenue if you consider that every campaign item that has "
+"reached this point has generated a certain revenue. You can get revenue "
+"statistics in the Reporting section"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.transition,trigger:0
+msgid "Trigger"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+msgid "Follow-Up"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:campaign.analysis,count:0
+msgid "# of Actions"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+msgid "Campaign Editor"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+#: selection:marketing.campaign,state:0
+#: view:marketing.campaign.segment:0
+#: selection:marketing.campaign.segment,state:0
+msgid "Running"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:email.template,body_html:marketing_campaign.email_template_3
+msgid ""
+"Hi, we are delighted to let you know that you have entered the select circle "
+"of our Gold Partners"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "March"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,object_id:0
+msgid "Object"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+msgid "Sync mode: only records created after last sync"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,condition:0
+msgid ""
+"Python expression to decide whether the activity can be executed, otherwise "
+"it will be deleted or cancelled.The expression may use the following "
+"[browsable] variables:\n"
+" - activity: the campaign activity\n"
+" - workitem: the campaign workitem\n"
+" - resource: the resource object this campaign item represents\n"
+" - transitions: list of campaign transitions outgoing from this activity\n"
+"...- re: Python regular expression module"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+#: view:marketing.campaign.segment:0
+msgid "Set to Draft"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.activity:0
+#: field:marketing.campaign.activity,to_ids:0
+msgid "Next Activities"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:148
+#, python-format
+msgid ""
+"The campaign cannot be started. It does not have any starting activity. "
+"Modify campaign's activities to mark one as the starting point."
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,email_template_id:0
+msgid "The email to send when this activity is activated"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+#: field:marketing.campaign.segment,date_run:0
+msgid "Launch Date"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,day:0
+msgid "Day"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.activity:0
+msgid "Outgoing Transitions"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.workitem:0
+msgid "Reset"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign,object_id:0
+msgid "Choose the resource on which you want this campaign to be run"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.actions.client,name:marketing_campaign.action_client_marketing_menu
+msgid "Open Marketing Menu"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.segment,sync_last_date:0
+msgid "Last Synchronization"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.transition,interval_type:0
+msgid "Year(s)"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.segment,sync_last_date:0
+msgid ""
+"Date on which this segment was synchronized last time (automatically or "
+"manually)"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,state:0
+#: selection:marketing.campaign,state:0
+#: selection:marketing.campaign.segment,state:0
+#: selection:marketing.campaign.workitem,state:0
+msgid "Cancelled"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.transition,trigger:0
+msgid "Automatic"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign,mode:0
+msgid ""
+"Test - It creates and process all the activities directly (without waiting "
+"for the delay on transitions) but does not send emails or produce reports.\n"
+"Test in Realtime - It creates and processes all the activities directly but "
+"does not send emails or produce reports.\n"
+"With Manual Confirmation - the campaigns runs normally, but the user has to "
+"validate all workitem manually.\n"
+"Normal - the campaign runs normally and automatically sends all emails and "
+"reports (be very careful with this mode, you're live!)"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.segment,date_run:0
+msgid "Initial start date of this segment."
+msgstr ""
+
+#. module: marketing_campaign
+#: view:res.partner:0
+msgid "False"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,campaign_id:0
+#: view:marketing.campaign:0
+#: field:marketing.campaign.activity,campaign_id:0
+#: view:marketing.campaign.segment:0
+#: field:marketing.campaign.segment,campaign_id:0
+#: view:marketing.campaign.workitem:0
+#: field:marketing.campaign.workitem,campaign_id:0
+msgid "Campaign"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:email.template,body_html:marketing_campaign.email_template_1
+msgid "Hello, you will receive your welcome pack via email shortly."
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,segment_id:0
+#: view:marketing.campaign.segment:0
+#: view:marketing.campaign.workitem:0
+#: field:marketing.campaign.workitem,segment_id:0
+msgid "Segment"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:214
+#, python-format
+msgid "You cannot duplicate a campaign, Not supported yet."
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,type:0
+msgid ""
+"The type of action to execute when an item enters this activity, such as:\n"
+" - Email: send an email using a predefined email template\n"
+" - Report: print an existing Report defined on the resource item and save "
+"it into a specific directory\n"
+" - Custom Action: execute a predefined action, e.g. to modify the fields "
+"of the resource record\n"
+" "
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.segment,date_next_sync:0
+msgid "Next time the synchronization job is scheduled to run automatically"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.transition,interval_type:0
+msgid "Month(s)"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,partner_id:0
+#: model:ir.model,name:marketing_campaign.model_res_partner
+#: field:marketing.campaign.workitem,partner_id:0
+msgid "Partner"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.filters,name:marketing_campaign.filter0
+msgid "Partners"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+msgid "Marketing Reports"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign,state:0
+#: selection:marketing.campaign.segment,state:0
+msgid "New"
+msgstr ""
+
+#. module: marketing_campaign
+#: sql_constraint:marketing.campaign.transition:0
+msgid "The interval must be positive or zero"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.activity,type:0
+msgid "Email"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign,name:0
+#: field:marketing.campaign.activity,name:0
+#: field:marketing.campaign.segment,name:0
+#: field:marketing.campaign.transition,name:0
+msgid "Name"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.workitem,res_name:0
+msgid "Resource Name"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.segment,sync_mode:0
+msgid "Synchronization mode"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+#: view:marketing.campaign.segment:0
+msgid "Run"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.activity:0
+#: field:marketing.campaign.activity,from_ids:0
+msgid "Previous Activities"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:email.template,subject:marketing_campaign.email_template_2
+msgid "Congratulations! You are now a Silver Partner!"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.segment,date_done:0
+msgid "Date this segment was last closed or cancelled."
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.workitem:0
+msgid "Marketing Campaign Activities"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.workitem:0
+#: field:marketing.campaign.workitem,error_msg:0
+msgid "Error Message"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.actions.act_window,name:marketing_campaign.action_marketing_campaign_form
+#: model:ir.ui.menu,name:marketing_campaign.menu_marketing_campaign
+#: model:ir.ui.menu,name:marketing_campaign.menu_marketing_campaign_form
+#: view:marketing.campaign:0
+#: view:res.partner:0
+msgid "Campaigns"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.transition,interval_type:0
+msgid "Interval Unit"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:campaign.analysis,country_id:0
+msgid "Country"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,report_id:0
+#: selection:marketing.campaign.activity,type:0
+msgid "Report"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "July"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.ui.menu,name:marketing_campaign.menu_marketing_configuration
+msgid "Configuration"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,variable_cost:0
+msgid ""
+"Set a variable cost if you consider that every campaign item that has "
+"reached this point has entailed a certain cost. You can get cost statistics "
+"in the Reporting section"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.transition,interval_type:0
+msgid "Hour(s)"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.model,name:marketing_campaign.model_marketing_campaign_segment
+msgid "Campaign Segment"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,keep_if_condition_not_met:0
+msgid ""
+"By activating this option, workitems that aren't executed because the "
+"condition is not met are marked as cancelled instead of being deleted."
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+msgid "Exceptions"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.actions.act_window,name:marketing_campaign.act_marketing_campaing_followup
+#: field:res.partner,workitem_ids:0
+msgid "Workitems"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign,fixed_cost:0
+msgid "Fixed Cost"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+msgid "Newly Modified"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.actions.act_window,help:marketing_campaign.action_marketing_campaign_form
+msgid ""
+"
\n"
+" Click to create a marketing campaign.\n"
+"
\n"
+" OpenERP's marketing campaign allows you to automate "
+"communication\n"
+" to your prospects. You can define a segment (set of conditions) "
+"on\n"
+" your leads and partners to fullfil the campaign.\n"
+"
\n"
+" A campaign can have many activities like sending an email, "
+"printing\n"
+" a letter, assigning to a team, etc. These activities are "
+"triggered\n"
+" from specific situations; contact form, 10 days after first\n"
+" contact, if a lead is not closed yet, etc.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.transition,interval_nbr:0
+msgid "Interval Value"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:campaign.analysis,revenue:0
+#: field:marketing.campaign.activity,revenue:0
+msgid "Revenue"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "September"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "December"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign,partner_field_id:0
+msgid ""
+"The generated workitems will be linked to the partner related to the record. "
+"If the record is the partner itself leave this field empty. This is useful "
+"for reporting purposes, via the Campaign Analysis or Campaign Follow-up "
+"views."
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,month:0
+msgid "Month"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.transition,activity_to_id:0
+msgid "Next Activity"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.actions.act_window,name:marketing_campaign.act_marketing_campaing_stat
+#: model:ir.actions.act_window,name:marketing_campaign.action_marketing_campaign_workitem
+#: model:ir.ui.menu,name:marketing_campaign.menu_action_marketing_campaign_workitem
+msgid "Campaign Follow-up"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+msgid "Test Mode"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.segment,sync_mode:0
+msgid "Only records modified after last sync (no duplicates)"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.model,name:marketing_campaign.model_ir_actions_report_xml
+msgid "ir.actions.report.xml"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+msgid "Campaign Statistics"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,server_action_id:0
+msgid "The action to perform when this activity is activated"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign,partner_field_id:0
+msgid "Partner Field"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: model:ir.actions.act_window,name:marketing_campaign.action_campaign_analysis_all
+#: model:ir.model,name:marketing_campaign.model_campaign_analysis
+#: model:ir.ui.menu,name:marketing_campaign.menu_action_campaign_analysis_all
+msgid "Campaign Analysis"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.segment,sync_mode:0
+msgid ""
+"Determines an additional criterion to add to the filter when selecting new "
+"records to inject in the campaign. \"No duplicates\" prevents selecting "
+"records which have already entered the campaign previously.If the campaign "
+"has a \"unique field\" set, \"no duplicates\" will also prevent selecting "
+"records which have the same value for the unique field as other records that "
+"already entered the campaign."
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign,mode:0
+msgid "Test in Realtime"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign,mode:0
+msgid "Test Directly"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,report_directory_id:0
+msgid "Directory"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+#: view:marketing.campaign.segment:0
+msgid "Draft"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.workitem:0
+msgid "Marketing Campaign Activity"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.workitem:0
+msgid "Preview"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,state:0
+#: view:marketing.campaign:0
+#: field:marketing.campaign,state:0
+#: view:marketing.campaign.segment:0
+#: field:marketing.campaign.segment,state:0
+#: view:marketing.campaign.workitem:0
+#: field:marketing.campaign.workitem,state:0
+msgid "Status"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "August"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign,mode:0
+msgid "Normal"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,start:0
+msgid "This activity is launched when the campaign starts."
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,signal:0
+msgid ""
+"An activity with a signal can be called programmatically. Be careful, the "
+"workitem is always created when a signal is sent"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: selection:campaign.analysis,state:0
+#: view:marketing.campaign.workitem:0
+#: selection:marketing.campaign.workitem,state:0
+msgid "To Do"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "June"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.model,name:marketing_campaign.model_email_template
+msgid "Email Templates"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+msgid "Sync mode: all records"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.segment,sync_mode:0
+msgid "All records (no duplicates)"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+msgid "Newly Created"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:campaign.analysis,date:0
+msgid "Date"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "November"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,condition:0
+msgid "Condition"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,report_id:0
+msgid "The report to generate when this activity is activated"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign,unique_field_id:0
+msgid "Unique Field"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,state:0
+#: view:marketing.campaign.workitem:0
+#: selection:marketing.campaign.workitem,state:0
+msgid "Exception"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "October"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,email_template_id:0
+msgid "Email Template"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "January"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.workitem:0
+#: field:marketing.campaign.workitem,date:0
+msgid "Execution Date"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.model,name:marketing_campaign.model_marketing_campaign_workitem
+msgid "Campaign Workitem"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.model,name:marketing_campaign.model_marketing_campaign_activity
+msgid "Campaign Activity"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.activity,report_directory_id:0
+msgid "This folder is used to store the generated reports"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:136
+#: code:addons/marketing_campaign/marketing_campaign.py:148
+#: code:addons/marketing_campaign/marketing_campaign.py:158
+#, python-format
+msgid "Error"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,server_action_id:0
+msgid "Action"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:528
+#, python-format
+msgid "Automatic transition"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,start:0
+msgid "Start"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:819
+#, python-format
+msgid "No preview"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.workitem:0
+msgid "Process"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:530
+#: selection:marketing.campaign.transition,trigger:0
+#, python-format
+msgid "Cosmetic"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.transition,trigger:0
+msgid "How is the destination workitem triggered"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: selection:campaign.analysis,state:0
+#: view:marketing.campaign:0
+#: selection:marketing.campaign,state:0
+#: selection:marketing.campaign.segment,state:0
+#: selection:marketing.campaign.workitem,state:0
+msgid "Done"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:214
+#, python-format
+msgid "Operation not supported"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.workitem:0
+msgid "Cancel"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+msgid "Close"
+msgstr ""
+
+#. module: marketing_campaign
+#: constraint:marketing.campaign.segment:0
+msgid "Model of filter must be same as resource model of Campaign "
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+msgid "Synchronize Manually"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.workitem:0
+#: field:marketing.campaign.workitem,res_id:0
+msgid "Resource ID"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.model,name:marketing_campaign.model_marketing_campaign_transition
+msgid "Campaign Transition"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+msgid "Marketing Campaign Segment"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.actions.act_window,name:marketing_campaign.act_marketing_campaing_segment_opened
+#: model:ir.actions.act_window,name:marketing_campaign.action_marketing_campaign_segment_form
+#: model:ir.ui.menu,name:marketing_campaign.menu_marketing_campaign_segment_form
+#: view:marketing.campaign:0
+#: view:marketing.campaign.segment:0
+msgid "Segments"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,keep_if_condition_not_met:0
+msgid "Don't Delete Workitems"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.activity:0
+msgid "Incoming Transitions"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.transition,interval_type:0
+msgid "Day(s)"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+#: field:marketing.campaign,activity_ids:0
+#: view:marketing.campaign.activity:0
+msgid "Activities"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign,mode:0
+msgid "With Manual Confirmation"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "May"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,type:0
+msgid "Type"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:email.template,subject:marketing_campaign.email_template_3
+msgid "Congratulations! You are now one of our Gold Partners!"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign,unique_field_id:0
+msgid ""
+"If set, this field will help segments that work in \"no duplicates\" mode to "
+"avoid selecting similar records twice. Similar records are records that have "
+"the same value for this unique field. For example by choosing the "
+"\"email_from\" field for CRM Leads you would prevent sending the same "
+"campaign to the same email address again. If not set, the \"no duplicates\" "
+"segments will only avoid selecting the same record again if it entered the "
+"campaign previously. Only easily comparable fields like textfields, "
+"integers, selections or single relationships may be used."
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:529
+#, python-format
+msgid "After %(interval_nbr)d %(interval_type)s"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:ir.model,name:marketing_campaign.model_marketing_campaign
+#: view:marketing.campaign:0
+msgid "Marketing Campaign"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.segment,date_done:0
+msgid "End Date"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "February"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,res_id:0
+#: view:marketing.campaign:0
+#: field:marketing.campaign,object_id:0
+#: field:marketing.campaign.segment,object_id:0
+#: view:marketing.campaign.workitem:0
+#: field:marketing.campaign.workitem,object_id:0
+msgid "Resource"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign,fixed_cost:0
+msgid ""
+"Fixed cost for running this campaign. You may also specify variable cost and "
+"revenue on each campaign activity. Cost and Revenue statistics are included "
+"in Campaign Reporting."
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+msgid "Sync mode: only records updated after last sync"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:793
+#, python-format
+msgid "Email Preview"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,signal:0
+msgid "Signal"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.workitem,date:0
+msgid "If date is not set, this workitem has to be run manually"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:campaign.analysis,month:0
+msgid "April"
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:158
+#, python-format
+msgid "The campaign cannot be marked as done before all segments are closed."
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign:0
+#: field:marketing.campaign,mode:0
+msgid "Mode"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,activity_id:0
+#: view:marketing.campaign.workitem:0
+#: field:marketing.campaign.workitem,activity_id:0
+msgid "Activity"
+msgstr ""
+
+#. module: marketing_campaign
+#: help:marketing.campaign.segment,ir_filter_id:0
+msgid ""
+"Filter to select the matching resource records that belong to this segment. "
+"New filters can be created and saved using the advanced search on the list "
+"view of the Resource. If no filter is set, all records are selected without "
+"filtering. The synchronization mode may also add a criterion to the filter."
+msgstr ""
+
+#. module: marketing_campaign
+#: code:addons/marketing_campaign/marketing_campaign.py:136
+#, python-format
+msgid "The campaign cannot be started. There are no activities in it."
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.segment,date_next_sync:0
+msgid "Next Synchronization"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:email.template,body_html:marketing_campaign.email_template_2
+msgid ""
+"Hi, we are delighted to welcome you among our Silver Partners as of today!"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.segment,ir_filter_id:0
+msgid "Filter"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:marketing.campaign.segment:0
+msgid "All"
+msgstr ""
+
+#. module: marketing_campaign
+#: selection:marketing.campaign.segment,sync_mode:0
+msgid "Only records created after last sync"
+msgstr ""
+
+#. module: marketing_campaign
+#: field:marketing.campaign.activity,variable_cost:0
+msgid "Variable Cost"
+msgstr ""
+
+#. module: marketing_campaign
+#: model:email.template,subject:marketing_campaign.email_template_1
+msgid "Welcome to the OpenERP Partner Channel!"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,total_cost:0
+msgid "Cost"
+msgstr ""
+
+#. module: marketing_campaign
+#: view:campaign.analysis:0
+#: field:campaign.analysis,year:0
+msgid "Year"
+msgstr ""
diff --git a/addons/mass_mailing/__init__.py b/addons/mass_mailing/__init__.py
new file mode 100644
index 00000000000..9dadc456842
--- /dev/null
+++ b/addons/mass_mailing/__init__.py
@@ -0,0 +1,26 @@
+# -*- 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 wizard
+import controllers
diff --git a/addons/mass_mailing/__openerp__.py b/addons/mass_mailing/__openerp__.py
new file mode 100644
index 00000000000..db7ac54263a
--- /dev/null
+++ b/addons/mass_mailing/__openerp__.py
@@ -0,0 +1,54 @@
+# -*- 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
+#
+##############################################################################
+
+{
+ 'name': 'Mass Mailing Campaigns',
+ 'description': """TODO""",
+ 'version': '1.0',
+ 'author': 'OpenERP',
+ 'website': 'http://www.openerp.com',
+ 'category': 'Marketing',
+ 'depends': [
+ 'mail',
+ 'email_template',
+ 'web_kanban_gauge',
+ 'web_kanban_sparkline',
+ ],
+ 'data': [
+ 'mail_data.xml',
+ 'mass_mailing_view.xml',
+ 'wizard/mail_compose_message_view.xml',
+ 'wizard/mail_mass_mailing_create_segment.xml',
+ 'security/ir.model.access.csv',
+ ],
+ 'js': [
+ 'static/src/js/mass_mailing.js',
+ ],
+ 'qweb': [],
+ 'css': [
+ 'static/src/css/mass_mailing.css'
+ ],
+ 'demo': [
+ 'mass_mailing_demo.xml',
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/addons/mass_mailing/controllers/__init__.py b/addons/mass_mailing/controllers/__init__.py
new file mode 100644
index 00000000000..e11f9ba81bb
--- /dev/null
+++ b/addons/mass_mailing/controllers/__init__.py
@@ -0,0 +1,3 @@
+import main
+
+# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py
new file mode 100644
index 00000000000..3ca238256b1
--- /dev/null
+++ b/addons/mass_mailing/controllers/main.py
@@ -0,0 +1,12 @@
+
+import openerp.addons.web.http as http
+from openerp.addons.web.http import request
+
+
+class MassMailController(http.Controller):
+ @http.route('/mail/track//blank.gif', type='http', auth='admin')
+ def track_mail_open(self, mail_id):
+ """ Email tracking. """
+ mail_mail_stats = request.registry.get('mail.mail.statistics')
+ mail_mail_stats.set_opened(request.cr, request.uid, mail_mail_ids=[mail_id])
+ return "data:image/gif;base64,R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
diff --git a/addons/mass_mailing/doc/changelog.rst b/addons/mass_mailing/doc/changelog.rst
new file mode 100644
index 00000000000..a000c4ae396
--- /dev/null
+++ b/addons/mass_mailing/doc/changelog.rst
@@ -0,0 +1,9 @@
+.. _changelog:
+
+Changelog
+=========
+
+`trunk (saas-2)`
+----------------
+
+ - added module
\ No newline at end of file
diff --git a/addons/mass_mailing/doc/index.rst b/addons/mass_mailing/doc/index.rst
new file mode 100644
index 00000000000..3d991c7d9dd
--- /dev/null
+++ b/addons/mass_mailing/doc/index.rst
@@ -0,0 +1,13 @@
+Mass Mailing module documentation
+=================================
+
+Mass Mailing documentation topics
+'''''''''''''''''''''''''''''''''
+
+Changelog
+'''''''''
+
+.. toctree::
+ :maxdepth: 1
+
+ changelog.rst
diff --git a/addons/mass_mailing/mail_data.xml b/addons/mass_mailing/mail_data.xml
new file mode 100644
index 00000000000..a1a0e404fe3
--- /dev/null
+++ b/addons/mass_mailing/mail_data.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ mail.bounce.alias
+ bounce
+
+
+
+
diff --git a/addons/mass_mailing/mail_mail.py b/addons/mass_mailing/mail_mail.py
new file mode 100644
index 00000000000..917334bdda4
--- /dev/null
+++ b/addons/mass_mailing/mail_mail.py
@@ -0,0 +1,65 @@
+# -*- 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 urlparse import urljoin
+
+from openerp import tools
+from openerp import SUPERUSER_ID
+from openerp.osv import osv, fields
+
+
+class MailMail(osv.Model):
+ """Add the mass mailing campaign data to mail"""
+ _name = 'mail.mail'
+ _inherit = ['mail.mail']
+
+ _columns = {
+ 'statistics_ids': fields.one2many(
+ 'mail.mail.statistics', 'mail_mail_id',
+ string='Statistics',
+ ),
+ }
+
+ def create(self, cr, uid, values, context=None):
+ """ Override mail_mail creation to create an entry in mail.mail.statistics """
+ # TDE note: should be after 'all values computed', to have values (FIXME after merging other branch holding create refactoring)
+ mail_id = super(MailMail, self).create(cr, uid, values, context=context)
+ if values.get('statistics_ids'):
+ mail = self.browse(cr, SUPERUSER_ID, mail_id)
+ for stat in mail.statistics_ids:
+ self.pool['mail.mail.statistics'].write(cr, uid, [stat.id], {'message_id': mail.message_id}, context=context)
+ return mail_id
+
+ 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)
+ return '' % track_url
+
+ 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)
+
+ # generate tracking URL
+ if mail.statistics_ids:
+ tracking_url = self._get_tracking_url(cr, uid, mail, partner, context=context)
+ if tracking_url:
+ body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
+ return body
diff --git a/addons/mass_mailing/mail_thread.py b/addons/mass_mailing/mail_thread.py
new file mode 100644
index 00000000000..0570596ba12
--- /dev/null
+++ b/addons/mass_mailing/mail_thread.py
@@ -0,0 +1,86 @@
+# -*- 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 logging
+
+from openerp import tools
+from openerp.addons.mail.mail_message import decode
+from openerp.addons.mail.mail_thread import decode_header
+from openerp.osv import osv
+
+_logger = logging.getLogger(__name__)
+
+
+class MailThread(osv.Model):
+ """ Update MailThread to add the feature of bounced emails and replied emails
+ in message_process. """
+ _name = 'mail.thread'
+ _inherit = ['mail.thread']
+
+ def message_route_check_bounce(self, cr, uid, message, context=None):
+ """ Override to verify that the email_to is the bounce alias. If it is the
+ case, log the bounce, set the parent and related document as bounced and
+ return False to end the routing process. """
+ bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
+ message_id = message.get('Message-Id')
+ email_from = decode_header(message, 'From')
+ email_to = decode_header(message, 'To')
+
+ # 0. Verify whether this is a bounced email (wrong destination,...) -> use it to collect data, such as dead leads
+ if bounce_alias in email_to:
+ bounce_match = tools.bounce_re.search(email_to)
+ if bounce_match:
+ bounced_mail_id = bounce_match.group(1)
+ stat_ids = self.pool['mail.mail.statistics'].set_bounced(cr, uid, mail_mail_ids=[bounced_mail_id], context=context)
+ for stat in self.pool['mail.mail.statistics'].browse(cr, uid, stat_ids, context=context):
+ bounced_model = stat.model
+ bounced_thread_id = stat.res_id
+ _logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s',
+ email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id)
+ if bounced_model and bounced_model in self.pool and hasattr(self.pool[bounced_model], 'message_receive_bounce'):
+ self.pool[bounced_model].message_receive_bounce(cr, uid, [bounced_thread_id], mail_id=bounced_mail_id, context=context)
+ return False
+
+ return True
+
+ def message_route(self, cr, uid, message, message_dict, model=None, thread_id=None,
+ custom_values=None, context=None):
+ if not self.message_route_check_bounce(cr, uid, message, context=context):
+ return []
+ return super(MailThread, self).message_route(cr, uid, message, message_dict, model, thread_id, custom_values, context)
+
+ def message_receive_bounce(self, cr, uid, ids, mail_id=None, context=None):
+ """Called by ``message_process`` when a bounce email (such as Undelivered
+ Mail Returned to Sender) is received for an existing thread. The default
+ behavior is to check is an integer ``message_bounce`` column exists.
+ If it is the case, its content is incremented. """
+ if self._all_columns.get('message_bounce'):
+ for obj in self.browse(cr, uid, ids, context=context):
+ self.write(cr, uid, [obj.id], {'message_bounce': obj.message_bounce + 1}, context=context)
+
+ def message_route_process(self, cr, uid, message, message_dict, routes, context=None):
+ """ Override to update the parent mail statistics. The parent is found
+ by using the References header of the incoming message and looking for
+ matching message_id in mail.mail.statistics. """
+ if message.get('References'):
+ message_ids = [x.strip() for x in decode(message['References']).split()]
+ self.pool['mail.mail.statistics'].set_replied(cr, uid, mail_message_ids=message_ids, context=context)
+ return super(MailThread, self).message_route_process(cr, uid, message, message_dict, routes, context=context)
diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py
new file mode 100644
index 00000000000..49ee106bc69
--- /dev/null
+++ b/addons/mass_mailing/mass_mailing.py
@@ -0,0 +1,376 @@
+# -*- 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)
+ for group in group_obj:
+ group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT).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 = {}
+ context['datetime_format'] = {
+ 'opened': {
+ 'interval': 'day',
+ 'groupby_format': 'yyyy-mm-dd',
+ 'display_format': 'dd MMMM YYYY'
+ },
+ 'replied': {
+ 'interval': 'day',
+ 'groupby_format': 'yyyy-mm-dd',
+ 'display_format': 'dd MMMM YYYY'
+ },
+ }
+ for id in ids:
+ res[id] = {}
+ date_begin = self.browse(cr, uid, id, context=context).date
+ domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin)]
+ res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened', context=context)
+ domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin)]
+ res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied', 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
new file mode 100644
index 00000000000..d75351e1166
--- /dev/null
+++ b/addons/mass_mailing/mass_mailing_demo.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+ Partner Newsletter 1
+
+
+ ${object.id}
+ Hello]]>
+
+
+ Partner Newsletter 2
+
+
+ ${object.id}
+ Hello]]>
+
+
+
+ Partners Newsletter
+
+
+
+
+ First Newsletter
+
+
+
+
+
+ Second Newsletter
+
+
+
+
+
+
+
+ 1111000@OpenERP.com
+
+
+ sent
+
+
+
+ 1111001@OpenERP.com
+
+
+ sent
+
+
+
+ 1111002@OpenERP.com
+
+ sent
+
+
+
+ 1111003@OpenERP.com
+ sent
+
+
+
+ 1111004@OpenERP.com
+
+ sent
+
+
+
+
+ 1111005@OpenERP.com
+
+ sent
+
+
+
+ 1111006@OpenERP.com
+
+ sent
+
+
+
+ 1111007@OpenERP.com
+ sent
+
+
+
+
diff --git a/addons/mass_mailing/mass_mailing_view.xml b/addons/mass_mailing/mass_mailing_view.xml
new file mode 100644
index 00000000000..f0c27f42a4a
--- /dev/null
+++ b/addons/mass_mailing/mass_mailing_view.xml
@@ -0,0 +1,376 @@
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv
new file mode 100644
index 00000000000..6931ab0265b
--- /dev/null
+++ b/addons/mass_mailing/security/ir.model.access.csv
@@ -0,0 +1,6 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,,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,,1,1,1,0
+access_mass_mailing_system,mail.mass_mailing.system,model_mail_mass_mailing,base.group_system,1,1,1,1
+access_mail_mail_statistics,mail.mail.statistics,model_mail_mail_statistics,,1,1,1,1
\ No newline at end of file
diff --git a/addons/mass_mailing/static/src/css/mass_mailing.css b/addons/mass_mailing/static/src/css/mass_mailing.css
new file mode 100644
index 00000000000..ca5455d6323
--- /dev/null
+++ b/addons/mass_mailing/static/src/css/mass_mailing.css
@@ -0,0 +1,55 @@
+.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_campaign {
+ width: 540px;
+}
+
+.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_segment {
+ width: 270px;
+}
+
+.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_mail_stats {
+ width: 120px;
+ 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 .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
new file mode 100644
index 00000000000..93f63f01ad1
--- /dev/null
+++ b/addons/mass_mailing/static/src/js/mass_mailing.js
@@ -0,0 +1,13 @@
+openerp.mass_mailing = function(openerp) {
+
+ 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();
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+ });
+
+};
diff --git a/addons/mass_mailing/tests/__init__.py b/addons/mass_mailing/tests/__init__.py
new file mode 100644
index 00000000000..f8610905393
--- /dev/null
+++ b/addons/mass_mailing/tests/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# 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.addons.mass_mailing.tests import test_mail
+
+checks = [
+ test_mail,
+]
diff --git a/addons/mass_mailing/tests/test_mail.py b/addons/mass_mailing/tests/test_mail.py
new file mode 100644
index 00000000000..0e3b4b8608e
--- /dev/null
+++ b/addons/mass_mailing/tests/test_mail.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# 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.addons.mail.tests.common import TestMail
+
+
+class test_message_compose(TestMail):
+
+ def test_OO_mail_mail_tracking(self):
+ """ Tests designed for mail_mail tracking (opened, replied, bounced) """
+ pass
diff --git a/addons/mass_mailing/wizard/__init__.py b/addons/mass_mailing/wizard/__init__.py
new file mode 100644
index 00000000000..669d12289c2
--- /dev/null
+++ b/addons/mass_mailing/wizard/__init__.py
@@ -0,0 +1,23 @@
+# -*- 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 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
new file mode 100644
index 00000000000..e5276eb7fd5
--- /dev/null
+++ b/addons/mass_mailing/wizard/mail_compose_message.py
@@ -0,0 +1,61 @@
+# -*- 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
+
+
+class MailComposeMessage(osv.TransientModel):
+ """Add concept of mass mailing campaign to the mail.compose.message wizard
+ """
+ _inherit = 'mail.compose.message'
+
+ _columns = {
+ 'mass_mailing_campaign_id': fields.many2one(
+ '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)]",
+ ),
+ }
+
+ def get_mail_values(self, cr, uid, wizard, res_ids, context=None):
+ """ Override method that generated the mail content by creating the
+ 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 ?
+ current_date = fields.datetime.now()
+ 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,
+ 'template_id': wizard.template_id and wizard.template_id.id or False,
+ }, context=context)
+ for res_id in res_ids:
+ res[res_id]['statistics_ids'] = [(0, 0, {
+ 'model': wizard.model,
+ 'res_id': res_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
new file mode 100644
index 00000000000..f5dbf57a808
--- /dev/null
+++ b/addons/mass_mailing/wizard/mail_compose_message_view.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ mail.compose.message.form.mass_mailing
+ mail.compose.message
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py
new file mode 100644
index 00000000000..9ea0d8aa1c5
--- /dev/null
+++ b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py
@@ -0,0 +1,133 @@
+# -*- 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',
+ 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_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
new file mode 100644
index 00000000000..e307bdd8909
--- /dev/null
+++ b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+ 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
+
+
+
+
diff --git a/addons/mrp/res_config.py b/addons/mrp/res_config.py
index 7343990a32f..929af752c42 100644
--- a/addons/mrp/res_config.py
+++ b/addons/mrp/res_config.py
@@ -28,48 +28,48 @@ class mrp_config_settings(osv.osv_memory):
_columns = {
'module_mrp_repair': fields.boolean("Manage repairs of products ",
- help="""Allows to manage all product repairs.
- * Add/remove products in the reparation
- * Impact for stocks
- * Invoicing (products and/or services)
- * Warranty concept
- * Repair quotation report
- * Notes for the technician and for the final customer.
- This installs the module mrp_repair."""),
- 'module_mrp_operations': fields.boolean("Allow detailed planning of work orders",
- help="""This allows to add state, date_start,date_stop in production order operation lines (in the "Work Centers" tab).
- This installs the module mrp_operations."""),
+ help='Allows to manage all product repairs.\n'
+ '* Add/remove products in the reparation\n'
+ '* Impact for stocks\n'
+ '* Invoicing (products and/or services)\n'
+ '* Warranty concept\n'
+ '* Repair quotation report\n'
+ '* Notes for the technician and for the final customer.\n'
+ '-This installs the module mrp_repair.'),
+ 'module_mrp_operations': fields.boolean("Allow detailed planning of work order",
+ help='This allows to add state, date_start,date_stop in production order operation lines (in the "Work Centers" tab).\n'
+ '-This installs the module mrp_operations.'),
'module_mrp_byproduct': fields.boolean("Produce several products from one manufacturing order",
- help="""You can configure by-products in the bill of material.
- Without this module: A + B + C -> D.
- With this module: A + B + C -> D + E.
- This installs the module mrp_byproduct."""),
+ help='You can configure by-products in the bill of material.\n'
+ 'Without this module: A + B + C -> D.\n'
+ 'With this module: A + B + C -> D + E.\n'
+ '-This installs the module mrp_byproduct.'),
'module_mrp_jit': fields.boolean("Generate procurement in real time",
- help="""This allows Just In Time computation of procurement orders.
- All procurement orders will be processed immediately, which could in some
- cases entail a small performance impact.
- This installs the module mrp_jit."""),
+ help='This allows Just In Time computation of procurement orders.\n'
+ 'All procurement orders will be processed immediately, which could in some '
+ 'cases entail a small performance impact.\n'
+ '-This installs the module mrp_jit.'),
'module_stock_no_autopicking': fields.boolean("Manage manual picking to fulfill manufacturing orders ",
- help="""This module allows an intermediate picking process to provide raw materials to production orders.
- For example to manage production made by your suppliers (sub-contracting).
- To achieve this, set the assembled product which is sub-contracted to "No Auto-Picking"
- and put the location of the supplier in the routing of the assembly operation.
- This installs the module stock_no_autopicking."""),
+ help='This module allows an intermediate picking process to provide raw materials to production orders.\n'
+ 'For example to manage production made by your suppliers (sub-contracting).\n'
+ 'To achieve this, set the assembled product which is sub-contracted to "No Auto-Picking" '
+ 'and put the location of the supplier in the routing of the assembly operation.\n'
+ '-This installs the module stock_no_autopicking.'),
'group_mrp_routings': fields.boolean("Manage routings and work orders ",
implied_group='mrp.group_mrp_routings',
- help="""Routings allow you to create and manage the manufacturing operations that should be followed
- within your work centers in order to produce a product. They are attached to bills of materials
- that will define the required raw materials."""),
- 'group_mrp_properties': fields.boolean("Allow several bill of materials per product using properties",
+ help='Routings allow you to create and manage the manufacturing operations that should be followed '
+ 'within your work centers in order to produce a product. They are attached to bills of materials '
+ 'that will define the required raw materials.'),
+ 'group_mrp_properties': fields.boolean("Allow several bill of materials per products using properties",
implied_group='product.group_mrp_properties',
help="""The selection of the right Bill of Material to use will depend on the properties specified on the sales order and the Bill of Material."""),
'module_product_manufacturer': fields.boolean("Define manufacturers on products ",
- help="""This allows you to define the following for a product:
- * Manufacturer
- * Manufacturer Product Name
- * Manufacturer Product Code
- * Product Attributes.
- This installs the module product_manufacturer."""),
+ help='This allows you to define the following for a product:\n'
+ '* Manufacturer\n'
+ '* Manufacturer Product Name\n'
+ '* Manufacturer Product Code\n'
+ '* Product Attributes.\n'
+ '-This installs the module product_manufacturer.'),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py
index cb05cef46c3..dfdc2c57cb9 100644
--- a/addons/point_of_sale/__openerp__.py
+++ b/addons/point_of_sale/__openerp__.py
@@ -98,6 +98,7 @@ Main Features
'static/src/js/widgets.js',
'static/src/js/devices.js',
'static/src/js/screens.js',
+ 'static/src/js/tests.js',
'static/src/js/main.js',
],
'css': [
diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py
index e0bd19545a6..de757238d6f 100644
--- a/addons/point_of_sale/controllers/main.py
+++ b/addons/point_of_sale/controllers/main.py
@@ -10,6 +10,8 @@ from openerp.addons.web import http
from openerp.addons.web.http import request
from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template
+_logger = logging.getLogger(__name__)
+
class PointOfSaleController(http.Controller):
def __init__(self):
self.scale = 'closed'
@@ -71,7 +73,7 @@ class PointOfSaleController(http.Controller):
@http.route('/pos/test_connection', type='json', auth='admin')
def test_connection(self):
- return
+ _logger.info('Received Connection Test from the Point of Sale');
@http.route('/pos/scan_item_success', type='json', auth='admin')
def scan_item_success(self, ean):
@@ -79,7 +81,6 @@ class PointOfSaleController(http.Controller):
A product has been scanned with success
"""
print 'scan_item_success: ' + str(ean)
- return
@http.route('/pos/scan_item_error_unrecognized')
def scan_item_error_unrecognized(self, ean):
@@ -87,7 +88,6 @@ class PointOfSaleController(http.Controller):
A product has been scanned without success
"""
print 'scan_item_error_unrecognized: ' + str(ean)
- return
@http.route('/pos/help_needed', type='json', auth='admin')
def help_needed(self):
@@ -95,7 +95,6 @@ class PointOfSaleController(http.Controller):
The user wants an help (ex: light is on)
"""
print "help_needed"
- return
@http.route('/pos/help_canceled', type='json', auth='admin')
def help_canceled(self):
@@ -103,7 +102,6 @@ class PointOfSaleController(http.Controller):
The user stops the help request
"""
print "help_canceled"
- return
@http.route('/pos/weighting_start', type='json', auth='admin')
def weighting_start(self):
@@ -115,7 +113,6 @@ class PointOfSaleController(http.Controller):
print "... Scale Open."
else:
print "WARNING: Scale already Connected !!!"
- return
@http.route('/pos/weighting_read_kg', type='json', auth='admin')
def weighting_read_kg(self):
@@ -156,41 +153,37 @@ class PointOfSaleController(http.Controller):
@http.route('/pos/payment_cancel', type='json', auth='admin')
def payment_cancel(self):
print "payment_cancel"
- return
@http.route('/pos/transaction_start', type='json', auth='admin')
def transaction_start(self):
print 'transaction_start'
- return
@http.route('/pos/transaction_end', type='json', auth='admin')
def transaction_end(self):
print 'transaction_end'
- return
@http.route('/pos/cashier_mode_activated', type='json', auth='admin')
def cashier_mode_activated(self):
print 'cashier_mode_activated'
- return
@http.route('/pos/cashier_mode_deactivated', type='json', auth='admin')
def cashier_mode_deactivated(self):
print 'cashier_mode_deactivated'
- return
@http.route('/pos/open_cashbox', type='json', auth='admin')
def open_cashbox(self):
print 'open_cashbox'
- return
@http.route('/pos/print_receipt', type='json', auth='admin')
def print_receipt(self, receipt):
print 'print_receipt' + str(receipt)
- return
+
+ @http.route('/pos/log', type='json', auth='admin')
+ def log(self, arguments):
+ _logger.info(' '.join(str(v) for v in arguments))
@http.route('/pos/print_pdf_invoice', type='json', auth='admin')
def print_pdf_invoice(self, pdfinvoice):
print 'print_pdf_invoice' + str(pdfinvoice)
- return
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 90505ccf11d..da8325cd046 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -21,6 +21,10 @@
user-select: none;
}
+.point-of-sale .oe_hidden{
+ display: none !important;
+}
+
.point-of-sale ul, .point-of-sale li {
margin: 0;
padding: 0;
diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js
index 0c737ec1e1a..bc697e96aca 100644
--- a/addons/point_of_sale/static/src/js/devices.js
+++ b/addons/point_of_sale/static/src/js/devices.js
@@ -40,6 +40,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
});
}else{
running = false;
+ scheduled_end_time = 0;
end_of_queue.resolve();
}
};
@@ -82,6 +83,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
module.ProxyDevice = instance.web.Class.extend({
init: function(options){
+ var self = this;
options = options || {};
url = options.url || 'http://localhost:8069';
@@ -99,10 +101,13 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
};
this.custom_payment_status = this.default_payment_status;
+ this.notifications = {};
+ this.bypass_proxy = false;
+
this.connection = new instance.web.Session(undefined,url);
this.connection.session_id = _.uniqueId('posproxy');
- this.bypass_proxy = false;
- this.notifications = {};
+ this.test_connection();
+ window.proxy = this;
},
close: function(){
@@ -113,7 +118,19 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
for(var i = 0; i < callbacks.length; i++){
callbacks[i](params);
}
- return this.connection.rpc('/pos/' + name, params || {});
+ if(this.connected){
+ return this.connection.rpc('/pos/' + name, params || {});
+ }else{
+ return (new $.Deferred()).reject();
+ }
+ },
+ test_connection: function(){
+ var self = this;
+ this.connected = true;
+ return this.message('test_connection').fail(function(){
+ self.connected = false;
+ console.error('Could not connect to the Proxy');
+ });
},
// this allows the client to be notified when a proxy call is made. The notification
@@ -124,6 +141,8 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
}
this.notifications[name].push(callback);
},
+
+
//a product has been scanned and recognized with success
// ean is a parsed ean object
@@ -316,6 +335,11 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
return this.message('print_receipt',{receipt: receipt});
},
+ // asks the proxy to log some information, as with the debug.log you can provide several arguments.
+ log: function(){
+ return this.message('log',{'arguments': _.toArray(arguments)});
+ },
+
// asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server )
print_pdf_invoice: function(pdfinvoice){
return this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice});
diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js
index b9263b91314..55f0bbd1f61 100644
--- a/addons/point_of_sale/static/src/js/main.js
+++ b/addons/point_of_sale/static/src/js/main.js
@@ -16,10 +16,12 @@ openerp.point_of_sale = function(instance) {
openerp_pos_scrollbar(instance,module); // import pos_scrollbar_widget.js
openerp_pos_screens(instance,module); // import pos_screens.js
+
+ openerp_pos_devices(instance,module); // import pos_devices.js
openerp_pos_widgets(instance,module); // import pos_widgets.js
- openerp_pos_devices(instance,module); // import pos_devices.js
+ openerp_pos_tests(instance,module); // import pos_tests.js
instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
};
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 365c47b71c0..6d5fd1fc422 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -263,13 +263,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
//removes the current order
delete_current_order: function(){
this.get('selectedOrder').destroy({'reason':'abandon'});
- console.log('coucou!');
},
// saves the order locally and try to send it to the backend.
// it returns a deferred that succeeds after having tried to send the order and all the other pending orders.
push_order: function(order) {
var self = this;
+ this.proxy.log('push_order',order.export_as_JSON());
var order_id = this.db.add_order(order.export_as_JSON());
var pushed = new $.Deferred();
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 7971c490ee6..a5017d517b7 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -253,7 +253,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.hidden = false;
if(this.$el){
- this.$el.show();
+ this.$el.removeClass('oe_hidden');
}
if(this.pos_widget.action_bar.get_button_count() > 0){
@@ -314,7 +314,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
hide: function(){
this.hidden = true;
if(this.$el){
- this.$el.hide();
+ this.$el.addClass('oe_hidden');
}
},
@@ -326,7 +326,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this._super();
if(this.hidden){
if(this.$el){
- this.$el.hide();
+ this.$el.addClass('oe_hidden');
}
}
},
@@ -335,7 +335,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
module.PopUpWidget = module.PosBaseWidget.extend({
show: function(){
if(this.$el){
- this.$el.show();
+ this.$el.removeClass('oe_hidden');
}
},
/* called before hide, when a popup is closed */
@@ -345,7 +345,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
* pos instantiation, so you don't want to do anything fancy in here */
hide: function(){
if(this.$el){
- this.$el.hide();
+ this.$el.addClass('oe_hidden');
}
},
});
@@ -455,7 +455,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
queue.schedule(function(){
return self.pos.proxy.weighting_start();
- },{ unclearable: true });
+ },{ important: true });
queue.schedule(function(){
return self.pos.proxy.weighting_read_kg().then(function(weight){
@@ -479,7 +479,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.pos.proxy_queue.clear();
this.pos.proxy_queue.schedule(function(){
return self.pos.proxy.weighting_end();
- },{ unclearable: true });
+ },{ important: true });
},
});
@@ -516,7 +516,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
queue.schedule(function(){
return self.pos.proxy.weighting_start()
- },{ unclearable: true });
+ },{ important: true });
queue.schedule(function(){
return self.pos.proxy.weighting_read_kg().then(function(weight){
@@ -542,8 +542,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}
},
order_product: function(){
- var weight = this.pos.proxy.weighting_read_kg();
- this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity:weight });
+ this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
},
get_product_name: function(){
var product = this.get_product();
@@ -567,7 +566,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.pos.proxy_queue.clear();
this.pos.proxy_queue.schedule(function(){
self.pos.proxy.weighting_end();
- },{ unclearable: true });
+ },{ important: true });
},
});
@@ -692,7 +691,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
barcode_client_action: function(ean){
this.pos.proxy.transaction_start();
this._super(ean);
- $('.goodbye-message').hide();
+ $('.goodbye-message').addClass('oe_hidden');
this.pos_widget.screen_selector.show_popup('choose-receipt');
},
@@ -704,14 +703,14 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
label: _t('Help'),
icon: '/point_of_sale/static/src/img/icons/png48/help.png',
click: function(){
- $('.goodbye-message').css({opacity:1}).hide();
+ $('.goodbye-message').css({opacity:1}).addClass('oe_hidden');
self.help_button_action();
},
});
- $('.goodbye-message').css({opacity:1}).show();
+ $('.goodbye-message').css({opacity:1}).removeClass('oe_hidden');
setTimeout(function(){
- $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').hide();});
+ $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').addClass('oe_hidden');});
},5000);
},
});
diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js
new file mode 100644
index 00000000000..3b1dbc14503
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/tests.js
@@ -0,0 +1,92 @@
+function openerp_pos_tests(instance, module){ //module is instance.point_of_sale
+
+ // Various UI Tests to measure performance and memory leaks.
+ module.UiTester = function(){
+ var running = false;
+ var queue = new module.JobQueue();
+
+ // stop the currently running test
+ this.stop = function(){
+ queue.clear();
+ };
+
+ // randomly switch product categories
+ this.category_switch = function(interval){
+ queue.schedule(function(){
+ var breadcrumbs = $('.breadcrumb a');
+ var categories = $('li.category-button');
+ if(categories.length > 0){
+ var rnd = Math.floor(Math.random()*categories.length);
+ categories.eq(rnd).click();
+ }else{
+ var rnd = Math.floor(Math.random()*breadcrumbs.length);
+ breadcrumbs.eq(rnd).click();
+ }
+ },{repeat:true, duration:interval});
+ };
+
+ // randomly order products then resets the order
+ this.order_products = function(interval){
+
+ queue.schedule(function(){
+ var def = new $.Deferred();
+ var order_queue = new module.JobQueue();
+ var order_size = 1 + Math.floor(Math.random()*10);
+
+ while(order_size--){
+ order_queue.schedule(function(){
+ var products = $('.product a');
+ if(products.length > 0){
+ var rnd = Math.floor(Math.random()*products.length);
+ products.eq(rnd).click();
+ }
+ },{duration:250});
+ }
+ order_queue.finished().then(function(){
+ $('.deleteorder-button').click();
+ def.resolve();
+ });
+ return def;
+ },{repeat:true, duration: interval});
+
+ };
+
+ // makes a complete product order cycle ( print via proxy must be activated, and scale deactivated )
+ this.full_order_cycle = function(interval){
+ queue.schedule(function(){
+ var def = new $.Deferred();
+ var order_queue = new module.JobQueue();
+ var order_size = 1 + Math.floor(Math.random()*50);
+
+ while(order_size--){
+ order_queue.schedule(function(){
+ var products = $('.product a');
+ if(products.length > 0){
+ var rnd = Math.floor(Math.random()*products.length);
+ products.eq(rnd).click();
+ }
+ },{duration:250});
+ }
+ order_queue.schedule(function(){
+ $('.paypad-button:first').click();
+ },{duration:250});
+ order_queue.schedule(function(){
+ $('.paymentline-amount input:first').val(10000);
+ $('.paymentline-amount input:first').keyup();
+ },{duration:250});
+ order_queue.schedule(function(){
+ $('.pos-actionbar-button-list .button:eq(2)').click();
+ },{duration:250});
+ order_queue.schedule(function(){
+ def.resolve();
+ });
+ return def;
+ },{repeat: true, duration: interval});
+ };
+ };
+
+ if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
+ window.pos_test_ui = new module.UiTester();
+ }
+
+}
diff --git a/addons/point_of_sale/static/src/js/widget_base.js b/addons/point_of_sale/static/src/js/widget_base.js
index 124be4e9f82..30d85f2696a 100644
--- a/addons/point_of_sale/static/src/js/widget_base.js
+++ b/addons/point_of_sale/static/src/js/widget_base.js
@@ -41,10 +41,10 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of
},
show: function(){
- this.$el.show();
+ this.$el.removeClass('oe_hidden');
},
hide: function(){
- this.$el.hide();
+ this.$el.addClass('oe_hidden');
},
});
diff --git a/addons/point_of_sale/static/src/js/widget_scrollbar.js b/addons/point_of_sale/static/src/js/widget_scrollbar.js
index e4660a9c458..001e96d0bb6 100644
--- a/addons/point_of_sale/static/src/js/widget_scrollbar.js
+++ b/addons/point_of_sale/static/src/js/widget_scrollbar.js
@@ -81,7 +81,7 @@ function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_
$(window).unbind('resize',this.resize_handler);
$(window).bind('resize',this.resize_handler);
- this.target().unbind('mousewheel',this.target_mousweheel_handler);
+ this.target().unbind('mousewheel',this.target_mousewheel_handler);
this.target().bind('mousewheel',this.target_mousewheel_handler);
// because the rendering is asynchronous we must wait for the next javascript update
@@ -93,22 +93,18 @@ function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_
},0);
},
- // binds the window resize and the target scrolling events.
- // it is good advice not to bind these multiple_times
- bind_events:function(){
- $(window).resize(function(){
- });
- this.target().bind('mousewheel',function(event,delta){
- self.scroll(delta*self.wheel_step);
- });
+ destroy: function(){
+ $(window).unbind('resize',this.resize_handler);
+ this.target().unbind('mousewheel',this.target_mousewheel_handler);
+ this._super();
},
// shows the scrollbar. if animated is true, it will do it in an animated fashion
show: function(animated){ //FIXME: animated show and hide don't work ... ?
if(animated){
- this.$el.show().animate({'width':'48px'}, 500, 'swing');
+ this.$el.removeClass('oe_hidden').animate({'width':'48px'}, 500, 'swing');
}else{
- this.$el.show().css('width','48px');
+ this.$el.removeClass('oe_hidden').css('width','48px');
}
this.on_show(this);
},
@@ -117,9 +113,9 @@ function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_
hide: function(animated){
var self = this;
if(animated){
- this.$el.animate({'width':'0px'}, 500, 'swing', function(){ self.$el.hide();});
+ this.$el.animate({'width':'0px'}, 500, 'swing', function(){ self.$el.addClass('oe_hidden');});
}else{
- this.$el.hide().css('width','0px');
+ this.$el.addClass('oe_hidden').css('width','0px');
}
this.on_hide(this);
},
diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js
index 9f2a342190e..cfec71be68d 100644
--- a/addons/point_of_sale/static/src/js/widgets.js
+++ b/addons/point_of_sale/static/src/js/widgets.js
@@ -137,17 +137,15 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.model = options.model;
this.order = options.order;
- this.model.bind('change', _.bind( function() {
- this.refresh();
- }, this));
- },
- click_handler: function() {
- this.order.selectLine(this.model);
- this.trigger('order_line_selected');
+ this.model.bind('change', this.refresh, this);
},
renderElement: function() {
+ var self = this;
this._super();
- this.$el.click(_.bind(this.click_handler, this));
+ this.$el.click(function(){
+ self.order.selectLine(this.model);
+ self.trigger('order_line_selected');
+ });
if(this.model.is_selected()){
this.$el.addClass('selected');
}
@@ -156,6 +154,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.renderElement();
this.trigger('order_line_refreshed');
},
+ destroy: function(){
+ this.model.unbind('change',this.refresh,this);
+ this._super();
+ },
});
module.OrderWidget = module.PosBaseWidget.extend({
@@ -281,27 +283,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
});
- module.ProductWidget = module.PosBaseWidget.extend({
- template: 'ProductWidget',
- init: function(parent, options) {
- this._super(parent,options);
- this.model = options.model;
- this.model.attributes.weight = options.weight;
- this.next_screen = options.next_screen; //when a product is clicked, this screen is set
- this.click_product_action = options.click_product_action;
- },
- // returns the url of the product thumbnail
- renderElement: function() {
- this._super();
- this.$('img').replaceWith(this.pos_widget.image_cache.get_image(this.model.get_image_url()));
- var self = this;
- $("a", this.$el).click(function(e){
- if(self.click_product_action){
- self.click_product_action(self.model);
- }
- });
- },
- });
module.PaymentlineWidget = module.PosBaseWidget.extend({
template: 'PaymentlineWidget',
@@ -358,16 +339,16 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
var self = this;
this.order = options.order;
- this.order.bind('destroy',function(){ self.destroy(); });
- this.order.bind('change', function(){ self.renderElement(); });
- this.pos.bind('change:selectedOrder', function() {
- self.renderElement();
- }, this);
+ this.order.bind('destroy',this.destroy, this );
+ this.order.bind('change', this.renderElement, this );
+ this.pos.bind('change:selectedOrder', this.renderElement,this );
},
renderElement:function(){
this._super();
- this.$('button.select-order').off('click').click(_.bind(this.selectOrder, this));
- this.$('button.close-order').off('click').click(_.bind(this.closeOrder, this));
+ var self = this;
+ this.$el.click(function(){
+ self.selectOrder();
+ });
if( this.order === this.pos.get('selectedOrder') ){
this.$el.addClass('selected-order');
}
@@ -377,8 +358,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
selectedOrder: this.order
});
},
- closeOrder: function(event) {
- this.order.destroy();
+ destroy: function(){
+ this.order.unbind('destroy', this.destroy, this);
+ this.order.unbind('change', this.renderElement, this);
+ this.pos.unbind('change:selectedOrder', this.renderElement, this);
+ this._super();
},
});
@@ -422,9 +406,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
if(visible != this.visibility[element]){
this.visibility[element] = !!visible;
if(visible){
- this.$('.'+element).show();
+ this.$('.'+element).removeClass('oe_hidden');
}else{
- this.$('.'+element).hide();
+ this.$('.'+element).addClass('oe_hidden');
}
}
if(visible && action){
@@ -459,10 +443,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
return button;
},
show:function(){
- this.$el.show();
+ this.$el.removeClass('oe_hidden');
},
hide:function(){
- this.$el.hide();
+ this.$el.addClass('oe_hidden');
},
});
@@ -614,25 +598,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
renderElement: function() {
var self = this;
this._super();
-
- // free subwidgets memory from previous renders
- for(var i = 0, len = this.productwidgets.length; i < len; i++){
- this.productwidgets[i].destroy();
- }
- this.productwidgets = [];
if(this.scrollbar){
this.scrollbar.destroy();
}
var products = this.pos.get('products').models || [];
- for(var i = 0, len = products.length; i < len; i++){
- var product = new module.ProductWidget(self, {
- model: products[i],
- click_product_action: this.click_product_action,
- });
- this.productwidgets.push(product);
- product.appendTo(this.$('.product-list'));
- }
+
+ _.each(products,function(product,i){
+ var $product = $(QWeb.render('Product',{ widget:self, product: products[i] }));
+ $product.find('img').replaceWith(self.pos_widget.image_cache.get_image(products[i].get_image_url()));
+ $product.find('a').click(function(){ self.click_product_action(product); });
+ $product.appendTo(self.$('.product-list'));
+ });
+
this.scrollbar = new module.ScrollbarWidget(this,{
target_widget: this,
target_selector: '.product-list-scroller',
@@ -693,8 +671,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.$el.click(function(){ self.action(); });
}
},
- show: function(){ this.$el.show(); },
- hide: function(){ this.$el.hide(); },
+ show: function(){ this.$el.removeClass('oe_hidden'); },
+ hide: function(){ this.$el.addClass('oe_hidden'); },
});
// The debug widget lets the user control and monitor the hardware and software status
@@ -873,13 +851,12 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
new_order_button.selectOrder();
}, self);
- self.pos.get('orders').add(new module.Order({ pos: self.pos }));
+ self.pos.add_new_order();
self.build_widgets();
self.screen_selector.set_default_screen();
- window.screen_selector = self.screen_selector;
self.pos.barcode_reader.connect();
@@ -892,7 +869,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
}
instance.web.unblockUI();
- self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').hide();});
+ self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
self.pos.flush();
@@ -1081,11 +1058,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
if(visible !== this.leftpane_visible){
this.leftpane_visible = visible;
if(visible){
- $('#leftpane').show().animate({'width':this.leftpane_width},500,'swing');
+ $('#leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing');
$('#rightpane').animate({'left':this.leftpane_width},500,'swing');
}else{
var leftpane = $('#leftpane');
- $('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.hide(); });
+ $('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); });
$('#rightpane').animate({'left':'0px'},500,'swing');
}
}
@@ -1095,11 +1072,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
if(visible !== this.cashier_controls_visible){
this.cashier_controls_visible = visible;
if(visible){
- $('#loggedas').show();
- $('#rightheader').show();
+ $('#loggedas').removeClass('oe_hidden');
+ $('#rightheader').removeClass('oe_hidden');
}else{
- $('#loggedas').hide();
- $('#rightheader').hide();
+ $('#loggedas').addClass('oe_hidden');
+ $('#rightheader').addClass('oe_hidden');
}
}
},
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index e1ee84d78ea..7456c29b9e5 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -399,24 +399,24 @@
-
+
\n"
+" "
+
+#. module: portal_sale
+#: model:ir.actions.act_window,help:portal_sale.action_orders_portal
+msgid "We haven't sent you any sales order."
+msgstr "У Вас нет никаких заказов."
+
+#. module: portal_sale
+#: model:ir.model,name:portal_sale.model_account_invoice
+msgid "Invoice"
+msgstr "Счёт"
+
+#. module: portal_sale
+#: model:ir.actions.act_window,name:portal_sale.action_orders_portal
+msgid "Sale Orders"
+msgstr "Заказы продаж"
diff --git a/addons/project/res_config.py b/addons/project/res_config.py
index ddf98582ef3..101a868d574 100644
--- a/addons/project/res_config.py
+++ b/addons/project/res_config.py
@@ -28,30 +28,30 @@ class project_configuration(osv.osv_memory):
_columns = {
'module_project_mrp': fields.boolean('Generate tasks from sale orders',
- help ="""This feature automatically creates project tasks from service products in sale orders.
- More precisely, tasks are created for procurement lines with product of type 'Service',
- procurement method 'Make to Order', and supply method 'Manufacture'.
- This installs the module project_mrp."""),
+ help='This feature automatically creates project tasks from service products in sale orders. '
+ 'More precisely, tasks are created for procurement lines with product of type \'Service\', '
+ 'procurement method \'Make to Order\', and supply method \'Manufacture\'.\n'
+ '-This installs the module project_mrp.'),
'module_pad': fields.boolean("Use integrated collaborative note pads on task",
- help="""Lets the company customize which Pad installation should be used to link to new pads
- (for example: http://ietherpad.com/).
- This installs the module pad."""),
+ help='Lets the company customize which Pad installation should be used to link to new pads '
+ '(for example: http://ietherpad.com/).\n'
+ '-This installs the module pad.'),
'module_project_timesheet': fields.boolean("Record timesheet lines per tasks",
- help="""This allows you to transfer the entries under tasks defined for Project Management to
- the timesheet line entries for particular date and user, with the effect of creating,
- editing and deleting either ways.
- This installs the module project_timesheet."""),
+ help='This allows you to transfer the entries under tasks defined for Project Management to '
+ '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.
- This installs the module project_long_term."""),
+ 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.
- This installs the module project_issue."""),
+ help='Provides management of issues/bugs in projects.\n'
+ '-This installs the module project_issue.'),
'time_unit': fields.many2one('product.uom', 'Working time unit', required=True,
help="""This will set the unit of measure used in projects and tasks."""),
'module_project_issue_sheet': fields.boolean("Invoice working time on issues",
- help="""Provides timesheet support for the issues/bugs management in project.
- This installs the module project_issue_sheet."""),
+ help='Provides timesheet support for the issues/bugs management in project.\n'
+ '-This installs the module project_issue_sheet.'),
'group_tasks_work_on_tasks': fields.boolean("Log work activities on tasks",
implied_group='project.group_tasks_work_on_tasks',
help="Allows you to compute work on tasks."),
diff --git a/addons/project/tests/test_project_base.py b/addons/project/tests/test_project_base.py
index f82561766fe..70f8b59812f 100644
--- a/addons/project/tests/test_project_base.py
+++ b/addons/project/tests/test_project_base.py
@@ -19,10 +19,10 @@
#
##############################################################################
-from openerp.addons.mail.tests.test_mail_base import TestMailBase
+from openerp.addons.mail.tests.common import TestMail
-class TestProjectBase(TestMailBase):
+class TestProjectBase(TestMail):
def setUp(self):
super(TestProjectBase, self).setUp()
diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml
index 7fd5d4b9aaa..6bed8bcbf9c 100644
--- a/addons/purchase/purchase_view.xml
+++ b/addons/purchase/purchase_view.xml
@@ -14,9 +14,6 @@
-
Pricelist Versions
@@ -40,10 +37,13 @@
id="menu_purchase_config_pricelist" name="Pricelists"
parent="menu_purchase_config_purchase" sequence="50" groups="product.group_purchase_pricelist"/>
+
+
-
diff --git a/addons/purchase/res_config.py b/addons/purchase/res_config.py
index f5f2dfcdda5..87db14c5a90 100644
--- a/addons/purchase/res_config.py
+++ b/addons/purchase/res_config.py
@@ -34,8 +34,8 @@ class purchase_config_settings(osv.osv_memory):
], 'Default invoicing control method', required=True, default_model='purchase.order'),
'group_purchase_pricelist':fields.boolean("Manage pricelist per supplier",
implied_group='product.group_purchase_pricelist',
- help="""Allows to manage different prices based on rules per category of Supplier.
- Example: 10% for retailers, promotion of 5 EUR on this product, etc."""),
+ help='Allows to manage different prices based on rules per category of Supplier.\n'
+ 'Example: 10% for retailers, promotion of 5 EUR on this product, etc.'),
'group_uom':fields.boolean("Manage different units of measure for products",
implied_group='product.group_uom',
help="""Allows you to select and maintain different units of measure for products."""),
@@ -43,19 +43,19 @@ class purchase_config_settings(osv.osv_memory):
implied_group='product.group_costing_method',
help="""Allows you to compute product cost price based on average cost."""),
'module_warning': fields.boolean("Alerts by products or supplier",
- help="""Allow to configure notification on products and trigger them when a user wants to purchase a given product or a given supplier.
-Example: Product: this product is deprecated, do not purchase more than 5.
- Supplier: don't forget to ask for an express delivery."""),
+ help='Allow to configure notification on products and trigger them when a user wants to purchase a given product or a given supplier.\n'
+ 'Example: Product: this product is deprecated, do not purchase more than 5.\n'
+ 'Supplier: don\'t forget to ask for an express delivery.'),
'module_purchase_double_validation': fields.boolean("Force two levels of approvals",
- help="""Provide a double validation mechanism for purchases exceeding minimum amount.
- This installs the module purchase_double_validation."""),
+ help='Provide a double validation mechanism for purchases exceeding minimum amount.\n'
+ '-This installs the module purchase_double_validation.'),
'module_purchase_requisition': fields.boolean("Manage purchase requisitions",
- help="""Purchase Requisitions are used when you want to request quotations from several suppliers for a given set of products.
- You can configure per product if you directly do a Request for Quotation
- to one supplier or if you want a purchase requisition to negotiate with several suppliers."""),
+ help='Purchase Requisitions are used when you want to request quotations from several suppliers for a given set of products.\n'
+ 'You can configure per product if you directly do a Request for Quotation '
+ 'to one supplier or if you want a purchase requisition to negotiate with several suppliers.'),
'module_purchase_analytic_plans': fields.boolean('Use multiple analytic accounts on purchase orders',
- help ="""Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.
- This installs the module purchase_analytic_plans."""),
+ help='Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.\n'
+ '-This installs the module purchase_analytic_plans.'),
'group_analytic_account_for_purchases': fields.boolean('Analytic accounting for purchases',
implied_group='purchase.group_analytic_accounting',
help="Allows you to specify an analytic account on purchase orders."),
@@ -77,8 +77,8 @@ class account_config_settings(osv.osv_memory):
_inherit = 'account.config.settings'
_columns = {
'module_purchase_analytic_plans': fields.boolean('Use multiple analytic accounts on orders',
- help ="""Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.
- This installs the module purchase_analytic_plans."""),
+ help='Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.\n'
+ '-This installs the module purchase_analytic_plans.'),
'group_analytic_account_for_purchases': fields.boolean('Analytic accounting for purchases',
implied_group='purchase.group_analytic_accounting',
help="Allows you to specify an analytic account on purchase orders."),
diff --git a/addons/sale/res_config.py b/addons/sale/res_config.py
index bfbaa5f0a6f..04958521ab5 100644
--- a/addons/sale/res_config.py
+++ b/addons/sale/res_config.py
@@ -34,15 +34,15 @@ class sale_configuration(osv.osv_memory):
implied_group='sale.group_invoice_so_lines',
help="To allow your salesman to make invoices for sales order lines using the menu 'Lines to Invoice'."),
'timesheet': fields.boolean('Prepare invoices based on timesheets',
- help = """For modifying account analytic view to show important data to project manager of services companies.
- You can also view the report of account analytic summary user-wise as well as month wise.
- This installs the module account_analytic_analysis."""),
+ help='For modifying account analytic view to show important data to project manager of services companies.'
+ 'You can also view the report of account analytic summary user-wise as well as month wise.\n'
+ '-This installs the module account_analytic_analysis.'),
'module_account_analytic_analysis': fields.boolean('Use contracts management',
- help = """Allows to define your customer contracts conditions: invoicing
- method (fixed price, on timesheet, advance invoice), the exact pricing
- (650€/day for a developer), the duration (one year support contract).
- You will be able to follow the progress of the contract and invoice automatically.
- It installs the account_analytic_analysis module."""),
+ help='Allows to define your customer contracts conditions: invoicing '
+ 'method (fixed price, on timesheet, advance invoice), the exact pricing '
+ '(650€/day for a developer), the duration (one year support contract).\n'
+ 'You will be able to follow the progress of the contract and invoice automatically.\n'
+ '-It installs the account_analytic_analysis module.'),
'time_unit': fields.many2one('product.uom', 'The default working time unit for services is'),
'group_sale_pricelist':fields.boolean("Use pricelists to adapt your price per customers",
implied_group='product.group_sale_pricelist',
@@ -58,26 +58,26 @@ Example: 10% for retailers, promotion of 5 EUR on this product, etc."""),
implied_group='product.group_product_variant',
help="""Allow to manage several variants per product. As an example, if you sell T-Shirts, for the same "Linux T-Shirt", you may have variants on sizes or colors; S, M, L, XL, XXL."""),
'module_warning': fields.boolean("Allow configuring alerts by customer or products",
- help="""Allow to configure notification on products and trigger them when a user wants to sell a given product or a given customer.
-Example: Product: this product is deprecated, do not purchase more than 5.
- Supplier: don't forget to ask for an express delivery."""),
+ help='Allow to configure notification on products and trigger them when a user wants to sell a given product or a given customer.\n'
+ 'Example: Product: this product is deprecated, do not purchase more than 5.\n'
+ 'Supplier: don\'t forget to ask for an express delivery.'),
'module_sale_margin': fields.boolean("Display margins on sales orders",
- help="""This adds the 'Margin' on sales order.
- This gives the profitability by calculating the difference between the Unit Price and Cost Price.
- This installs the module sale_margin."""),
+ help='This adds the \'Margin\' on sales order.\n'
+ 'This gives the profitability by calculating the difference between the Unit Price and Cost Price.\n'
+ '-This installs the module sale_margin.'),
'module_sale_journal': fields.boolean("Allow batch invoicing of delivery orders through journals",
- help="""Allows you to categorize your sales and deliveries (picking lists) between different journals,
- and perform batch operations on journals.
- This installs the module sale_journal."""),
+ help='Allows you to categorize your sales and deliveries (picking lists) between different journals, '
+ 'and perform batch operations on journals.\n'
+ '-This installs the module sale_journal.'),
'module_analytic_user_function': fields.boolean("One employee can have different roles per contract",
- help="""Allows you to define what is the default function of a specific user on a given account.
- This is mostly used when a user encodes his timesheet. The values are retrieved and the fields are auto-filled.
- But the possibility to change these values is still available.
- This installs the module analytic_user_function."""),
+ help='Allows you to define what is the default function of a specific user on a given account.\n'
+ 'This is mostly used when a user encodes his timesheet. The values are retrieved and the fields are auto-filled. '
+ 'But the possibility to change these values is still available.\n'
+ '-This installs the module analytic_user_function.'),
'module_project': fields.boolean("Project"),
'module_sale_stock': fields.boolean("Trigger delivery orders automatically from sales orders",
- help="""Allows you to Make Quotation, Sale Order using different Order policy and Manage Related Stock.
- This installs the module sale_stock."""),
+ help='Allows you to Make Quotation, Sale Order using different Order policy and Manage Related Stock.\n'
+ '-This installs the module sale_stock.'),
}
def default_get(self, cr, uid, fields, context=None):
diff --git a/addons/sale_stock/res_config.py b/addons/sale_stock/res_config.py
index 4c3b92f1d33..f52dcf89659 100644
--- a/addons/sale_stock/res_config.py
+++ b/addons/sale_stock/res_config.py
@@ -33,18 +33,18 @@ class sale_configuration(osv.osv_memory):
implied_group='sale_stock.group_invoice_deli_orders',
help="To allow your salesman to make invoices for Delivery Orders using the menu 'Deliveries to Invoice'."),
'task_work': fields.boolean("Prepare invoices based on task's activities",
- help="""Lets you transfer the entries under tasks defined for Project Management to
- the Timesheet line entries for particular date and particular user with the effect of creating, editing and deleting either ways
- and to automatically creates project tasks from procurement lines.
- This installs the modules project_timesheet and project_mrp."""),
+ help='Lets you transfer the entries under tasks defined for Project Management to '
+ 'the Timesheet line entries for particular date and particular user with the effect of creating, editing and deleting either ways '
+ 'and to automatically creates project tasks from procurement lines.\n'
+ '-This installs the modules project_timesheet and project_mrp.'),
'default_order_policy': fields.selection(
[('manual', 'Invoice based on sales orders'), ('picking', 'Invoice based on deliveries')],
'The default invoicing method is', default_model='sale.order',
help="You can generate invoices based on sales orders or based on shippings."),
'module_delivery': fields.boolean('Allow adding shipping costs',
- help ="""Allows you to add delivery methods in sales orders and delivery orders.
- You can define your own carrier and delivery grids for prices.
- This installs the module delivery."""),
+ help='Allows you to add delivery methods in sales orders and delivery orders.\n'
+ 'You can define your own carrier and delivery grids for prices.\n'
+ '-This installs the module delivery.'),
'default_picking_policy' : fields.boolean("Deliver all at once when all products are available.",
help = "Sales order by default will be configured to deliver all products at once instead of delivering each product when it is available. This may have an impact on the shipping price."),
'group_mrp_properties': fields.boolean('Product properties on order lines',
diff --git a/addons/stock/res_config.py b/addons/stock/res_config.py
index d0b0563875c..453f86bc607 100644
--- a/addons/stock/res_config.py
+++ b/addons/stock/res_config.py
@@ -27,12 +27,12 @@ class stock_config_settings(osv.osv_memory):
_columns = {
'module_claim_from_delivery': fields.boolean("Allow claim on deliveries",
- help="""Adds a Claim link to the delivery order.
- This installs the module claim_from_delivery."""),
+ help='Adds a Claim link to the delivery order.\n'
+ '-This installs the module claim_from_delivery.'),
'module_stock_invoice_directly': fields.boolean("Create and open the invoice when the user finish a delivery order",
- help="""This allows to automatically launch the invoicing wizard if the delivery is
- to be invoiced when you send or deliver goods.
- This installs the module stock_invoice_directly."""),
+ help='This allows to automatically launch the invoicing wizard if the delivery is '
+ 'to be invoiced when you send or deliver goods.\n'
+ '-This installs the module stock_invoice_directly.'),
'module_product_expiry': fields.boolean("Expiry date on serial numbers",
help="""Track different dates on products and serial numbers.
The following dates can be tracked:
@@ -42,17 +42,17 @@ The following dates can be tracked:
- alert date.
This installs the module product_expiry."""),
'module_stock_location': fields.boolean("Create push/pull logistic rules",
- help="""Provide push and pull inventory flows. Typical uses of this feature are:
- manage product manufacturing chains, manage default locations per product,
- define routes within your warehouse according to business needs, etc.
- This installs the module stock_location."""),
+ help='Provide push and pull inventory flows. Typical uses of this feature are: '
+ 'manage product manufacturing chains, manage default locations per product, '
+ 'define routes within your warehouse according to business needs, etc.\n'
+ '-This installs the module stock_location.'),
'group_uom': fields.boolean("Manage different units of measure for products",
implied_group='product.group_uom',
help="""Allows you to select and maintain different units of measure for products."""),
'group_uos': fields.boolean("Invoice products in a different unit of measure than the sales order",
implied_group='product.group_uos',
- help="""Allows you to sell units of a product, but invoice based on a different unit of measure.
- For instance, you can sell pieces of meat that you invoice based on their weight."""),
+ help='Allows you to sell units of a product, but invoice based on a different unit of measure.\n'
+ 'For instance, you can sell pieces of meat that you invoice based on their weight.'),
'group_stock_packaging': fields.boolean("Allow to define several packaging methods on products",
implied_group='product.group_stock_packaging',
help="""Allows you to create and manage your packaging dimensions and types you want to be maintained in your system."""),
@@ -67,8 +67,8 @@ This installs the module product_expiry."""),
help="""Allows to configure inventory valuations on products and product categories."""),
'group_stock_multiple_locations': fields.boolean("Manage multiple locations and warehouses",
implied_group='stock.group_locations',
- help="""This allows to configure and use multiple stock locations and warehouses,
- instead of having a single default one."""),
+ help='This allows to configure and use multiple stock locations and warehouses, '
+ 'instead of having a single default one.'),
'decimal_precision': fields.integer('Decimal precision on weight', help="As an example, a decimal precision of 2 will allow weights like: 9.99 kg, whereas a decimal precision of 4 will allow weights like: 0.0231 kg."),
}