[MERGE] with trunk

bzr revid: dizzy.zala@gmail.com-20130919084914-iz5egv2twh8mzo42
This commit is contained in:
Dharmraj Zala (OpenERP Trainee) 2013-09-19 14:19:14 +05:30
commit e394117423
77 changed files with 4385 additions and 981 deletions

View File

@ -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."),

View File

@ -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:

View File

@ -131,6 +131,10 @@
<field name="module_web_linkedin"/>
<label for="module_web_linkedin"/>
</div>
<div>
<field name="module_mass_mailing" class="oe_inline"/>
<label for="module_mass_mailing"/>
</div>
</div>
</group>
</div>

View File

@ -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'),

View File

@ -169,16 +169,17 @@
<field name="description"/>
</page>
<page string="Extra Info">
<group string="Categorization" groups="base.group_multi_company,base.group_no_one" name="categorization">
<field name="company_id"
groups="base.group_multi_company"
widget="selection" colspan="2"/>
</group>
<group string="Mailings">
<field name="opt_out"/>
</group>
<group string="Misc">
<group>
<group>
<group string="Categorization" groups="base.group_multi_company,base.group_no_one" name="categorization">
<field name="company_id"
groups="base.group_multi_company"
widget="selection"/>
</group>
<group string="Mailings">
<field name="opt_out"/>
<field name="message_bounce"/>
</group>
<group string="Misc">
<field name="probability" groups="base.group_no_one"/>
<field name="active"/>
<field name="referred"/>

View File

@ -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."""),

View File

@ -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:

View File

@ -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': '<p>Dummy body</p>'},
{'subject': 'Forget me subject', 'body': '<p>Dummy body</p>', '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', [])]

View File

@ -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:

View File

@ -11,7 +11,7 @@
<label string="Template Recipients" for="partner_to"
groups="base.group_no_one"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<div groups="base.group_no_one"
<div groups="base.group_no_one" name="template_recipients"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}">
<group class="oe_grey">
<!-- <label string="Partners" for="partner_to"/> -->

View File

@ -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.'),
}

View File

@ -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):

View File

@ -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:

View File

@ -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')

View File

@ -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``.

View File

@ -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 = '<small>%s</small>' % (sent_by % {
'company': company,
'openerp': "<a style='color:inherit' href='https://www.openerp.com/'>OpenERP</a>"
})
'company': company,
'openerp': "<a style='color:inherit' href='https://www.openerp.com/'>OpenERP</a>"
})
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)

View File

@ -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 <pigs@openerp.com' format
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 values.get('mail_message_id'):
message = self.pool.get('mail.message').browse(cr, uid, values.get('mail_message_id'), context=context)
if message.reply_to:
email_reply_to = message.reply_to
format_name = False
if not model:
model = message.model
if not res_id:
res_id = message.res_id
if not email_from:
email_from = message.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 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:
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 <email_address>'
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 _("""<small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small>""") % 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

View File

@ -14,7 +14,7 @@
<sheet>
<label for="subject" class="oe_edit_only"/>
<h2><field name="subject"/></h2>
<div>
<div style="vertical-align: top;">
by <field name="author_id" class="oe_inline" string="User"/> on <field name="date" class="oe_inline"/>
<button name="%(action_email_compose_message_wizard)d" string="Reply" type="action" icon="terp-mail-replied"
context="{'default_composition_mode':'reply', 'default_parent_id': active_id}" states='received,sent,exception,cancel'/>
@ -32,20 +32,26 @@
</page>
<page string="Advanced" groups="base.group_no_one">
<group>
<group>
<field name="auto_delete"/>
<field name="type"/>
<field name="state"/>
<field name="mail_server_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
<group>
<field name="message_id"/>
<field name="references"/>
<field name="partner_ids" widget="many2many_tags"/>
<field name="notified_partner_ids" widget="many2many_tags"/>
</group>
<div>
<group string="Status">
<field name="auto_delete"/>
<field name="type"/>
<field name="state"/>
<field name="mail_server_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
</div>
<div>
<group string="Headers">
<field name="message_id"/>
<field name="references"/>
</group>
<group string="Recipients">
<field name="partner_ids" widget="many2many_tags"/>
<field name="notified_partner_ids" widget="many2many_tags"/>
</group>
</div>
</group>
</page>
<page string="Attachments">

View File

@ -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 <email_address>'
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

View File

@ -56,7 +56,8 @@
<field name="priority">25</field>
<field name="arch" type="xml">
<search string="Messages Search">
<field name="subject" string="Content" filter_domain="['|', ('subject', 'ilike', self), ('body', 'ilike', self)]" />
<field name="body" string="Content" filter_domain="['|', ('subject', 'ilike', self), ('body', 'ilike', self)]" />
<field name="subject"/>
<field name="type"/>
<field name="author_id"/>
<field name="partner_ids"/>
@ -66,23 +67,13 @@
<filter string="To Read"
name="message_unread" help="Show messages to read"
domain="[('to_read', '=', True)]"/>
<filter string="Read"
name="message_read" help="Show already read messages"
domain="[('to_read', '=', False)]"/>
<separator/>
<filter string="Comments"
name="comments" help="Comments"
domain="[('type', '=', 'comment')]"/>
<filter string="Notifications"
name="notifications" help="Notifications"
domain="[('type', '=', 'notification')]"/>
<filter string="Emails"
name="emails" help="Emails"
domain="[('type', '=', 'email')]"/>
<separator/>
<filter string="Has attachments"
name="attachments"
domain="[('attachment_ids', '!=', False)]"/>
<group expand="0" string="Group By...">
<filter string="Type" name="thread" domain="[]" context="{'group_by':'type'}"/>
</group>
</search>
</field>
</record>

View File

@ -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.

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -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',

View File

@ -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: <whatever-2a840@postmaster.twitter.com>
@ -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 <test@test.fr>'], link_mail=False)[0]
self.assertEqual(partner_info['full_name'], 'Maybe Raoul <test@test.fr>',
'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 <test@test.fr>'], 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 <test@test.fr>'], 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@raoul.fr>'
raoul_from_alias = 'Raoul Grosbedon <raoul@schlouby.fr>'
raoul_reply = '"Followers of Pigs" <raoul@raoul.fr>'
raoul_reply_alias = '"Followers of Pigs" <group+pigs@schlouby.fr>'
# 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')

View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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')

View File

@ -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 <test@test.fr>'], link_mail=False)[0]
self.assertEqual(partner_info['full_name'], 'Maybe Raoul <test@test.fr>',
'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 <test@test.fr>'], 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 <test@test.fr>'], 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@raoul.fr>'
raoul_from_alias = 'Raoul Grosbedon <raoul@schlouby.fr>'
raoul_reply = '"Followers of Pigs" <raoul@raoul.fr>'
raoul_reply_alias = '"Followers of Pigs" <group+pigs@schlouby.fr>'
# --------------------------------------------------
# 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):

View File

@ -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. """

View File

@ -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]

View File

@ -19,15 +19,7 @@
<field name="email_from"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="subject" placeholder="Subject..." required="True"/>
<field name="post"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="notify"
attrs="{'invisible':['|', ('post', '!=', True), ('composition_mode', '!=', 'mass_mail')]}"/>
<field name="same_thread"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="reply_to" placeholder="Email address te redirect replies..."
attrs="{'invisible':['|', ('same_thread', '=', True), ('composition_mode', '!=', 'mass_mail')],
'required':[('same_thread', '!=', True)]}"/>
<!-- classic message composer -->
<label for="partner_ids" string="Recipients"
attrs="{'invisible':[('composition_mode', '=', 'mass_mail')]}"/>
<div groups="base.group_user"
@ -40,6 +32,16 @@
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
context="{'force_email':True, 'show_email':True}"/>
</div>
<!-- mass post / mass mailing -->
<field name="post"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="notify"
attrs="{'invisible':['|', ('post', '!=', True), ('composition_mode', '!=', 'mass_mail')]}"/>
<field name="same_thread"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="reply_to" placeholder="Email address te redirect replies..."
attrs="{'invisible':['|', ('same_thread', '=', True), ('composition_mode', '!=', 'mass_mail')],
'required':[('same_thread', '!=', True)]}"/>
</group>
<field name="body"/>
<field name="attachment_ids" widget="many2many_binary" string="Attach a file"/>

View File

@ -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:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>
#
##############################################################################
import mass_mailing
import mail_mail
import mail_thread
import wizard
import controllers

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>
#
##############################################################################
{
'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,
}

View File

@ -0,0 +1,3 @@
import main
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -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/<int:mail_id>/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=="

View File

@ -0,0 +1,9 @@
.. _changelog:
Changelog
=========
`trunk (saas-2)`
----------------
- added module

View File

@ -0,0 +1,13 @@
Mass Mailing module documentation
=================================
Mass Mailing documentation topics
'''''''''''''''''''''''''''''''''
Changelog
'''''''''
.. toctree::
:maxdepth: 1
changelog.rst

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<!-- Bounce Email Alias -->
<record id="icp_mail_bounce_alias" model="ir.config_parameter">
<field name="key">mail.bounce.alias</field>
<field name="value">bounce</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>
#
##############################################################################
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 '<img src="%s" alt=""/>' % 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

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>
#
##############################################################################
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)

View File

@ -0,0 +1,376 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>
#
##############################################################################
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

View File

@ -0,0 +1,90 @@
<?xml version="1.0"?>
<openerp>
<!-- <data noupdate="1"> -->
<data>
<record id="mass_mail_template_1" model="email.template">
<field name="name">Partner Newsletter 1</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="auto_delete" eval="False"/>
<field name="partner_to">${object.id}</field>
<field name="body_html"><![CDATA[<p>Hello</p>]]></field>
</record>
<record id="mass_mail_template_2" model="email.template">
<field name="name">Partner Newsletter 2</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="auto_delete" eval="False"/>
<field name="partner_to">${object.id}</field>
<field name="body_html"><![CDATA[<p>Hello</p>]]></field>
</record>
<record id="mass_mail_campaign_1" model="mail.mass_mailing.campaign">
<field name="name">Partners Newsletter</field>
<field name="user_id" eval="ref('base.user_root')"/>
</record>
<record id="mass_mail_1" model="mail.mass_mailing">
<field name="name">First Newsletter</field>
<field name="template_id" eval="ref('mass_mail_template_1')"/>
<field name="date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
</record>
<record id="mass_mail_2" model="mail.mass_mailing">
<field name="name">Second Newsletter</field>
<field name="template_id" eval="ref('mass_mail_template_2')"/>
<field name="date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
</record>
<record id="mass_mail_email_1" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111000@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field>
</record>
<record id="mass_mail_email_2" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111001@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field>
</record>
<record id="mass_mail_email_3" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111002@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field>
</record>
<record id="mass_mail_email_4" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111003@OpenERP.com</field>
<field name="state">sent</field>
</record>
<record id="mass_mail_email_5" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111004@OpenERP.com</field>
<field name="bounced" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field>
</record>
<record id="mass_mail_email_2_1" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111005@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field>
</record>
<record id="mass_mail_email_2_2" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111006@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field>
</record>
<record id="mass_mail_email_2_3" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111007@OpenERP.com</field>
<field name="state">sent</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,376 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- MASS MAILING !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_search">
<field name="name">mail.mass_mailing.search</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name" string="Mailings"/>
<field name="mass_mailing_campaign_id"/>
<field name="template_id"/>
<group expand="0" string="Group By...">
<filter string="Campaign" name="group_mass_mailing_campaign_id"
context="{'group_by': 'mass_mailing_campaign_id'}"/>
<filter string="Template" name="group_template_id"
context="{'group_by': 'template_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_tree">
<field name="name">mail.mass_mailing.tree</field>
<field name="model">mail.mass_mailing</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings">
<field name="name"/>
<field name="sent"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="mass_mailing_campaign_id" invisible="1"/>
<field name="template_id" invisible="1"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_form">
<field name="name">mail.mass_mailing.form</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<form string="Mass Mailing" version="7.0">
<sheet>
<group>
<group>
<field name="name"/>
<field name="mass_mailing_campaign_id" readonly="True"/>
</group>
<group>
<field name="template_id"/>
<field name="domain"/>
<field name="date"/>
</group>
</group>
<group string="Email Statistics">
<field name="statistics_ids" nolabel="1" colspan="2"/>
<group>
<field name="sent"/>
<field name="opened"/>
<field name="bounced"/>
</group>
<group>
<field name="delivered"/>
<field name="replied"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_kanban">
<field name="name">mail.mass_mailing.kanban</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<kanban>
<field name='color'/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing oe_kanban_mass_mailing_segment">
<div class="oe_kanban_content">
<div>
<h3>
<field name="name"/>
</h3>
<p style="margin-left: 10px; margin-top: 8px;">
Sent: <field name="date"/><br />
Campaign: <field name="mass_mailing_campaign_id"/>
</p>
</div>
<div>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="sent"/></span><br />
Sent
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="delivered"/></span><br />
Delivered
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="opened"/></span><br />
Opened
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="replied"/></span><br />
Replied
</p>
</div>
<div>
<div class="oe_sparkline_container">
<h4 class="oe_sparkline_bar_title">Opened</h4><br />
<field name="opened_monthly" widget="sparkline_bar" options="{'height': '50px', 'barWidth': 10, 'barSpacing': 5}"/>
</div>
<div class="oe_sparkline_container">
<h4 class="oe_sparkline_bar_title">Replied</h4><br />
<field name="replied_monthly" widget="sparkline_bar" options="{'height': '50px', 'barWidth': 10, 'barSpacing': 5}"/>
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailings" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
</record>
<record id="action_view_mass_mailings_from_campaign" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'search_default_mass_mailing_campaign_id': [active_id],
'default_mass_mailing_campaign_id': active_id,
}
</field>
</record>
<!-- MASS MAILING CAMPAIGNS !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_search">
<field name="name">mail.mass_mailing.campaign.search</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<search string="Mass Mailing Campaigns">
<field name="name" string="Campaigns"/>
<field name="user_id"/>
<group expand="0" string="Group By...">
<filter string="Responsibles" name="group_user_id"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_tree">
<field name="name">mail.mass_mailing.campaign.tree</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailing Campaigns">
<field name="name"/>
<field name="user_id"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_form">
<field name="name">mail.mass_mailing.campaign.form</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<form string="Mass Mailing Campaign" version="7.0">
<header>
<button name="launch_mass_mailing_create_wizard" type="object"
class="oe_highlight" string="Create a New Mailing"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="user_id"/>
</group>
<group>
<group>
<field name="sent"/>
<field name="opened"/>
<field name="bounced"/>
</group>
<group>
<field name="delivered"/>
<field name="replied"/>
</group>
</group>
<group>
<field name="mass_mailing_ids" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_kanban">
<field name="name">mail.mass_mailing.campaign.kanban</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<kanban>
<field name="mass_mailing_kanban_ids"/>
<field name='sent'/>
<field name='color'/>
<templates>
<t t-name="mass_mailing.mass_mailing">
<div class="oe_mass_mailings">
<div>
<a name="%(action_view_mass_mailings_from_campaign)d" type="action">
<h4><t t-raw="mass_mailing.name"/></h4>
</a>
</div>
<div>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.sent"/></span><br />
Sent
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.delivered"/></span><br />
Delivered
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.opened"/></span><br />
Opened
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.replied"/></span><br />
Replied
</p>
</div>
</div>
</t>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing oe_kanban_mass_mailing_campaign">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')">
<li><a type="edit">Settings</a></li>
</t>
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
<li><ul class="oe_kanban_colorpicker" data-field="color"/></li>
</ul>
</div>
<div class="oe_kanban_content">
<h3>
<field name="name"/>
</h3>
<div>
<field name="delivered" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/>
<field name="opened" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/>
<field name="replied" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/>
</div>
<t t-foreach='record.mass_mailing_kanban_ids.value' t-as='mass_mailing'>
<t t-call="mass_mailing.mass_mailing"/>
</t>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailing_campaigns" model="ir.actions.act_window">
<field name="name">Mass Mailing Campaigns</field>
<field name="res_model">mail.mass_mailing.campaign</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new mass mailing campaign.
</p><p>
Create a campaign to structure mass mailing and get analysis from email status.
</p>
</field>
</record>
<!-- MAIL MAIL STATISTICS !-->
<record model="ir.ui.view" id="view_mail_mail_statistics_search">
<field name="name">mail.mail.statistics.search</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<search string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_tree">
<field name="name">mail.mail.statistics.tree</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<tree string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_form">
<field name="name">mail.mail.statistics.form</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<form string="Mail Statistics" version="7.0">
<group>
<group>
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</group>
<group>
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id"/>
<field name="template_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
</group>
</form>
</field>
</record>
<record id="action_view_mail_mail_statistics" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mail.mail.statistics</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Top menu item -->
<menuitem name="Marketing" id="base.marketing_menu" sequence="85"/>
<!-- Add in marketing -->
<menuitem name="Mass Mailing" id="mass_mailing_campaign"
parent="base.marketing_menu" sequence="1"/>
<menuitem name="Campaigns" id="menu_email_campaigns"
parent="mass_mailing_campaign" sequence="1"
action="action_view_mass_mailing_campaigns"/>
<menuitem name="Mass Mailings" id="menu_email_mass_mailings"
parent="mass_mailing_campaign" sequence="2"
action="action_view_mass_mailings"/>
<!-- Add in Technical/Email -->
<menuitem name="Mail Statistics" id="menu_email_statistics"
parent="base.menu_email" sequence="50"
action="action_view_mail_mail_statistics"/>
</data>
</openerp>

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mass_mailing_campaign mail.mass_mailing.campaign model_mail_mass_mailing_campaign 1 1 1 0
3 access_mass_mailing_campaign_system mail.mass_mailing.campaign.system model_mail_mass_mailing_campaign base.group_system 1 1 1 1
4 access_mass_mailing mail.mass_mailing model_mail_mass_mailing 1 1 1 0
5 access_mass_mailing_system mail.mass_mailing.system model_mail_mass_mailing base.group_system 1 1 1 1
6 access_mail_mail_statistics mail.mail.statistics model_mail_mail_statistics 1 1 1 1

View File

@ -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
*/

View File

@ -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);
}
},
});
};

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.addons.mass_mailing.tests import test_mail
checks = [
test_mail,
]

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>
#
##############################################################################
import mail_compose_message
import mail_mass_mailing_create_segment

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>
#
##############################################################################
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

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Add mass mail campaign to the mail.compose.message form view -->
<record model="ir.ui.view" id="email_compose_form_mass_mailing">
<field name="name">mail.compose.message.form.mass_mailing</field>
<field name="model">mail.compose.message</field>
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='notify']" position="after">
<field name="mass_mailing_campaign_id"
attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}"/>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 <http://www.gnu.org/licenses/>
#
##############################################################################
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,
}

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Wizard form view -->
<record model="ir.ui.view" id="view_mail_mass_mailing_create_form">
<field name="name">mail.mass_mailing.create.form</field>
<field name="model">mail.mass_mailing.create</field>
<field name="arch" type="xml">
<form string="Create a Mass Mailing" version="7.0">
<group>
<field name="model_model" invisible="1"/>
<field name="domain" invisible="1"/>
<label for="mass_mailing_campaign_id"/>
<div>
<field name="mass_mailing_campaign_id"/>
<p class="oe_grey"
attrs="{'invisible': [('mass_mailing_campaign_id', '!=', False)]}">
Please choose a mass mailing campaign that will hold the new mailing.
</p>
</div>
<label for="model_id"/>
<div>
<field name="model_id"
on_change="on_change_model_id(model_id, context)"/>
<p class="oe_grey"
attrs="{'invisible': [('model_id', '!=', False)]}">
Please choose a model on which you will run the mass mailing.
</p>
</div>
<label for="filter_id"/>
<div>
<field name="filter_id"
on_change="on_change_filter_id(filter_id, context)"/>
<p class="oe_grey"
attrs="{'invisible': [('filter_id', '!=', False)]}">
Please choose a filter that will be applied on the model
to find the records on which you will run the mass mailing.
</p>
</div>
<label for="model_id"/>
<div>
<field name="template_id"/>
<p class="oe_grey"
attrs="{'invisible': [('template_id', '!=', False)]}">
Please choose the template to use to render the emails
to send.
</p>
</div>
<label for="name"/>
<div>
<field name="name"/>
<p class="oe_grey"
attrs="{'invisible': [('name', '!=', False)]}">
Please choose the name of the mailing.
</p>
</div>
<button name="launch_composer" type="object"
string="Create mailing and launch email composer"/>
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_mail_mass_mailing_create">
<field name="name">Create Mass Mailing</field>
<field name="res_model">mail.mass_mailing.create</field>
<field name="src_model">mail.mass_mailing.campaign</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</openerp>

View File

@ -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:

View File

@ -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': [

View File

@ -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

View File

@ -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;

View File

@ -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});

View File

@ -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');
};

View File

@ -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();

View File

@ -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);
},
});

View File

@ -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();
}
}

View File

@ -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');
},
});

View File

@ -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);
},

View File

@ -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');
}
}
},

View File

@ -399,24 +399,24 @@
</div>
</t>
<t t-name="ProductWidget">
<t t-name="Product">
<li class='product'>
<a href="#">
<div class="product-img">
<img src='' /> <!-- the product thumbnail -->
<t t-if="!widget.model.get('to_weight')">
<t t-if="!product.get('to_weight')">
<span class="price-tag">
<t t-esc="widget.format_currency(widget.model.get('price'))"/>
<t t-esc="widget.format_currency(product.get('price'))"/>
</span>
</t>
<t t-if="widget.model.get('to_weight')">
<t t-if="product.get('to_weight')">
<span class="price-tag">
<t t-esc="widget.format_currency(widget.model.get('price'))+'/Kg'"/>
<t t-esc="widget.format_currency(product.get('price'))+'/Kg'"/>
</span>
</t>
</div>
<div class="product-name">
<t t-esc="widget.model.get('name')"/>
<t t-esc="product.get('name')"/>
</div>
</a>
</li>

View File

@ -19,12 +19,12 @@
#
##############################################################################
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.misc import mute_logger
class test_portal(TestMailBase):
class test_portal(TestMail):
def setUp(self):
super(test_portal, self).setUp()

View File

@ -0,0 +1,554 @@
# Russian 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 <EMAIL@ADDRESS>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-12-21 17:06+0000\n"
"PO-Revision-Date: 2013-09-16 07:59+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Russian <ru@li.org>\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-17 04:37+0000\n"
"X-Generator: Launchpad (build 16765)\n"
#. module: portal_sale
#: model:ir.model,name:portal_sale.model_account_config_settings
msgid "account.config.settings"
msgstr ""
#. module: portal_sale
#: model:ir.actions.act_window,help:portal_sale.portal_action_invoices
msgid "We haven't sent you any invoice."
msgstr "У Вас нет никаких счетов."
#. module: portal_sale
#: model:email.template,report_name:portal_sale.email_template_edi_sale
msgid ""
"${(object.name or '').replace('/','_')}_${object.state == 'draft' and "
"'draft' or ''}"
msgstr ""
"${(object.name or '').replace('/','_')}_${object.state == 'draft' and "
"'проект' or ''}"
#. module: portal_sale
#: model:res.groups,name:portal_sale.group_payment_options
msgid "View Online Payment Options"
msgstr "Посмотреть варианты оплаты онлайн"
#. module: portal_sale
#: field:account.config.settings,group_payment_options:0
msgid "Show payment buttons to employees too"
msgstr "Показывать кнопки оплаты сотрудникам"
#. module: portal_sale
#: model:email.template,subject:portal_sale.email_template_edi_sale
msgid ""
"${object.company_id.name} ${object.state in ('draft', 'sent') and "
"'Quotation' or 'Order'} (Ref ${object.name or 'n/a' })"
msgstr ""
"${object.company_id.name} ${object.state in ('draft', 'sent') and "
"'Предложение' or 'Заказ'} № ${object.name or 'б/н' }"
#. module: portal_sale
#: model:ir.actions.act_window,help:portal_sale.action_quotations_portal
msgid "We haven't sent you any quotation."
msgstr "У Вас нет никаких предложений цен"
#. module: portal_sale
#: model:ir.ui.menu,name:portal_sale.portal_sales_orders
msgid "Sales Orders"
msgstr "Заказы продаж"
#. module: portal_sale
#: model:res.groups,comment:portal_sale.group_payment_options
msgid ""
"Members of this group see the online payment options\n"
"on Sale Orders and Customer Invoices. These options are meant for customers "
"who are accessing\n"
"their documents through the portal."
msgstr ""
"Члены этой группы видят варианты оплаты счетов онлайн\n"
"по заказам продаж и счетам заказчиков. Эти варианты предназначены для "
"заказчиков, имеющих доступ\n"
"к своим документам через портал."
#. module: portal_sale
#: model:email.template,body_html:portal_sale.email_template_edi_sale
msgid ""
"\n"
"<div style=\"font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-"
"serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, "
"255, 255); \">\n"
"\n"
" <p>Hello ${object.partner_id.name},</p>\n"
" \n"
" <p>Here is your ${object.state in ('draft', 'sent') and 'quotation' or "
"'order confirmation'} from ${object.company_id.name}: </p>\n"
"\n"
" <p style=\"border-left: 1px solid #8e0000; margin-left: 30px;\">\n"
" &nbsp;&nbsp;<strong>REFERENCES</strong><br />\n"
" &nbsp;&nbsp;Order number: <strong>${object.name}</strong><br />\n"
" &nbsp;&nbsp;Order total: <strong>${object.amount_total} "
"${object.pricelist_id.currency_id.name}</strong><br />\n"
" &nbsp;&nbsp;Order date: ${object.date_order}<br />\n"
" % if object.origin:\n"
" &nbsp;&nbsp;Order reference: ${object.origin}<br />\n"
" % endif\n"
" % if object.client_order_ref:\n"
" &nbsp;&nbsp;Your reference: ${object.client_order_ref}<br />\n"
" % endif\n"
" % if object.user_id:\n"
" &nbsp;&nbsp;Your contact: <a href=\"mailto:${object.user_id.email or "
"''}?subject=Order%20${object.name}\">${object.user_id.name}</a>\n"
" % endif\n"
" </p>\n"
"\n"
" <% set signup_url = object.get_signup_url() %>\n"
" % if signup_url:\n"
" <p>\n"
" You can access this document and pay online via our Customer Portal:\n"
" </p>\n"
" <a style=\"display:block; width: 150px; height:20px; margin-left: "
"120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-"
"serif; font-size: 13px; font-weight: bold; text-align: center; text-"
"decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; "
"background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat "
"no-repeat;\"\n"
" href=\"${signup_url}\">View ${object.state in ('draft', 'sent') "
"and 'Quotation' or 'Order'}</a>\n"
" % endif\n"
"\n"
" % if object.paypal_url:\n"
" <br/>\n"
" <p>It is also possible to directly pay with Paypal:</p>\n"
" <a style=\"margin-left: 120px;\" href=\"${object.paypal_url}\">\n"
" <img class=\"oe_edi_paypal_button\" "
"src=\"https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif\"/>\n"
" </a>\n"
" % endif\n"
"\n"
" <br/>\n"
" <p>If you have any question, do not hesitate to contact us.</p>\n"
" <p>Thank you for choosing ${object.company_id.name or 'us'}!</p>\n"
" <br/>\n"
" <br/>\n"
" <div style=\"width: 375px; margin: 0px; padding: 0px; background-color: "
"#8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; "
"background-repeat: repeat no-repeat;\">\n"
" <h3 style=\"margin: 0px; padding: 2px 14px; font-size: 12px; color: "
"#DDD;\">\n"
" <strong style=\"text-"
"transform:uppercase;\">${object.company_id.name}</strong></h3>\n"
" </div>\n"
" <div style=\"width: 347px; margin: 0px; padding: 5px 14px; line-height: "
"16px; background-color: #F2F2F2;\">\n"
" <span style=\"color: #222; margin-bottom: 5px; display: block; \">\n"
" % if object.company_id.street:\n"
" ${object.company_id.street}<br/>\n"
" % endif\n"
" % if object.company_id.street2:\n"
" ${object.company_id.street2}<br/>\n"
" % endif\n"
" % if object.company_id.city or object.company_id.zip:\n"
" ${object.company_id.zip} ${object.company_id.city}<br/>\n"
" % endif\n"
" % if object.company_id.country_id:\n"
" ${object.company_id.state_id and ('%s, ' % "
"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name "
"or ''}<br/>\n"
" % endif\n"
" </span>\n"
" % if object.company_id.phone:\n"
" <div style=\"margin-top: 0px; margin-right: 0px; margin-bottom: "
"0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: "
"0px; padding-left: 0px; \">\n"
" Phone:&nbsp; ${object.company_id.phone}\n"
" </div>\n"
" % endif\n"
" % if object.company_id.website:\n"
" <div>\n"
" Web :&nbsp;<a "
"href=\"${object.company_id.website}\">${object.company_id.website}</a>\n"
" </div>\n"
" % endif\n"
" <p></p>\n"
" </div>\n"
"</div>\n"
" "
msgstr ""
"\n"
"<div style=\"font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-"
"serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, "
"255, 255); \">\n"
"\n"
" <p>Здравствуйте, ${object.partner_id.name}!</p>\n"
" \n"
" <p>Направляем Вам ${object.state in ('draft', 'sent') and 'предложение' "
"or 'подтверждение заказа'} от ${object.company_id.name}: </p>\n"
"\n"
" <p style=\"border-left: 1px solid #8e0000; margin-left: 30px;\">\n"
" &nbsp;&nbsp;<strong>СВОДКА</strong><br />\n"
" &nbsp;&nbsp;Заказ: <strong>${object.name}</strong><br />\n"
" &nbsp;&nbsp;Итог: <strong>${object.amount_total} "
"${object.pricelist_id.currency_id.name}</strong><br />\n"
" &nbsp;&nbsp;Дата: ${object.date_order}<br />\n"
" % if object.origin:\n"
" &nbsp;&nbsp;Основание заказа: ${object.origin}<br />\n"
" % endif\n"
" % if object.client_order_ref:\n"
" &nbsp;&nbsp;Ваша ссылка: ${object.client_order_ref}<br />\n"
" % endif\n"
" % if object.user_id:\n"
" &nbsp;&nbsp;Ваш контакт: <a href=\"mailto:${object.user_id.email or "
"''}?subject=Order%20${object.name}\">${object.user_id.name}</a>\n"
" % endif\n"
" </p>\n"
"\n"
" <% set signup_url = object.get_signup_url() %>\n"
" % if signup_url:\n"
" <p>\n"
" Вы можете ознакомиться с этим документом онлайн и узнать о вариантах "
"оплаты через наш Портал:\n"
" </p>\n"
" <a style=\"display:block; width: 150px; height:20px; margin-left: "
"120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-"
"serif; font-size: 13px; font-weight: bold; text-align: center; text-"
"decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; "
"background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat "
"no-repeat;\"\n"
" href=\"${signup_url}\">Посмотреть ${object.state in ('draft', "
"'sent') and 'Предложение' or 'Заказ'}</a>\n"
" % endif\n"
"\n"
" % if object.paypal_url:\n"
" <br/>\n"
" <p>Также возможно произвести оплату непосредственно через Paypal:</p>\n"
" <a style=\"margin-left: 120px;\" href=\"${object.paypal_url}\">\n"
" <img class=\"oe_edi_paypal_button\" "
"src=\"https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif\"/>\n"
" </a>\n"
" % endif\n"
"\n"
" <br/>\n"
" <p>Если у Вас есть вопросы - пожалуйста, свяжитесь с нами.</p>\n"
" <p>Спасибо, что выбрали ${object.company_id.name or 'нас'}!</p>\n"
" <br/>\n"
" <br/>\n"
" <div style=\"width: 375px; margin: 0px; padding: 0px; background-color: "
"#8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; "
"background-repeat: repeat no-repeat;\">\n"
" <h3 style=\"margin: 0px; padding: 2px 14px; font-size: 12px; color: "
"#DDD;\">\n"
" <strong style=\"text-"
"transform:uppercase;\">${object.company_id.name}</strong></h3>\n"
" </div>\n"
" <div style=\"width: 347px; margin: 0px; padding: 5px 14px; line-height: "
"16px; background-color: #F2F2F2;\">\n"
" <span style=\"color: #222; margin-bottom: 5px; display: block; \">\n"
" % if object.company_id.street:\n"
" ${object.company_id.street}<br/>\n"
" % endif\n"
" % if object.company_id.street2:\n"
" ${object.company_id.street2}<br/>\n"
" % endif\n"
" % if object.company_id.city or object.company_id.zip:\n"
" ${object.company_id.zip} ${object.company_id.city}<br/>\n"
" % endif\n"
" % if object.company_id.country_id:\n"
" ${object.company_id.state_id and ('%s, ' % "
"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name "
"or ''}<br/>\n"
" % endif\n"
" </span>\n"
" % if object.company_id.phone:\n"
" <div style=\"margin-top: 0px; margin-right: 0px; margin-bottom: "
"0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: "
"0px; padding-left: 0px; \">\n"
" Phone:&nbsp; ${object.company_id.phone}\n"
" </div>\n"
" % endif\n"
" % if object.company_id.website:\n"
" <div>\n"
" Сайт :&nbsp;<a "
"href=\"${object.company_id.website}\">${object.company_id.website}</a>\n"
" </div>\n"
" % endif\n"
" <p></p>\n"
" </div>\n"
"</div>\n"
" "
#. module: portal_sale
#: model:email.template,report_name:portal_sale.email_template_edi_invoice
msgid ""
"Invoice_${(object.number or '').replace('/','_')}_${object.state == 'draft' "
"and 'draft' or ''}"
msgstr ""
"Счёт_${(object.number or '').replace('/','_')}_${object.state == 'draft' and "
"'проект' or ''}"
#. module: portal_sale
#: model:email.template,subject:portal_sale.email_template_edi_invoice
msgid "${object.company_id.name} Invoice (Ref ${object.number or 'n/a' })"
msgstr "${object.company_id.name} Счёт № ${object.number or 'n/a' }"
#. module: portal_sale
#: model:ir.model,name:portal_sale.model_mail_mail
msgid "Outgoing Mails"
msgstr "Исходящая почта"
#. module: portal_sale
#: model:ir.actions.act_window,name:portal_sale.action_quotations_portal
#: model:ir.ui.menu,name:portal_sale.portal_quotations
msgid "Quotations"
msgstr "Предложения цен"
#. module: portal_sale
#: model:ir.model,name:portal_sale.model_sale_order
msgid "Sales Order"
msgstr "Заказ продаж"
#. module: portal_sale
#: field:account.invoice,portal_payment_options:0
#: field:sale.order,portal_payment_options:0
msgid "Portal Payment Options"
msgstr "Варианты оплаты через портал"
#. module: portal_sale
#: help:account.config.settings,group_payment_options:0
msgid ""
"Show online payment options on Sale Orders and Customer Invoices to "
"employees. If not checked, these options are only visible to portal users."
msgstr ""
"Показывать варианты оплаты по заказам продаж и счетам заказчиков "
"сотрудникам. Если не отмечено, то эти варианты видны только пользователям "
"портала."
#. module: portal_sale
#: model:ir.actions.act_window,name:portal_sale.portal_action_invoices
#: model:ir.ui.menu,name:portal_sale.portal_invoices
msgid "Invoices"
msgstr "Счета"
#. module: portal_sale
#: view:account.config.settings:0
msgid "Configure payment acquiring methods"
msgstr "Настроить методы получения платежей"
#. module: portal_sale
#: model:email.template,body_html:portal_sale.email_template_edi_invoice
msgid ""
"\n"
"<div style=\"font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-"
"serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, "
"255, 255); \">\n"
"\n"
" <p>Hello ${object.partner_id.name},</p>\n"
"\n"
" <p>A new invoice is available for you: </p>\n"
" \n"
" <p style=\"border-left: 1px solid #8e0000; margin-left: 30px;\">\n"
" &nbsp;&nbsp;<strong>REFERENCES</strong><br />\n"
" &nbsp;&nbsp;Invoice number: <strong>${object.number}</strong><br />\n"
" &nbsp;&nbsp;Invoice total: <strong>${object.amount_total} "
"${object.currency_id.name}</strong><br />\n"
" &nbsp;&nbsp;Invoice date: ${object.date_invoice}<br />\n"
" % if object.origin:\n"
" &nbsp;&nbsp;Order reference: ${object.origin}<br />\n"
" % endif\n"
" % if object.user_id:\n"
" &nbsp;&nbsp;Your contact: <a href=\"mailto:${object.user_id.email or "
"''}?subject=Invoice%20${object.number}\">${object.user_id.name}</a>\n"
" % endif\n"
" </p> \n"
"\n"
" <% set signup_url = object.get_signup_url() %>\n"
" % if signup_url:\n"
" <p>\n"
" You can access the invoice document and pay online via our Customer "
"Portal:\n"
" </p>\n"
" <a style=\"display:block; width: 150px; height:20px; margin-left: "
"120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-"
"serif; font-size: 13px; font-weight: bold; text-align: center; text-"
"decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; "
"background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat "
"no-repeat;\"\n"
" href=\"${signup_url}\">View Invoice</a>\n"
" % endif\n"
" \n"
" % if object.paypal_url:\n"
" <br/>\n"
" <p>It is also possible to directly pay with Paypal:</p>\n"
" <a style=\"margin-left: 120px;\" href=\"${object.paypal_url}\">\n"
" <img class=\"oe_edi_paypal_button\" "
"src=\"https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif\"/>\n"
" </a>\n"
" % endif\n"
" \n"
" <br/>\n"
" <p>If you have any question, do not hesitate to contact us.</p>\n"
" <p>Thank you for choosing ${object.company_id.name or 'us'}!</p>\n"
" <br/>\n"
" <br/>\n"
" <div style=\"width: 375px; margin: 0px; padding: 0px; background-color: "
"#8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; "
"background-repeat: repeat no-repeat;\">\n"
" <h3 style=\"margin: 0px; padding: 2px 14px; font-size: 12px; color: "
"#DDD;\">\n"
" <strong style=\"text-"
"transform:uppercase;\">${object.company_id.name}</strong></h3>\n"
" </div>\n"
" <div style=\"width: 347px; margin: 0px; padding: 5px 14px; line-height: "
"16px; background-color: #F2F2F2;\">\n"
" <span style=\"color: #222; margin-bottom: 5px; display: block; \">\n"
" % if object.company_id.street:\n"
" ${object.company_id.street}<br/>\n"
" % endif\n"
" % if object.company_id.street2:\n"
" ${object.company_id.street2}<br/>\n"
" % endif\n"
" % if object.company_id.city or object.company_id.zip:\n"
" ${object.company_id.zip} ${object.company_id.city}<br/>\n"
" % endif\n"
" % if object.company_id.country_id:\n"
" ${object.company_id.state_id and ('%s, ' % "
"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name "
"or ''}<br/>\n"
" % endif\n"
" </span>\n"
" % if object.company_id.phone:\n"
" <div style=\"margin-top: 0px; margin-right: 0px; margin-bottom: "
"0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: "
"0px; padding-left: 0px; \">\n"
" Phone:&nbsp; ${object.company_id.phone}\n"
" </div>\n"
" % endif\n"
" % if object.company_id.website:\n"
" <div>\n"
" Web :&nbsp;<a "
"href=\"${object.company_id.website}\">${object.company_id.website}</a>\n"
" </div>\n"
" % endif\n"
" <p></p>\n"
" </div>\n"
"</div>\n"
" "
msgstr ""
"\n"
"<div style=\"font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-"
"serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, "
"255, 255); \">\n"
"\n"
" <p>Здравствуйте, ${object.partner_id.name},</p>\n"
"\n"
" <p>Вам выставлен счёт: </p>\n"
" \n"
" <p style=\"border-left: 1px solid #8e0000; margin-left: 30px;\">\n"
" &nbsp;&nbsp;<strong>СВОДКА</strong><br />\n"
" &nbsp;&nbsp;Счёт: <strong>${object.number}</strong><br />\n"
" &nbsp;&nbsp;Итог: <strong>${object.amount_total} "
"${object.currency_id.name}</strong><br />\n"
" &nbsp;&nbsp;Дата: ${object.date_invoice}<br />\n"
" % if object.origin:\n"
" &nbsp;&nbsp;Основание: ${object.origin}<br />\n"
" % endif\n"
" % if object.user_id:\n"
" &nbsp;&nbsp;Ваш контакт: <a href=\"mailto:${object.user_id.email or "
"''}?subject=Счёт%20${object.number}\">${object.user_id.name}</a>\n"
" % endif\n"
" </p> \n"
"\n"
" <% set signup_url = object.get_signup_url() %>\n"
" % if signup_url:\n"
" <p>\n"
" Вы можете ознакомиться с документом онлайн и узнать о вариантах оплаты "
"через наш Портал:\n"
" </p>\n"
" <a style=\"display:block; width: 150px; height:20px; margin-left: "
"120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-"
"serif; font-size: 13px; font-weight: bold; text-align: center; text-"
"decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; "
"background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat "
"no-repeat;\"\n"
" href=\"${signup_url}\">Посмотреть счёт</a>\n"
" % endif\n"
" \n"
" % if object.paypal_url:\n"
" <br/>\n"
" <p>Также возможно произвести оплату через Paypal:</p>\n"
" <a style=\"margin-left: 120px;\" href=\"${object.paypal_url}\">\n"
" <img class=\"oe_edi_paypal_button\" "
"src=\"https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif\"/>\n"
" </a>\n"
" % endif\n"
" \n"
" <br/>\n"
" <p>Если у Вас есть вопросы - пожалуйста, свяжитесь с нами.</p>\n"
" <p>Спасибо, что выбрали ${object.company_id.name or 'нас'}!</p>\n"
" <br/>\n"
" <br/>\n"
" <div style=\"width: 375px; margin: 0px; padding: 0px; background-color: "
"#8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; "
"background-repeat: repeat no-repeat;\">\n"
" <h3 style=\"margin: 0px; padding: 2px 14px; font-size: 12px; color: "
"#DDD;\">\n"
" <strong style=\"text-"
"transform:uppercase;\">${object.company_id.name}</strong></h3>\n"
" </div>\n"
" <div style=\"width: 347px; margin: 0px; padding: 5px 14px; line-height: "
"16px; background-color: #F2F2F2;\">\n"
" <span style=\"color: #222; margin-bottom: 5px; display: block; \">\n"
" % if object.company_id.street:\n"
" ${object.company_id.street}<br/>\n"
" % endif\n"
" % if object.company_id.street2:\n"
" ${object.company_id.street2}<br/>\n"
" % endif\n"
" % if object.company_id.city or object.company_id.zip:\n"
" ${object.company_id.zip} ${object.company_id.city}<br/>\n"
" % endif\n"
" % if object.company_id.country_id:\n"
" ${object.company_id.state_id and ('%s, ' % "
"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name "
"or ''}<br/>\n"
" % endif\n"
" </span>\n"
" % if object.company_id.phone:\n"
" <div style=\"margin-top: 0px; margin-right: 0px; margin-bottom: "
"0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: "
"0px; padding-left: 0px; \">\n"
" Phone:&nbsp; ${object.company_id.phone}\n"
" </div>\n"
" % endif\n"
" % if object.company_id.website:\n"
" <div>\n"
" Сайт:&nbsp;<a "
"href=\"${object.company_id.website}\">${object.company_id.website}</a>\n"
" </div>\n"
" % endif\n"
" <p></p>\n"
" </div>\n"
"</div>\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 "Заказы продаж"

View File

@ -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."),

View File

@ -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()

View File

@ -14,9 +14,6 @@
<menuitem id="menu_purchase_config_purchase" name="Configuration"
groups="group_purchase_manager"
parent="base.menu_purchase_root" sequence="100"/>
<menuitem
action="product.product_pricelist_action_for_purchase" id="menu_product_pricelist_action2_purchase"
parent="menu_purchase_config_purchase" sequence="10" groups="product.group_purchase_pricelist" />
<record id="purchase_pricelist_version_action" model="ir.actions.act_window">
<field name="name">Pricelist Versions</field>
@ -40,10 +37,13 @@
id="menu_purchase_config_pricelist" name="Pricelists"
parent="menu_purchase_config_purchase" sequence="50" groups="product.group_purchase_pricelist"/>
<menuitem
action="product.product_pricelist_action_for_purchase" id="menu_product_pricelist_action2_purchase"
parent="menu_purchase_config_pricelist" sequence="1" groups="product.group_purchase_pricelist" />
<menuitem
action="purchase_pricelist_version_action" id="menu_purchase_pricelist_version_action"
parent="menu_purchase_config_pricelist" sequence="2" groups="product.group_purchase_pricelist"/>
<menuitem
action="product.product_price_type_action" id="menu_product_pricelist_action2_purchase_type"
@ -73,7 +73,7 @@
parent="menu_purchase_config_purchase"/>
<menuitem
action="base.action_partner_category_form" id="menu_partner_categories_in_form" name="Partner Categories"
action="base.action_partner_category_form" id="menu_partner_categories_in_form" name="Partner Tags"
parent="purchase.menu_purchase_partner_cat" groups="base.group_no_one"/>
<!--Supplier menu-->

View File

@ -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."),

View File

@ -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):

View File

@ -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',

View File

@ -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."),
}