[MRG] merge with lp:openobject-addons

bzr revid: tpa@tinyerp.com-20131009093540-y9ueph05ah04gixg
This commit is contained in:
Turkesh Patel (Open ERP) 2013-10-09 15:05:40 +05:30
commit 40f189adfb
205 changed files with 5653 additions and 2332 deletions

View File

@ -1416,14 +1416,17 @@ class account_move(osv.osv):
l[2]['period_id'] = default_period
context['period_id'] = default_period
if 'line_id' in vals:
if vals.get('line_id', False):
c = context.copy()
c['novalidate'] = True
c['period_id'] = vals['period_id'] if 'period_id' in vals else self._get_period(cr, uid, context)
c['journal_id'] = vals['journal_id']
if 'date' in vals: c['date'] = vals['date']
result = super(account_move, self).create(cr, uid, vals, c)
self.validate(cr, uid, [result], context)
tmp = self.validate(cr, uid, [result], context)
journal = self.pool.get('account.journal').browse(cr, uid, vals['journal_id'], context)
if journal.entry_posted and tmp:
self.button_validate(cr,uid, [result], context)
else:
result = super(account_move, self).create(cr, uid, vals, context)
return result

View File

@ -349,6 +349,7 @@
<page string="Invoice Lines">
<field name="invoice_line" nolabel="1" widget="one2many_list" context="{'type': type}">
<tree string="Invoice Lines" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="product_id"
on_change="product_id_change(product_id, uos_id, quantity, name, parent.type, parent.partner_id, parent.fiscal_position, price_unit, parent.currency_id, context, parent.company_id)"/>
<field name="name"/>

View File

@ -1283,7 +1283,7 @@ class account_move_line(osv.osv):
self.create(cr, uid, data, context)
del vals['account_tax_id']
if check and ((not context.get('no_store_function')) or journal.entry_posted):
if check and not context.get('novalidate') and ((not context.get('no_store_function')) or journal.entry_posted):
tmp = move_obj.validate(cr, uid, [vals['move_id']], context)
if journal.entry_posted and tmp:
move_obj.button_validate(cr,uid, [vals['move_id']], context)

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

@ -1,7 +1,7 @@
-
Create a user as 'Accountant'
-
!record {model: res.users, id: res_users_account_user}:
!record {model: res.users, id: res_users_account_user, view: False}:
company_id: base.main_company
name: Accountant
login: acc
@ -17,7 +17,7 @@
-
Create a user as 'Financial Manager'
-
!record {model: res.users, id: res_users_account_manager}:
!record {model: res.users, id: res_users_account_manager, view: False}:
company_id: base.main_company
name: Financial Manager
login: fm

View File

@ -15,7 +15,7 @@
<newline/>
</xpath>
<xpath expr="//field[@name='filter']" position="replace">
<field name="filter" on_change="onchange_filter(filter, fiscalyear_id)" colspan="4"/>
<field name="filter" on_change="onchange_filter(filter, fiscalyear_id)"/>
<field name="initial_balance" attrs="{'readonly':[('filter', 'in', ('filter_no', 'unreconciled'))]}" />
</xpath>
</data>

View File

@ -0,0 +1,23 @@
# Spanish (Argentina) 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:04+0000\n"
"PO-Revision-Date: 2013-10-07 21:16+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Spanish (Argentina) <es_AR@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-10-08 05:42+0000\n"
"X-Generator: Launchpad (build 16799)\n"
#. module: account_accountant
#: model:ir.actions.client,name:account_accountant.action_client_account_menu
msgid "Open Accounting Menu"
msgstr ""

View File

@ -655,7 +655,7 @@ class account_analytic_account(osv.osv):
if not contract.partner_id:
raise osv.except_osv(_('No Customer Defined!'),_("You must first select a Customer for Contract %s!") % contract.name )
fpos = contract.partner_id.property_account_position.id or False
fpos = contract.partner_id.property_account_position or False
journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', contract.company_id.id or False)], limit=1)
if not journal_ids:
raise osv.except_osv(_('Error!'),
@ -673,7 +673,7 @@ class account_analytic_account(osv.osv):
'journal_id': len(journal_ids) and journal_ids[0] or False,
'date_invoice': contract.recurring_next_date,
'origin': contract.name,
'fiscal_position': fpos,
'fiscal_position': fpos and fpos.id,
'payment_term': partner_payment_term,
'company_id': contract.company_id.id or False,
}
@ -687,7 +687,7 @@ class account_analytic_account(osv.osv):
account_id = res.categ_id.property_account_income_categ.id
account_id = fpos_obj.map_account(cr, uid, fpos, account_id)
taxes = res.taxes_id and res.taxes_id or False
taxes = res.taxes_id or False
tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
invoice_line_vals = {

View File

@ -8,19 +8,19 @@ msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-12-21 17:05+0000\n"
"PO-Revision-Date: 2011-11-08 10:42+0000\n"
"Last-Translator: OpenERP Danmark / Mikhael Saxtorph <Unknown>\n"
"PO-Revision-Date: 2013-09-26 21:19+0000\n"
"Last-Translator: Morten Schou <ms@msteknik.dk>\n"
"Language-Team: Danish <da@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-12 06:17+0000\n"
"X-Generator: Launchpad (build 16761)\n"
"X-Launchpad-Export-Date: 2013-09-27 05:49+0000\n"
"X-Generator: Launchpad (build 16774)\n"
#. module: account_cancel
#: view:account.invoice:0
msgid "Cancel"
msgstr ""
msgstr "Annuller"
#~ msgid "Account Cancel"
#~ msgstr "Annuller post"

View File

@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-12-21 17:05+0000\n"
"PO-Revision-Date: 2013-03-21 19:22+0000\n"
"Last-Translator: Liuming <gumdam20@me.com>\n"
"PO-Revision-Date: 2013-09-24 02:46+0000\n"
"Last-Translator: DWXXX <Unknown>\n"
"Language-Team: Chinese (Simplified) <zh_CN@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-12 06:40+0000\n"
"X-Generator: Launchpad (build 16761)\n"
"X-Launchpad-Export-Date: 2013-09-25 05:28+0000\n"
"X-Generator: Launchpad (build 16771)\n"
#. module: account_test
#: view:accounting.assert.test:0
@ -106,7 +106,7 @@ msgstr ""
#. module: account_test
#: view:accounting.assert.test:0
msgid "Description"
msgstr ""
msgstr "描述"
#. module: account_test
#: model:accounting.assert.test,desc:account_test.account_test_06_1
@ -123,13 +123,13 @@ msgstr ""
#: model:ir.actions.report.xml,name:account_test.account_assert_test_report
#: model:ir.ui.menu,name:account_test.menu_action_license
msgid "Accounting Tests"
msgstr ""
msgstr "帐户测试"
#. module: account_test
#: code:addons/account_test/report/account_test_report.py:74
#, python-format
msgid "The test was passed successfully"
msgstr ""
msgstr "测试通过"
#. module: account_test
#: field:accounting.assert.test,active:0
@ -155,7 +155,7 @@ msgstr ""
#. module: account_test
#: field:accounting.assert.test,code_exec:0
msgid "Python code"
msgstr ""
msgstr "Python代码"
#. module: account_test
#: model:accounting.assert.test,desc:account_test.account_test_07
@ -209,7 +209,7 @@ msgstr ""
#. module: account_test
#: view:accounting.assert.test:0
msgid "Python Code"
msgstr ""
msgstr "Python代码"
#. module: account_test
#: model:ir.actions.act_window,help:account_test.action_accounting_assert

View File

@ -1,7 +1,7 @@
-
Create a user as 'Accountant for account voucher'
-
!record {model: res.users, id: res_users_account_voucher_user}:
!record {model: res.users, id: res_users_account_voucher_user, view: False}:
company_id: base.main_company
name: Voucher Accountant
login: vacc
@ -17,7 +17,7 @@
-
Create a user as 'Financial Manager for account voucher'
-
!record {model: res.users, id: res_users_account_voucher_manager}:
!record {model: res.users, id: res_users_account_voucher_manager, view: False}:
company_id: base.main_company
name: Financial Manager for voucher
login: fmv

View File

@ -0,0 +1,279 @@
# Chinese (Traditional) 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:05+0000\n"
"PO-Revision-Date: 2013-09-19 09:40+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Chinese (Traditional) <zh_TW@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-20 05:38+0000\n"
"X-Generator: Launchpad (build 16765)\n"
#. module: auth_signup
#: field:res.partner,signup_type:0
msgid "Signup Token Type"
msgstr ""
#. module: auth_signup
#: field:base.config.settings,auth_signup_uninvited:0
msgid "Allow external users to sign up"
msgstr "允許外部使用者註冊"
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:19
#, python-format
msgid "Confirm Password"
msgstr "確認密碼"
#. module: auth_signup
#: help:base.config.settings,auth_signup_uninvited:0
msgid "If unchecked, only invited users may sign up."
msgstr "如果未勾選,只有被邀請的使用者可以註冊"
#. module: auth_signup
#: model:ir.model,name:auth_signup.model_base_config_settings
msgid "base.config.settings"
msgstr ""
#. module: auth_signup
#: code:addons/auth_signup/res_users.py:266
#, python-format
msgid "Cannot send email: user has no email address."
msgstr "無法送出email使用者沒有email 地址"
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:27
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:31
#, python-format
msgid "Reset password"
msgstr "重設密碼"
#. module: auth_signup
#: field:base.config.settings,auth_signup_template_user_id:0
msgid "Template user for new users created through signup"
msgstr ""
#. module: auth_signup
#: model:email.template,subject:auth_signup.reset_password_email
msgid "Password reset"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/js/auth_signup.js:120
#, python-format
msgid "Please enter a password and confirm it."
msgstr ""
#. module: auth_signup
#: view:res.users:0
msgid "Send an email to the user to (re)set their password."
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:26
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:29
#, python-format
msgid "Sign Up"
msgstr ""
#. module: auth_signup
#: selection:res.users,state:0
msgid "New"
msgstr ""
#. module: auth_signup
#: code:addons/auth_signup/res_users.py:258
#, python-format
msgid "Mail sent to:"
msgstr ""
#. module: auth_signup
#: field:res.users,state:0
msgid "Status"
msgstr ""
#. module: auth_signup
#: model:email.template,body_html:auth_signup.reset_password_email
msgid ""
"\n"
"<p>A password reset was requested for the OpenERP account linked to this "
"email.</p>\n"
"\n"
"<p>You may change your password by following <a "
"href=\"${object.signup_url}\">this link</a>.</p>\n"
"\n"
"<p>Note: If you do not expect this, you can safely ignore this email.</p>"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/js/auth_signup.js:114
#, python-format
msgid "Please enter a name."
msgstr ""
#. module: auth_signup
#: model:ir.model,name:auth_signup.model_res_users
msgid "Users"
msgstr ""
#. module: auth_signup
#: field:res.partner,signup_url:0
msgid "Signup URL"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/js/auth_signup.js:117
#, python-format
msgid "Please enter a username."
msgstr ""
#. module: auth_signup
#: selection:res.users,state:0
msgid "Active"
msgstr ""
#. module: auth_signup
#: code:addons/auth_signup/res_users.py:270
#, python-format
msgid ""
"Cannot send email: no outgoing email server configured.\n"
"You can configure it under Settings/General Settings."
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:12
#, python-format
msgid "Username"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:8
#, python-format
msgid "Name"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/js/auth_signup.js:173
#, python-format
msgid "Please enter a username or email address."
msgstr ""
#. module: auth_signup
#: selection:res.users,state:0
msgid "Resetting Password"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:13
#, python-format
msgid "Username (Email)"
msgstr ""
#. module: auth_signup
#: field:res.partner,signup_expiration:0
msgid "Signup Expiration"
msgstr ""
#. module: auth_signup
#: help:base.config.settings,auth_signup_reset_password:0
msgid "This allows users to trigger a password reset from the Login page."
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:25
#, python-format
msgid "Log in"
msgstr ""
#. module: auth_signup
#: field:res.partner,signup_valid:0
msgid "Signup Token is Valid"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/js/auth_signup.js:111
#: code:addons/auth_signup/static/src/js/auth_signup.js:114
#: code:addons/auth_signup/static/src/js/auth_signup.js:117
#: code:addons/auth_signup/static/src/js/auth_signup.js:120
#: code:addons/auth_signup/static/src/js/auth_signup.js:123
#: code:addons/auth_signup/static/src/js/auth_signup.js:170
#: code:addons/auth_signup/static/src/js/auth_signup.js:173
#, python-format
msgid "Login"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/js/auth_signup.js:97
#, python-format
msgid "Invalid signup token"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/js/auth_signup.js:123
#, python-format
msgid "Passwords do not match; please retype them."
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/js/auth_signup.js:111
#: code:addons/auth_signup/static/src/js/auth_signup.js:170
#, python-format
msgid "No database selected !"
msgstr ""
#. module: auth_signup
#: view:res.users:0
msgid "Reset Password"
msgstr ""
#. module: auth_signup
#: field:base.config.settings,auth_signup_reset_password:0
msgid "Enable password reset from Login page"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:30
#, python-format
msgid "Back to Login"
msgstr ""
#. module: auth_signup
#. openerp-web
#: code:addons/auth_signup/static/src/xml/auth_signup.xml:22
#, python-format
msgid "Sign up"
msgstr ""
#. module: auth_signup
#: model:ir.model,name:auth_signup.model_res_partner
msgid "Partner"
msgstr ""
#. module: auth_signup
#: field:res.partner,signup_token:0
msgid "Signup Token"
msgstr ""

View File

@ -114,19 +114,6 @@ def real_id2base_calendar_id(real_id, recurrent_date):
return '%d-%s' % (real_id, recurrent_date)
return real_id
def _links_get(self, cr, uid, context=None):
"""
Get request link.
@param cr: the current row, from the database cursor
@param uid: the current user's ID for security checks
@param context: a standard dictionary for contextual values
@return: list of dictionary which contain object and name and id
"""
obj = self.pool.get('res.request.link')
ids = obj.search(cr, uid, [])
res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
return [(r['object'], r['name']) for r in res]
html_invitation = """
<html>
<head>
@ -307,19 +294,6 @@ class calendar_attendee(osv.osv):
return result
def _links_get(self, cr, uid, context=None):
"""
Get request link for ref field in calendar attendee.
@param cr: the current row, from the database cursor
@param uid: the current user's id for security checks
@param context: A standard dictionary for contextual values
@return: list of dictionary which contain object and name and id
"""
obj = self.pool.get('res.request.link')
ids = obj.search(cr, uid, [])
res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
return [(r['object'], r['name']) for r in res]
def _lang_get(self, cr, uid, context=None):
"""
Get language for language selection field.
@ -385,7 +359,7 @@ property or property parameter."),
'event_end_date': fields.function(_compute_data, \
string='Event End Date', type="datetime", \
multi='event_end_date'),
'ref': fields.reference('Event Ref', selection=_links_get, size=128),
'ref': fields.reference('Event Ref', selection=openerp.addons.base.res.res_request.referencable_models, size=128),
'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
}
_defaults = {
@ -1208,20 +1182,44 @@ rule or repeating pattern of time to exclude from the recurring rule."),
new_rrule_str = ';'.join(new_rrule_str)
rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
for r_date in rdates:
ok = True
# fix domain evaluation
# step 1: check date and replace expression by True or False, replace other expressions by True
# step 2: evaluation of & and |
# check if there are one False
pile = []
for arg in domain:
if arg[0] in ('date', 'date_deadline'):
if (arg[1]=='='):
ok = ok and r_date.strftime('%Y-%m-%d')==arg[2]
if (arg[1]=='>'):
ok = ok and r_date.strftime('%Y-%m-%d')>arg[2]
if (arg[1]=='<'):
ok = ok and r_date.strftime('%Y-%m-%d')<arg[2]
if (arg[1]=='>='):
ok = ok and r_date.strftime('%Y-%m-%d')>=arg[2]
if (arg[1]=='<='):
ok = ok and r_date.strftime('%Y-%m-%d')<=arg[2]
if not ok:
if str(arg[0]) in (str('date'), str('date_deadline')):
if (arg[1] == '='):
ok = r_date.strftime('%Y-%m-%d')==arg[2]
if (arg[1] == '>'):
ok = r_date.strftime('%Y-%m-%d')>arg[2]
if (arg[1] == '<'):
ok = r_date.strftime('%Y-%m-%d')<arg[2]
if (arg[1] == '>='):
ok = r_date.strftime('%Y-%m-%d')>=arg[2]
if (arg[1] == '<='):
ok = r_date.strftime('%Y-%m-%d')<=arg[2]
pile.append(ok)
elif str(arg) == str('&') or str(arg) == str('|'):
pile.append(arg)
else:
pile.append(True)
pile.reverse()
new_pile = []
for item in pile:
if not isinstance(item, basestring):
res = item
elif str(item) == str('&'):
first = new_pile.pop()
second = new_pile.pop()
res = first and second
elif str(item) == str('|'):
first = new_pile.pop()
second = new_pile.pop()
res = first or second
new_pile.append(res)
if [True for item in new_pile if not item]:
continue
idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
result.append(idval)
@ -1346,18 +1344,17 @@ rule or repeating pattern of time to exclude from the recurring rule."),
for arg in args:
new_arg = arg
if arg[0] in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
if arg[0] in ('date_deadline', unicode('date_deadline')):
if context.get('virtual_id', True):
new_args += ['|','&',('recurrency','=',1),('recurrent_id_date', arg[1], arg[2])]
new_args += ['|','&',('recurrency','=',1),('end_date', arg[1], arg[2])]
elif arg[0] == "id":
new_id = get_real_ids(arg[2])
new_arg = (arg[0], arg[1], new_id)
new_args.append(new_arg)
#offset, limit and count must be treated separately as we may need to deal with virtual ids
res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=order, context=context, count=False)
if context.get('virtual_id', True):
res = self.get_recurrent_ids(cr, uid, res, new_args, limit, context=context)
res = self.get_recurrent_ids(cr, uid, res, args, limit, context=context)
if count:
return len(res)
elif limit:
@ -1436,6 +1433,14 @@ rule or repeating pattern of time to exclude from the recurring rule."),
vals['vtimezone'] = vals['vtimezone'][40:]
res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
# set end_date for calendar searching
if vals.get('recurrency', True) and vals.get('end_type', 'count') in ('count', unicode('count')) and \
(vals.get('rrule_type') or vals.get('count') or vals.get('date') or vals.get('date_deadline')):
for data in self.read(cr, uid, ids, ['date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context):
end_date = self._set_recurrency_end_date(data, context=context)
super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context)
if vals.get('partner_ids', False):
self.create_attendees(cr, uid, ids, context)
@ -1554,6 +1559,21 @@ rule or repeating pattern of time to exclude from the recurring rule."),
self.unlink_events(cr, uid, ids, context=context)
return res
def _set_recurrency_end_date(self, data, context=None):
end_date = data.get('end_date')
if data.get('recurrency') and data.get('end_type') in ('count', unicode('count')):
data_date_deadline = datetime.strptime(data.get('date_deadline'), '%Y-%m-%d %H:%M:%S')
if data.get('rrule_type') in ('daily', unicode('count')):
rel_date = relativedelta(days=data.get('count')+1)
elif data.get('rrule_type') in ('weekly', unicode('weekly')):
rel_date = relativedelta(days=(data.get('count')+1)*7)
elif data.get('rrule_type') in ('monthly', unicode('monthly')):
rel_date = relativedelta(months=data.get('count')+1)
elif data.get('rrule_type') in ('yearly', unicode('yearly')):
rel_date = relativedelta(years=data.get('count')+1)
end_date = data_date_deadline + rel_date
return end_date
def create(self, cr, uid, vals, context=None):
if context is None:
context = {}
@ -1561,7 +1581,9 @@ rule or repeating pattern of time to exclude from the recurring rule."),
if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
vals['vtimezone'] = vals['vtimezone'][40:]
vals['end_date'] = self._set_recurrency_end_date(vals, context=context)
res = super(calendar_event, self).create(cr, uid, vals, context)
alarm_obj = self.pool.get('res.alarm')
alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
self.create_attendees(cr, uid, [res], context)

View File

@ -20,14 +20,20 @@
##############################################################################
from openerp.osv import fields, osv
import re
from openerp.report.render.rml2pdf import customfonts
class base_config_settings(osv.osv_memory):
_name = 'base.config.settings'
_inherit = 'res.config.settings'
def _get_font(self, cr, uid, context=None):
return sorted(customfonts.RegisterCustomFonts())
_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',
@ -38,8 +44,13 @@ class base_config_settings(osv.osv_memory):
'module_base_import': fields.boolean("Allow users to import data from CSV files"),
'module_google_drive': fields.boolean('Attach Google documents to any record',
help="""This installs the module google_docs."""),
'font': fields.selection(_get_font, "Select Font", help="Set the font into the report header, will be used for every RML report of the user company"),
}
_defaults= {
'font': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.font or 'Helvetica',
}
def open_company(self, cr, uid, ids, context=None):
user = self.pool.get('res.users').browse(cr, uid, uid, context)
return {
@ -52,6 +63,20 @@ class base_config_settings(osv.osv_memory):
'target': 'current',
}
def _change_header(self, header,font):
""" Replace default fontname use in header and setfont tag """
default_para = re.sub('fontName.?=.?".*"', 'fontName="%s"'% font,header)
return re.sub('(<setFont.?name.?=.?)(".*?")(.)', '\g<1>"%s"\g<3>'% font,default_para)
def set_base_defaults(self, cr, uid, ids, context=None):
ir_model_data = self.pool.get('ir.model.data')
wizard = self.browse(cr, uid, ids)[0]
if wizard.font:
user = self.pool.get('res.users').browse(cr, uid, uid, context)
user.company_id.write({'font':wizard.font,'rml_header': self._change_header(user.company_id.rml_header,wizard.font), 'rml_header2': self._change_header(user.company_id.rml_header2,wizard.font), 'rml_header3': self._change_header(user.company_id.rml_header3,wizard.font)})
return {}
# Preferences wizard for Sales & CRM.
# It is defined here because it is inherited independently in modules sale, crm,
# plugin_outlook and plugin_thunderbird.
@ -64,18 +89,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:
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -90,6 +90,14 @@
</div>
</div>
</group>
<group string="Report Settings">
<label for="font" string="RML font (Header/footer)" />
<div>
<div>
<field name="font" class="oe_inline"/>
</div>
</div>
</group>
</form>
</field>
</record>
@ -123,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

@ -19,6 +19,7 @@
#
##############################################################################
import calendar
from datetime import date, datetime
from dateutil import relativedelta
@ -117,9 +118,9 @@ class crm_case_section(osv.osv):
"""
month_begin = date.today().replace(day=1)
section_result = [{
'value': 0,
'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'),
} for i in range(self._period_number - 1, -1, -1)]
'value': 0,
'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'),
} for i in range(self._period_number - 1, -1, -1)]
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)
@ -135,12 +136,14 @@ class crm_case_section(osv.osv):
obj = self.pool.get('crm.lead')
res = dict.fromkeys(ids, False)
month_begin = date.today().replace(day=1)
groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
date_begin = month_begin - relativedelta.relativedelta(months=self._period_number - 1)
date_end = month_begin.replace(day=calendar.monthrange(month_begin.year, month_begin.month)[1])
date_domain = [('create_date', '>=', date_begin.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)), ('create_date', '<=', date_end.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))]
for id in ids:
res[id] = dict()
lead_domain = [('type', '=', 'lead'), ('section_id', '=', id), ('create_date', '>=', groupby_begin)]
lead_domain = date_domain + [('type', '=', 'lead'), ('section_id', '=', id)]
res[id]['monthly_open_leads'] = self.__get_bar_values(cr, uid, obj, lead_domain, ['create_date'], 'create_date_count', 'create_date', context=context)
opp_domain = [('type', '=', 'opportunity'), ('section_id', '=', id), ('create_date', '>=', groupby_begin)]
opp_domain = date_domain + [('type', '=', 'opportunity'), ('section_id', '=', id)]
res[id]['monthly_planned_revenue'] = self.__get_bar_values(cr, uid, obj, opp_domain, ['planned_revenue', 'create_date'], 'planned_revenue', 'create_date', context=context)
return res
@ -255,13 +258,6 @@ class crm_case_resource_type(osv.osv):
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
}
def _links_get(self, cr, uid, context=None):
"""Gets links value for reference field"""
obj = self.pool.get('res.request.link')
ids = obj.search(cr, uid, [])
res = obj.read(cr, uid, ids, ['object', 'name'], context)
return [(r['object'], r['name']) for r in res]
class crm_payment_mode(osv.osv):
""" Payment Mode for Fund """
_name = "crm.payment.mode"

View File

@ -23,6 +23,7 @@ import crm
from datetime import datetime
from operator import itemgetter
import openerp
from openerp import SUPERUSER_ID
from openerp import tools
from openerp.addons.base.res.res_partner import format_address
@ -253,11 +254,13 @@ 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'),
'ref': fields.reference('Reference', selection=crm._links_get, size=128),
'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
'ref': fields.reference('Reference', selection=openerp.addons.base.res.res_request.referencable_models),
'ref2': fields.reference('Reference 2', selection=openerp.addons.base.res.res_request.referencable_models),
'phone': fields.char("Phone", size=64),
'date_deadline': fields.date('Expected Closing', help="Estimate of the date on which the opportunity will be won."),
'date_action': fields.date('Next Action Date', select=True),
@ -296,7 +299,7 @@ class crm_lead(format_address, osv.osv):
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
'color': 0,
'date_last_stage_update': fields.datetime.now(),
'date_last_stage_update': fields.datetime.now,
}
_sql_constraints = [

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

@ -1,7 +1,7 @@
-
Create a user as 'Crm Salesmanager'
-
!record {model: res.users, id: crm_res_users_salesmanager}:
!record {model: res.users, id: crm_res_users_salesmanager, view: False}:
company_id: base.main_company
name: Crm Sales manager
login: csm
@ -16,7 +16,7 @@
-
Create a user as 'Crm Salesman'
-
!record {model: res.users, id: crm_res_users_salesman}:
!record {model: res.users, id: crm_res_users_salesman, view: False}:
company_id: base.main_company
name: Crm Salesman
login: csu

View File

@ -63,7 +63,7 @@ class crm_lead2opportunity_partner(osv.osv_memory):
for id in ids:
tomerge.add(id)
if email:
ids = lead_obj.search(cr, uid, [('email_from', 'ilike', email[0]), ('probability', '<', '100')])
ids = lead_obj.search(cr, uid, [('email_from', '=ilike', email[0]), ('probability', '<', '100')])
for id in ids:
tomerge.add(id)

View File

@ -19,6 +19,7 @@
#
##############################################################################
import openerp
from openerp.addons.crm import crm
from openerp.osv import fields, osv
from openerp import tools
@ -83,7 +84,7 @@ class crm_claim(osv.osv):
'date_deadline': fields.date('Deadline'),
'date_closed': fields.datetime('Closed', readonly=True),
'date': fields.datetime('Claim Date', select=True),
'ref' : fields.reference('Reference', selection=crm._links_get, size=128),
'ref': fields.reference('Reference', selection=openerp.addons.base.res.res_request.referencable_models),
'categ_id': fields.many2one('crm.case.categ', 'Category', \
domain="[('section_id','=',section_id),\
('object_id.model', '=', 'crm.claim')]"),
@ -108,7 +109,7 @@ class crm_claim(osv.osv):
_defaults = {
'user_id': lambda s, cr, uid, c: uid,
'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
'date': fields.datetime.now(),
'date': fields.datetime.now,
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.case', context=c),
'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
'active': lambda *a: 1,

View File

@ -19,6 +19,7 @@
#
##############################################################################
import openerp
from openerp.addons.crm import crm
from openerp.osv import fields, osv
from openerp import tools
@ -53,8 +54,8 @@ class crm_helpdesk(osv.osv):
'email_cc': fields.text('Watchers Emails', size=252 , help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
'email_from': fields.char('Email', size=128, help="Destination email for email gateway"),
'date': fields.datetime('Date'),
'ref' : fields.reference('Reference', selection=crm._links_get, size=128),
'ref2' : fields.reference('Reference 2', selection=crm._links_get, size=128),
'ref': fields.reference('Reference', selection=openerp.addons.base.res.res_request.referencable_models),
'ref2': fields.reference('Reference 2', selection=openerp.addons.base.res.res_request.referencable_models),
'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
'planned_revenue': fields.float('Planned Revenue'),
'planned_cost': fields.float('Planned Costs'),
@ -80,7 +81,7 @@ class crm_helpdesk(osv.osv):
'active': lambda *a: 1,
'user_id': lambda s, cr, uid, c: uid,
'state': lambda *a: 'draft',
'date': lambda *a: fields.datetime.now(),
'date': fields.datetime.now,
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
}

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

@ -33,48 +33,45 @@ class actions_server(osv.Model):
return res
_columns = {
'email_from': fields.char('From',
help="Sender address; define the template to see its value. If not set, the default "
"value will be the author's email alias if configured, or email address."),
'email_to': fields.char('To (Emails)',
help="Comma-separated recipient addresses; define the template to see its value"),
'partner_to': fields.char('To (Partners)',
help="Comma-separated ids of recipient partners; define the template to see its value"),
'subject': fields.char('Subject',
help="Email subject; define the template to see its value"),
'body_html': fields.text('Body',
help="Rich-text/HTML version of the message; define the template to see its value"),
'template_id': fields.many2one('email.template', 'Email Template', ondelete='set null',
help="Define the email template to use for the email to send.")
'email_from': fields.related(
'template_id', 'email_from', type='char',
readonly=True, string='From'
),
'email_to': fields.related(
'template_id', 'email_to', type='char',
readonly=True, string='To (Emails)'
),
'partner_to': fields.related(
'template_id', 'partner_to', type='char',
readonly=True, string='To (Partners)'
),
'subject': fields.related(
'template_id', 'subject', type='char',
readonly=True, string='Subject'
),
'body_html': fields.related(
'template_id', 'body_html', type='text',
readonly=True, string='Body'
),
'template_id': fields.many2one(
'email.template', 'Email Template', ondelete='set null',
domain="[('model_id', '=', model_id)]",
),
}
def on_change_template_id(self, cr, uid, ids, template_id, context=None):
""" Render the raw template in the server action fields. """
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to']
if template_id:
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
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))
if not values.get('email_from'):
return {'warning': {'title': 'Incomplete template', 'message': 'Your template should define email_from'}, 'value': values}
else:
values = self.default_get(cr, uid, ['subject', 'body_html', 'email_from', 'email_to', 'partner_to'], context=context)
values = dict.fromkeys(fields, False)
return {'value': values}
def create(self, cr, uid, values, context=None):
if values.get('template_id'):
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
template_values = self.pool.get('email.template').read(cr, uid, values.get('template_id'), fields, context)
values.update(dict((field, template_values[field]) for field in fields if template_values.get(field)))
return super(actions_server, self).create(cr, uid, values, context=context)
def write(self, cr, uid, ids, values, context=None):
if values.get('template_id'):
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
template_values = self.pool.get('email.template').read(cr, uid, values.get('template_id'), fields, context)
values.update(dict((field, template_values[field]) for field in fields if template_values.get(field)))
return super(actions_server, self).write(cr, uid, ids, values, context=context)
def run_action_email(self, cr, uid, action, eval_context=None, context=None):
if not action.template_id or not context.get('active_id'):
return False

View File

@ -16,7 +16,6 @@
<group attrs="{'invisible': [('model_id', '=', False)]}">
<field name="template_id"
on_change='on_change_template_id(template_id)'
domain="[('model_id', '=', model_id)]"
attrs="{'required': [('state', '=', 'email')]}"/>
<p colspan="2" attrs="{'invisible': [('template_id', '!=', False)]}">
Choose a template to display its values.

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

@ -1,7 +1,7 @@
-
Create a user as 'Event manager'
-
!record {model: res.users, id: res_users_eventmanager}:
!record {model: res.users, id: res_users_eventmanager, view: False}:
company_id: base.main_company
name: Event manager
login: em
@ -16,7 +16,7 @@
-
Create a user as 'Event user'
-
!record {model: res.users, id: res_users_eventuser}:
!record {model: res.users, id: res_users_eventuser, view: False}:
company_id: base.main_company
name: User
login: eu

View File

@ -356,26 +356,9 @@ class res_users(osv.osv):
_name = 'res.users'
_inherit = 'res.users'
def create(self, cr, uid, data, context=None):
user_id = super(res_users, self).create(cr, uid, data, context=context)
# add shortcut unless 'noshortcut' is True in context
if not(context and context.get('noshortcut', False)):
data_obj = self.pool.get('ir.model.data')
try:
data_id = data_obj._get_id(cr, uid, 'hr', 'ir_ui_view_sc_employee')
view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
self.pool.get('ir.ui.view_sc').copy(cr, uid, view_id, default = {
'user_id': user_id}, context=context)
except:
# Tolerate a missing shortcut. See product/product.py for similar code.
_logger.debug('Skipped meetings shortcut for user "%s".', data.get('name','<new'))
return user_id
_columns = {
'employee_ids': fields.one2many('hr.employee', 'user_id', 'Related employees'),
}
}

View File

@ -224,13 +224,6 @@
<menuitem action="open_view_employee_list_my" id="menu_open_view_employee_list_my" sequence="3" parent="menu_hr_main"/>
<record id="ir_ui_view_sc_employee" model="ir.ui.view_sc">
<field name="name">Employees</field>
<field name="resource">ir.ui.menu</field>
<field name="user_id" ref="base.user_root"/>
<field name="res_id" ref="hr.menu_open_view_employee_list_my"/>
</record>
<!-- Employee architecture -->
<record id="view_partner_tree2" model="ir.ui.view">
<field name="name">hr.employee.tree</field>

View File

@ -1,7 +1,7 @@
-
Create a user as 'HR Manager'
-
!record {model: res.users, id: res_users_hr_manager}:
!record {model: res.users, id: res_users_hr_manager, view: False}:
company_id: base.main_company
name: HR manager
login: hrm
@ -15,7 +15,7 @@
-
Create a user as 'HR Officer'
-
!record {model: res.users, id: res_users_hr_officer}:
!record {model: res.users, id: res_users_hr_officer, view: False}:
company_id: base.main_company
name: HR Officer
login: hro
@ -29,7 +29,7 @@
-
Create a user as 'Employee'
-
!record {model: res.users, id: res_users_employee}:
!record {model: res.users, id: res_users_employee, view: False}:
company_id: base.main_company
name: Employee
login: emp

View File

@ -1,7 +1,7 @@
-
Create a user as 'HR Attendance Officer'
-
!record {model: res.users, id: res_users_attendance_officer}:
!record {model: res.users, id: res_users_attendance_officer, view: False}:
company_id: base.main_company
name: HR Officer
login: ao

File diff suppressed because one or more lines are too long

View File

@ -225,7 +225,7 @@ class hr_applicant(osv.Model):
'department_id': lambda s, cr, uid, c: s._get_default_department_id(cr, uid, c),
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'hr.applicant', context=c),
'color': 0,
'date_last_stage_update': fields.datetime.now(),
'date_last_stage_update': fields.datetime.now,
}
_group_by_full = {

View File

@ -27,14 +27,12 @@ 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."""),
'alias_prefix': fields.char('Default Alias Name for Jobs'),
'alias_domain' : fields.char('Alias Domain'),
help='Allow applicants to send their job application to an email address (jobs@mycompany.com), '
'and create automatically application documents in the system.'),
}
def get_default_alias_prefix(self, cr, uid, ids, context=None):
@ -74,4 +72,4 @@ class hr_applicant_settings(osv.osv_memory):
alias_domain = urlparse.urlsplit(domain).netloc.split(':')[0]
except Exception:
pass
return {'alias_domain': alias_domain}
return {'alias_domain': alias_domain}

View File

@ -1,7 +1,7 @@
-
Create a user as 'HR Recruitment Officer'
-
!record {model: res.users, id: res_users_hr_recruitment_officer}:
!record {model: res.users, id: res_users_hr_recruitment_officer, view: False}:
company_id: base.main_company
name: HR Recruitment Officer
login: hrro

View File

@ -1,7 +1,7 @@
-
Create a user as 'HR timesheet Manager'
-
!record {model: res.users, id: res_hr_timesheet_manager}:
!record {model: res.users, id: res_hr_timesheet_manager, view: False}:
company_id: base.main_company
name: HR timesheet manager
login: hrtm
@ -15,7 +15,7 @@
-
Create a user as 'HR timesheet Officer'
-
!record {model: res.users, id: res_hr_timesheet_officer}:
!record {model: res.users, id: res_hr_timesheet_officer, view: False}:
company_id: base.main_company
name: HR timesheet Officer
login: hrto
@ -29,7 +29,7 @@
-
Create a user as 'Timesheet Employee'
-
!record {model: res.users, id: res_hr_timesheet_employee}:
!record {model: res.users, id: res_hr_timesheet_employee, view: False}:
company_id: base.main_company
name: Timesheet Employee
login: empt

View File

@ -133,7 +133,7 @@ class hr_si_project(osv.osv_memory):
def check_state(self, cr, uid, ids, context=None):
obj_model = self.pool.get('ir.model.data')
emp_id = self.default_get(cr, uid, context)['emp_id']
emp_id = self.default_get(cr, uid, ['emp_id'], context)['emp_id']
# get the latest action (sign_in or out) for this employee
cr.execute('select action from hr_attendance where employee_id=%s and action in (\'sign_in\',\'sign_out\') order by name desc limit 1', (emp_id,))
res = (cr.fetchone() or ('sign_out',))[0]

View File

@ -458,7 +458,7 @@ class hr_timesheet_sheet_sheet_day(osv.osv):
THEN (SUM(total_attendance) +
CASE WHEN current_date <> name
THEN 1440
ELSE (EXTRACT(hour FROM current_time) * 60) + EXTRACT(minute FROM current_time)
ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC') * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC')
END
)
ELSE SUM(total_attendance)

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

@ -303,7 +303,7 @@ class im_user(osv.osv):
'name': fields.function(_get_name, type='char', size=200, string="Name", store=True, readonly=True),
'assigned_name': fields.char(string="Assigned Name", size=200, required=False),
'image': fields.related('user_id', 'image_small', type='binary', string="Image", readonly=True),
'user_id': fields.many2one("res.users", string="User", select=True, ondelete='cascade'),
'user_id': fields.many2one("res.users", string="User", select=True, ondelete='cascade', oldname='user'),
'uuid': fields.char(string="UUID", size=50, select=True),
'im_last_received': fields.integer(string="Instant Messaging Last Received Message"),
'im_last_status': fields.boolean(strint="Instant Messaging Last Status"),

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

@ -875,6 +875,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):
@ -927,8 +961,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')),
@ -940,36 +973,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):
@ -1275,8 +1279,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

@ -1218,7 +1218,7 @@ openerp.mail = function (session) {
init: function (parent, datasets, options) {
var self = this;
this._super(parent, options);
this.MailWidget = parent.__proto__ == mail.Widget.prototype ? parent : false;
this.MailWidget = parent instanceof mail.Widget ? parent : false;
this.domain = options.domain || [];
this.context = _.extend(options.context || {});

View File

@ -48,7 +48,8 @@ openerp.mail.suggestions = function(session, mail) {
join_group: function (event) {
var self = this;
return this.mail_group.call('message_subscribe_users', [[$(event.currentTarget).attr('id')],[this.session.uid]]).then(function(res) {
var group_id = parseInt($(event.currentTarget).attr('id'), 10);
return this.mail_group.call('message_subscribe_users', [[group_id], [this.session.uid]]).then(function(res) {
self.fetch_suggested_groups();
});
},

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,58 @@
# -*- 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': """
Easily send mass mailing to your leads, opportunities or customers. Track
marketing campaigns performance to improve conversion rates. Design
professional emails and reuse templates in a few clicks.
""",
'version': '1.0',
'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,379 @@
# -*- 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 = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened', context=context)
domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied', 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,227 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Mass Mailing Made Easy</h2>
<h3 class="oe_slogan">Design, Send, Track Emails</h3>
<div class="oe_span6">
<div class="oe_bg_img">
<img src="mass_mailing_campaign.png" class="oe_picture oe_screenshot">
</div>
</div>
<div class="oe_span6">
<p class='oe_mt32'>
Easily send mass mailing to your leads, opportunities or customers. Track
marketing campaigns performance to improve conversion rates. Design
professional emails and reuse templates in a few clicks.
</p>
<div class="oe_centeralign oe_websiteonly">
<a href="http://www.openerp.com/start" class="oe_button oe_big oe_tacky">Start your <span class="oe_emph">free</span> trial</a>
</div>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row">
<h2 class="oe_slogan">Send Professional Emails</h2>
<div class="oe_span6">
<p class='oe_mt32'>
Import database of prospects or filter on
existing leads, opportunities and customers in just a few clicks.
</p><p>
Define email templates to reuse content or specific design for your newsletter.
Setup several email servers with their own IP/domain to optimise opening rates.
</p>
</div>
<div class="oe_span6">
<div class="oe_bg_img">
<img class="oe_picture oe_screenshot" src="mass_mailing_composer.png">
</div>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row">
<h2 class="oe_slogan">Organize Marketing Campaigns</h2>
<h3 class="oe_slogan">Design, Send, Track by Campaigns</h3>
<div class="oe_span6">
<div class="oe_bg_img">
<img class="oe_picture oe_screenshot" src="mass_mailing_campaign.png">
</div>
</div>
<div class="oe_span6">
<p class='oe_mt32'>
Get real time statistics on campaigns performance to improve your
conversion rate. Track mails sent, received, opened and
answered.
</p><p>
Easily manage your marketing campaigns, discussion groups,
leads and opportunities in one simple and powerful platform.
</p>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row">
<h2 class="oe_slogan">Integrated with OpenERP Apps</h2>
<h2 class="oe_slogan"></h2>
<div class="oe_span6">
<p class='oe_mt32'>
Get access to mass mailing features from every OpenERP app to improve
the way your users communicate.
</p><p>
Send template of emails from <a href="/apps/crm">CRM</a> opportunities, select leads based on
marketing segments, send <a href="/apps/hr_recruitment">jobs offers</a> and automate answers to applicants, reuse
email template in the <a href="/apps/marketing_campaign">lead automation marketing campaigns</a>.
</p><p>
Answers to your emails appears automatically in the history of every document with the
<a href="/apps/mail">social network</a> module.
</p>
</div>
<div class="oe_span6">
<div class="oe_bg_img">
<img class="oe_picture oe_screenshot" src="mass_mailing_tasks.png">
</div>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row">
<h2 class="oe_slogan">Clean Your Lead Database</h2>
<h3 class="oe_slogan">Handle bounce and conversion rates</h3>
<div class="oe_span6">
<div class="oe_bg_img">
<img class="oe_picture oe_screenshot" src="mass_mailing_mailing.png">
</div>
</div>
<div class="oe_span6">
<p class='oe_mt32'>
Get a clean lead database that improves over the time using the
performance of your mails. OpenERP handle bounce mails
efficiently, flag erroneous leads accordingly and gives you
statistics on the quality of your leads.
</p>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row">
<h2 class="oe_slogan">One click emails send</h2>
<h3 class="oe_slogan">Select any documents, send emails</h3>
<div class="oe_span6">
<p class='oe_mt32'>
The marketing department will love working on campaigns. But you can also
give a one click mass mailing facility to all others users on their own
prospects or documents.
</p><p>
Select a few documents (e.g. leads, support tickets, suppliers, applicants,
...) and send emails to their contacts in one click, reusing existing emails
templates.
</p>
</div>
<div class="oe_span6">
<div class="oe_bg_img">
<img class="oe_picture oe_screenshot" src="mass_mailing_composer.png">
</div>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row">
<h2 class="oe_slogan">Follow-up On Answers</h2>
<h3 class="oe_slogan">Communicate efficiently with prospects</h3>
<div class="oe_span6">
<div class="oe_bg_img">
<img class="oe_picture oe_screenshot" src="mass_mailing_history.png">
</div>
</div>
<div class="oe_span6">
<p class='oe_mt32'>
The chatter feature enables you to communicate faster and more efficiently with
your customer. Get documents created automatically (leads, opportunities,
tasks, ...) based on answers to your mass mailing campaigns Follow the
discussion directly on the business documents within OpenERP or via email.
</p><p>
Get all the negotiations and discussions attached to the right document
and relevent managers notified on specific events.
</p>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Campaigns Dashboard</h2>
<h3 class="oe_slogan">Analyse the performance of your campaigns</h3>
<div class="oe_span6">
<p class="oe_mt32">
Get the insights you need to make smarter marketing campaign. Track statistics
per campaign: bounce rates, sent mails, best content, etc. The clear dashboards
gives you a direct overview of your campaign performance.
</p>
</div>
<div class="oe_span6">
<div class="oe_bg_img">
<img class="oe_picture oe_screenshot" src="mass_mailing_dashboard.png">
</div>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row">
<h2 class="oe_slogan">Fully Integrated With Others Apps</h2>
<h3 class="oe_slogan">Efficient mailing for every users</h3>
</div>
<div class="oe_row">
<div class="oe_span4">
<a href="/apps/marketing_campaign">
<h3>Lead Automation</h3>
<div class="oe_row_img oe_centered">
<img class="oe_picture oe_screenshot" src="app_marketing_campaign.png">
</div>
</a>
<p>
Define automated actions (e.g. ask a salesperson to call, send
an email, ...) based on triggers (no activity since 20 days,
answered a promotional email, etc.)
</p><p>
Optimize campaigns from lead to close, on every channel. Make
smarter decisions about where to invest and show the impact of
your marketing activities on your company's bottom line.
</p>
</div>
<div class="oe_span4">
<a href="/apps/website_crm">
<h3>Website Forms</h3>
<div class="oe_row_img oe_centered">
<img class="oe_picture oe_screenshot" src="app_website_crm.png">
</div>
</a>
<p>
Integrate a contact form in your website easily. Forms
submissions create leads automatically in OpenERP CRM. Leads
can be used in marketing campaigns.
</p>
</div>
<div class="oe_span4">
<a href="/apps/crm">
<h3>CRM</h3>
<div class="oe_row_img oe_centered">
<img class="oe_picture oe_screenshot" src="app_crm.png">
</div>
</a>
<p>
Manage your sales funnel with no effort. Attract leads, follow-up on phone
calls and meetings. Analyse the quality of your leads to make informed
decisions and save time by integrating emails directly into the application.
</p>
</div>
</div>
</section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

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

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-2011 OpenERP s.a. (<http://openerp.com>).
# 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
@ -18,27 +18,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Web Shortcuts',
'version': '1.0',
'category': 'Tools',
'description': """
Enable shortcuts feature in the web client.
===========================================
Add a Shortcut icon in the systray in order to access the user's shortcuts (if any).
from openerp.addons.mail.tests.common import TestMail
Add a Shortcut icon besides the views title in order to add/remove a shortcut.
""",
'author': 'OpenERP SA',
'website': 'http://openerp.com',
'depends': ['base'],
'data': [],
'js' : ['static/src/js/web_shortcuts.js'],
'css' : ['static/src/css/web_shortcuts.css'],
'qweb' : ['static/src/xml/*.xml'],
'installable': True,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
class test_message_compose(TestMail):
def test_OO_mail_mail_tracking(self):
""" Tests designed for mail_mail tracking (opened, replied, bounced) """
pass

View File

@ -1,21 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
# 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.
# 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.
# 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/>.
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
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

@ -77,6 +77,7 @@ Dashboard / Reports for MRP will include:
#TODO: This yml tests are needed to be completely reviewed again because the product wood panel is removed in product demo as it does not suit for new demo context of computer and consultant company
# so the ymls are too complex to change at this stage
'test': [
'test/bom_with_service_type_product.yml',
'test/mrp_users.yml',
'test/order_demo.yml',
'test/order_process.yml',

Some files were not shown because too many files have changed in this diff Show More