[Merge] lp:openobject-addons
bzr revid: dbr@tinyerp.com-20130924102731-bkh9bgamdhl60whk
@@ -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."),
|
||||
|
||||
@@ -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-24 05:55+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
|
||||
|
||||
@@ -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 ""
|
||||
@@ -26,8 +26,8 @@ class base_config_settings(osv.osv_memory):
|
||||
_inherit = 'res.config.settings'
|
||||
_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',
|
||||
@@ -64,18 +64,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:
|
||||
|
||||
@@ -123,6 +123,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>
|
||||
|
||||
@@ -253,6 +253,8 @@ class crm_lead(format_address, osv.osv):
|
||||
multi='day_close', type="float", store=True),
|
||||
'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
|
||||
|
||||
# Messaging and marketing
|
||||
'message_bounce': fields.integer('Bounce'),
|
||||
# Only used for type opportunity
|
||||
'probability': fields.float('Success Rate (%)', group_operator="avg"),
|
||||
'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
|
||||
@@ -296,7 +298,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 = [
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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."""),
|
||||
|
||||
@@ -108,7 +108,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,
|
||||
|
||||
@@ -80,7 +80,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],
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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', [])]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"/> -->
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -27,11 +27,11 @@ class hr_applicant_settings(osv.osv_memory):
|
||||
|
||||
_columns = {
|
||||
'module_document_ftp': fields.boolean('Allow the automatic indexation of resumes',
|
||||
help="""Manage your CV's and motivation letter related to all applicants.
|
||||
This installs the module document_ftp. This will install the knowledge management module in order to allow you to search using specific keywords through the content of all documents (PDF, .DOCx...)"""),
|
||||
help='Manage your CV\'s and motivation letter related to all applicants.\n'
|
||||
'-This installs the module document_ftp. This will install the knowledge management module in order to allow you to search using specific keywords through the content of all documents (PDF, .DOCx...)'),
|
||||
'fetchmail_applicants': fields.boolean('Create applicants from an incoming email account',
|
||||
fetchmail_model='hr.applicant', fetchmail_name='Incoming HR Applications',
|
||||
help ="""Allow applicants to send their job application to an email address (jobs@mycompany.com),
|
||||
and create automatically application documents in the system."""),
|
||||
help='Allow applicants to send their job application to an email address (jobs@mycompany.com), '
|
||||
'and create automatically application documents in the system.'),
|
||||
}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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``.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -874,6 +874,40 @@ class mail_thread(osv.AbstractModel):
|
||||
"No possible route found for incoming message from %s to %s (Message-Id %s:)." \
|
||||
"Create an appropriate mail.alias or force the destination model." % (email_from, email_to, message_id)
|
||||
|
||||
def message_route_process(self, cr, uid, message, message_dict, routes, context=None):
|
||||
# postpone setting message_dict.partner_ids after message_post, to avoid double notifications
|
||||
partner_ids = message_dict.pop('partner_ids', [])
|
||||
thread_id = False
|
||||
for model, thread_id, custom_values, user_id, alias in routes:
|
||||
if self._name == 'mail.thread':
|
||||
context.update({'thread_model': model})
|
||||
if model:
|
||||
model_pool = self.pool[model]
|
||||
assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
|
||||
"Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
|
||||
(message_dict['message_id'], model)
|
||||
|
||||
# disabled subscriptions during message_new/update to avoid having the system user running the
|
||||
# email gateway become a follower of all inbound messages
|
||||
nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True)
|
||||
if thread_id and hasattr(model_pool, 'message_update'):
|
||||
model_pool.message_update(cr, user_id, [thread_id], message_dict, context=nosub_ctx)
|
||||
else:
|
||||
thread_id = model_pool.message_new(cr, user_id, message_dict, custom_values, context=nosub_ctx)
|
||||
else:
|
||||
assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
|
||||
model_pool = self.pool.get('mail.thread')
|
||||
if not hasattr(model_pool, 'message_post'):
|
||||
context['thread_model'] = model
|
||||
model_pool = self.pool['mail.thread']
|
||||
new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **message_dict)
|
||||
|
||||
if partner_ids:
|
||||
# postponed after message_post, because this is an external message and we don't want to create
|
||||
# duplicate emails due to notifications
|
||||
self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
|
||||
return thread_id
|
||||
|
||||
def message_process(self, cr, uid, model, message, custom_values=None,
|
||||
save_original=False, strip_attachments=False,
|
||||
thread_id=None, context=None):
|
||||
@@ -926,8 +960,7 @@ class mail_thread(osv.AbstractModel):
|
||||
msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context)
|
||||
if strip_attachments:
|
||||
msg.pop('attachments', None)
|
||||
# postpone setting msg.partner_ids after message_post, to avoid double notifications
|
||||
partner_ids = msg.pop('partner_ids', [])
|
||||
|
||||
if msg.get('message_id'): # should always be True as message_parse generate one if missing
|
||||
existing_msg_ids = self.pool.get('mail.message').search(cr, SUPERUSER_ID, [
|
||||
('message_id', '=', msg.get('message_id')),
|
||||
@@ -939,36 +972,7 @@ class mail_thread(osv.AbstractModel):
|
||||
|
||||
# find possible routes for the message
|
||||
routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context)
|
||||
thread_id = False
|
||||
for model, thread_id, custom_values, user_id, alias in routes:
|
||||
if self._name == 'mail.thread':
|
||||
context.update({'thread_model': model})
|
||||
if model:
|
||||
model_pool = self.pool[model]
|
||||
assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
|
||||
"Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
|
||||
(msg['message_id'], model)
|
||||
|
||||
# disabled subscriptions during message_new/update to avoid having the system user running the
|
||||
# email gateway become a follower of all inbound messages
|
||||
nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True)
|
||||
if thread_id and hasattr(model_pool, 'message_update'):
|
||||
model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
|
||||
else:
|
||||
thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
|
||||
else:
|
||||
assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
|
||||
model_pool = self.pool.get('mail.thread')
|
||||
if not hasattr(model_pool, 'message_post'):
|
||||
context['thread_model'] = model
|
||||
model_pool = self.pool['mail.thread']
|
||||
new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
|
||||
|
||||
if partner_ids:
|
||||
# postponed after message_post, because this is an external message and we don't want to create
|
||||
# duplicate emails due to notifications
|
||||
self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
|
||||
|
||||
thread_id = self.message_route_process(cr, uid, msg_txt, msg, routes, context=context)
|
||||
return thread_id
|
||||
|
||||
def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
|
||||
@@ -1274,8 +1278,8 @@ class mail_thread(osv.AbstractModel):
|
||||
return result
|
||||
|
||||
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
|
||||
subtype=None, parent_id=False, attachments=None, context=None,
|
||||
content_subtype='html', **kwargs):
|
||||
subtype=None, parent_id=False, attachments=None, context=None,
|
||||
content_subtype='html', **kwargs):
|
||||
""" Post a new message in an existing thread, returning the new
|
||||
mail.message ID.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -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):
|
||||
|
||||
@@ -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. """
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import main
|
||||
|
||||
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
@@ -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=="
|
||||
@@ -0,0 +1,9 @@
|
||||
.. _changelog:
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
`trunk (saas-2)`
|
||||
----------------
|
||||
|
||||
- added module
|
||||
@@ -0,0 +1,13 @@
|
||||
Mass Mailing module documentation
|
||||
=================================
|
||||
|
||||
Mass Mailing documentation topics
|
||||
'''''''''''''''''''''''''''''''''
|
||||
|
||||
Changelog
|
||||
'''''''''
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
changelog.rst
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1,376 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil import relativedelta
|
||||
|
||||
from openerp import tools
|
||||
from openerp.tools.translate import _
|
||||
from openerp.osv import osv, fields
|
||||
|
||||
|
||||
class MassMailingCampaign(osv.Model):
|
||||
"""Model of mass mailing campaigns.
|
||||
"""
|
||||
_name = "mail.mass_mailing.campaign"
|
||||
_description = 'Mass Mailing Campaign'
|
||||
# number of embedded mailings in kanban view
|
||||
_kanban_mailing_nbr = 4
|
||||
|
||||
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
|
||||
""" Compute statistics of the mass mailing campaign """
|
||||
results = dict.fromkeys(ids, False)
|
||||
for campaign in self.browse(cr, uid, ids, context=context):
|
||||
results[campaign.id] = {
|
||||
'sent': len(campaign.statistics_ids),
|
||||
# delivered: shouldn't be: all mails - (failed + bounced) ?
|
||||
'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]), # stat.state == 'sent' and
|
||||
'opened': len([stat for stat in campaign.statistics_ids if stat.opened]),
|
||||
'replied': len([stat for stat in campaign.statistics_ids if stat.replied]),
|
||||
'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]),
|
||||
}
|
||||
return results
|
||||
|
||||
def _get_mass_mailing_kanban_ids(self, cr, uid, ids, name, arg, context=None):
|
||||
""" Gather data about mass mailings to display them in kanban view as
|
||||
nested kanban views is not possible currently. """
|
||||
results = dict.fromkeys(ids, '')
|
||||
for campaign in self.browse(cr, uid, ids, context=context):
|
||||
mass_mailing_results = []
|
||||
for mass_mailing in campaign.mass_mailing_ids[:self._kanban_mailing_nbr]:
|
||||
mass_mailing_object = {}
|
||||
for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']:
|
||||
mass_mailing_object[attr] = getattr(mass_mailing, attr)
|
||||
mass_mailing_results.append(mass_mailing_object)
|
||||
results[campaign.id] = mass_mailing_results
|
||||
return results
|
||||
|
||||
_columns = {
|
||||
'name': fields.char(
|
||||
'Campaign Name', required=True,
|
||||
),
|
||||
'user_id': fields.many2one(
|
||||
'res.users', 'Responsible',
|
||||
required=True,
|
||||
),
|
||||
'mass_mailing_ids': fields.one2many(
|
||||
'mail.mass_mailing', 'mass_mailing_campaign_id',
|
||||
'Mass Mailings',
|
||||
),
|
||||
'mass_mailing_kanban_ids': fields.function(
|
||||
_get_mass_mailing_kanban_ids,
|
||||
type='text', string='Mass Mailings (kanban data)',
|
||||
help='This field has for purpose to gather data about mass mailings '
|
||||
'to display them in kanban view as nested kanban views is not '
|
||||
'possible currently',
|
||||
),
|
||||
'statistics_ids': fields.one2many(
|
||||
'mail.mail.statistics', 'mass_mailing_campaign_id',
|
||||
'Sent Emails',
|
||||
),
|
||||
'color': fields.integer('Color Index'),
|
||||
# stat fields
|
||||
'sent': fields.function(
|
||||
_get_statistics,
|
||||
string='Sent Emails',
|
||||
type='integer', multi='_get_statistics'
|
||||
),
|
||||
'delivered': fields.function(
|
||||
_get_statistics,
|
||||
string='Delivered',
|
||||
type='integer', multi='_get_statistics',
|
||||
),
|
||||
'opened': fields.function(
|
||||
_get_statistics,
|
||||
string='Opened',
|
||||
type='integer', multi='_get_statistics',
|
||||
),
|
||||
'replied': fields.function(
|
||||
_get_statistics,
|
||||
string='Replied',
|
||||
type='integer', multi='_get_statistics'
|
||||
),
|
||||
'bounced': fields.function(
|
||||
_get_statistics,
|
||||
string='Bounced',
|
||||
type='integer', multi='_get_statistics'
|
||||
),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'user_id': lambda self, cr, uid, ctx=None: uid,
|
||||
}
|
||||
|
||||
def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None):
|
||||
ctx = dict(context)
|
||||
ctx.update({
|
||||
'default_mass_mailing_campaign_id': ids[0],
|
||||
})
|
||||
return {
|
||||
'name': _('Create a Mass Mailing for the Campaign'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.mass_mailing.create',
|
||||
'views': [(False, 'form')],
|
||||
'view_id': False,
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
|
||||
class MassMailing(osv.Model):
|
||||
""" MassMailing models a wave of emails for a mass mailign campaign.
|
||||
A mass mailing is an occurence of sending emails. """
|
||||
|
||||
_name = 'mail.mass_mailing'
|
||||
_description = 'Wave of sending emails'
|
||||
# number of periods for tracking mail_mail statistics
|
||||
_period_number = 6
|
||||
_order = 'date DESC'
|
||||
|
||||
def __get_bar_values(self, cr, uid, id, obj, domain, read_fields, value_field, groupby_field, context=None):
|
||||
""" Generic method to generate data for bar chart values using SparklineBarWidget.
|
||||
This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
|
||||
|
||||
:param obj: the target model (i.e. crm_lead)
|
||||
:param domain: the domain applied to the read_group
|
||||
:param list read_fields: the list of fields to read in the read_group
|
||||
:param str value_field: the field used to compute the value of the bar slice
|
||||
:param str groupby_field: the fields used to group
|
||||
|
||||
:return list section_result: a list of dicts: [
|
||||
{ 'value': (int) bar_column_value,
|
||||
'tootip': (str) bar_column_tooltip,
|
||||
}
|
||||
]
|
||||
"""
|
||||
date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
|
||||
section_result = [{'value': 0,
|
||||
'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
|
||||
} for i in range(0, self._period_number)]
|
||||
group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
|
||||
for group in group_obj:
|
||||
group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT).date()
|
||||
timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
|
||||
section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
|
||||
return section_result
|
||||
|
||||
def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
|
||||
""" Get the daily statistics of the mass mailing. This is done by a grouping
|
||||
on opened and replied fields. Using custom format in context, we obtain
|
||||
results for the next 6 days following the mass mailing date. """
|
||||
obj = self.pool['mail.mail.statistics']
|
||||
res = {}
|
||||
context['datetime_format'] = {
|
||||
'opened': {
|
||||
'interval': 'day',
|
||||
'groupby_format': 'yyyy-mm-dd',
|
||||
'display_format': 'dd MMMM YYYY'
|
||||
},
|
||||
'replied': {
|
||||
'interval': 'day',
|
||||
'groupby_format': 'yyyy-mm-dd',
|
||||
'display_format': 'dd MMMM YYYY'
|
||||
},
|
||||
}
|
||||
for id in ids:
|
||||
res[id] = {}
|
||||
date_begin = self.browse(cr, uid, id, context=context).date
|
||||
domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin)]
|
||||
res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened', context=context)
|
||||
domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin)]
|
||||
res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied', context=context)
|
||||
return res
|
||||
|
||||
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
|
||||
""" Compute statistics of the mass mailing campaign """
|
||||
results = dict.fromkeys(ids, False)
|
||||
for mass_mailing in self.browse(cr, uid, ids, context=context):
|
||||
results[mass_mailing.id] = {
|
||||
'sent': len(mass_mailing.statistics_ids),
|
||||
'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]), # mail.state == 'sent' and
|
||||
'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]),
|
||||
'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]),
|
||||
'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]),
|
||||
}
|
||||
return results
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Name', required=True),
|
||||
'mass_mailing_campaign_id': fields.many2one(
|
||||
'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
|
||||
ondelete='cascade', required=True,
|
||||
),
|
||||
'template_id': fields.many2one(
|
||||
'email.template', 'Email Template',
|
||||
ondelete='set null',
|
||||
),
|
||||
'domain': fields.char('Domain'),
|
||||
'date': fields.datetime('Date'),
|
||||
'color': fields.related(
|
||||
'mass_mailing_campaign_id', 'color',
|
||||
type='integer', string='Color Index',
|
||||
),
|
||||
# statistics data
|
||||
'statistics_ids': fields.one2many(
|
||||
'mail.mail.statistics', 'mass_mailing_id',
|
||||
'Emails Statistics',
|
||||
),
|
||||
'sent': fields.function(
|
||||
_get_statistics,
|
||||
string='Sent Emails',
|
||||
type='integer', multi='_get_statistics'
|
||||
),
|
||||
'delivered': fields.function(
|
||||
_get_statistics,
|
||||
string='Delivered',
|
||||
type='integer', multi='_get_statistics',
|
||||
),
|
||||
'opened': fields.function(
|
||||
_get_statistics,
|
||||
string='Opened',
|
||||
type='integer', multi='_get_statistics',
|
||||
),
|
||||
'replied': fields.function(
|
||||
_get_statistics,
|
||||
string='Replied',
|
||||
type='integer', multi='_get_statistics'
|
||||
),
|
||||
'bounced': fields.function(
|
||||
_get_statistics,
|
||||
string='Bounce',
|
||||
type='integer', multi='_get_statistics'
|
||||
),
|
||||
# monthly ratio
|
||||
'opened_monthly': fields.function(
|
||||
_get_daily_statistics,
|
||||
string='Opened',
|
||||
type='char', multi='_get_daily_statistics',
|
||||
),
|
||||
'replied_monthly': fields.function(
|
||||
_get_daily_statistics,
|
||||
string='Replied',
|
||||
type='char', multi='_get_daily_statistics',
|
||||
),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'date': fields.datetime.now,
|
||||
}
|
||||
|
||||
|
||||
class MailMailStats(osv.Model):
|
||||
""" MailMailStats models the statistics collected about emails. Those statistics
|
||||
are stored in a separated model and table to avoid bloating the mail_mail table
|
||||
with statistics values. This also allows to delete emails send with mass mailing
|
||||
without loosing the statistics about them. """
|
||||
|
||||
_name = 'mail.mail.statistics'
|
||||
_description = 'Email Statistics'
|
||||
_rec_name = 'message_id'
|
||||
_order = 'message_id'
|
||||
|
||||
_columns = {
|
||||
'mail_mail_id': fields.integer(
|
||||
'Mail ID',
|
||||
help='ID of the related mail_mail. This field is an integer field because'
|
||||
'the related mail_mail can be deleted separately from its statistics.'
|
||||
),
|
||||
'message_id': fields.char(
|
||||
'Message-ID',
|
||||
),
|
||||
'model': fields.char(
|
||||
'Document model',
|
||||
),
|
||||
'res_id': fields.integer(
|
||||
'Document ID',
|
||||
),
|
||||
# campaign / wave data
|
||||
'mass_mailing_id': fields.many2one(
|
||||
'mail.mass_mailing', 'Mass Mailing',
|
||||
ondelete='set null',
|
||||
),
|
||||
'mass_mailing_campaign_id': fields.related(
|
||||
'mass_mailing_id', 'mass_mailing_campaign_id',
|
||||
type='many2one', ondelete='set null',
|
||||
relation='mail.mass_mailing.campaign',
|
||||
string='Mass Mailing Campaign',
|
||||
store=True, readonly=True,
|
||||
),
|
||||
'template_id': fields.related(
|
||||
'mass_mailing_id', 'template_id',
|
||||
type='many2one', ondelete='set null',
|
||||
relation='email.template',
|
||||
string='Email Template',
|
||||
store=True, readonly=True,
|
||||
),
|
||||
# Bounce and tracking
|
||||
'opened': fields.datetime(
|
||||
'Opened',
|
||||
help='Date when this email has been opened for the first time.'),
|
||||
'replied': fields.datetime(
|
||||
'Replied',
|
||||
help='Date when this email has been replied for the first time.'),
|
||||
'bounced': fields.datetime(
|
||||
'Bounced',
|
||||
help='Date when this email has bounced.'
|
||||
),
|
||||
}
|
||||
|
||||
def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
|
||||
""" Set as opened """
|
||||
if not ids and mail_mail_ids:
|
||||
ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
|
||||
elif not ids and mail_message_ids:
|
||||
ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
|
||||
else:
|
||||
ids = []
|
||||
for stat in self.browse(cr, uid, ids, context=context):
|
||||
if not stat.opened:
|
||||
self.write(cr, uid, [stat.id], {'opened': fields.datetime.now()}, context=context)
|
||||
return ids
|
||||
|
||||
def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
|
||||
""" Set as replied """
|
||||
if not ids and mail_mail_ids:
|
||||
ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
|
||||
elif not ids and mail_message_ids:
|
||||
ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
|
||||
else:
|
||||
ids = []
|
||||
for stat in self.browse(cr, uid, ids, context=context):
|
||||
if not stat.replied:
|
||||
self.write(cr, uid, [stat.id], {'replied': fields.datetime.now()}, context=context)
|
||||
return ids
|
||||
|
||||
def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
|
||||
""" Set as bounced """
|
||||
if not ids and mail_mail_ids:
|
||||
ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
|
||||
elif not ids and mail_message_ids:
|
||||
ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
|
||||
else:
|
||||
ids = []
|
||||
for stat in self.browse(cr, uid, ids, context=context):
|
||||
if not stat.bounced:
|
||||
self.write(cr, uid, [stat.id], {'bounced': fields.datetime.now()}, context=context)
|
||||
return ids
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 56 KiB |
@@ -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
|
||||
*/
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
};
|
||||
@@ -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,
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.addons.mail.tests.common import TestMail
|
||||
|
||||
|
||||
class test_message_compose(TestMail):
|
||||
|
||||
def test_OO_mail_mail_tracking(self):
|
||||
""" Tests designed for mail_mail tracking (opened, replied, bounced) """
|
||||
pass
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import mail_compose_message
|
||||
import mail_mass_mailing_create_segment
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -28,48 +28,48 @@ class mrp_config_settings(osv.osv_memory):
|
||||
|
||||
_columns = {
|
||||
'module_mrp_repair': fields.boolean("Manage repairs of products ",
|
||||
help="""Allows to manage all product repairs.
|
||||
* Add/remove products in the reparation
|
||||
* Impact for stocks
|
||||
* Invoicing (products and/or services)
|
||||
* Warranty concept
|
||||
* Repair quotation report
|
||||
* Notes for the technician and for the final customer.
|
||||
This installs the module mrp_repair."""),
|
||||
'module_mrp_operations': fields.boolean("Allow detailed planning of work orders",
|
||||
help="""This allows to add state, date_start,date_stop in production order operation lines (in the "Work Centers" tab).
|
||||
This installs the module mrp_operations."""),
|
||||
help='Allows to manage all product repairs.\n'
|
||||
'* Add/remove products in the reparation\n'
|
||||
'* Impact for stocks\n'
|
||||
'* Invoicing (products and/or services)\n'
|
||||
'* Warranty concept\n'
|
||||
'* Repair quotation report\n'
|
||||
'* Notes for the technician and for the final customer.\n'
|
||||
'-This installs the module mrp_repair.'),
|
||||
'module_mrp_operations': fields.boolean("Allow detailed planning of work order",
|
||||
help='This allows to add state, date_start,date_stop in production order operation lines (in the "Work Centers" tab).\n'
|
||||
'-This installs the module mrp_operations.'),
|
||||
'module_mrp_byproduct': fields.boolean("Produce several products from one manufacturing order",
|
||||
help="""You can configure by-products in the bill of material.
|
||||
Without this module: A + B + C -> D.
|
||||
With this module: A + B + C -> D + E.
|
||||
This installs the module mrp_byproduct."""),
|
||||
help='You can configure by-products in the bill of material.\n'
|
||||
'Without this module: A + B + C -> D.\n'
|
||||
'With this module: A + B + C -> D + E.\n'
|
||||
'-This installs the module mrp_byproduct.'),
|
||||
'module_mrp_jit': fields.boolean("Generate procurement in real time",
|
||||
help="""This allows Just In Time computation of procurement orders.
|
||||
All procurement orders will be processed immediately, which could in some
|
||||
cases entail a small performance impact.
|
||||
This installs the module mrp_jit."""),
|
||||
help='This allows Just In Time computation of procurement orders.\n'
|
||||
'All procurement orders will be processed immediately, which could in some '
|
||||
'cases entail a small performance impact.\n'
|
||||
'-This installs the module mrp_jit.'),
|
||||
'module_stock_no_autopicking': fields.boolean("Manage manual picking to fulfill manufacturing orders ",
|
||||
help="""This module allows an intermediate picking process to provide raw materials to production orders.
|
||||
For example to manage production made by your suppliers (sub-contracting).
|
||||
To achieve this, set the assembled product which is sub-contracted to "No Auto-Picking"
|
||||
and put the location of the supplier in the routing of the assembly operation.
|
||||
This installs the module stock_no_autopicking."""),
|
||||
help='This module allows an intermediate picking process to provide raw materials to production orders.\n'
|
||||
'For example to manage production made by your suppliers (sub-contracting).\n'
|
||||
'To achieve this, set the assembled product which is sub-contracted to "No Auto-Picking" '
|
||||
'and put the location of the supplier in the routing of the assembly operation.\n'
|
||||
'-This installs the module stock_no_autopicking.'),
|
||||
'group_mrp_routings': fields.boolean("Manage routings and work orders ",
|
||||
implied_group='mrp.group_mrp_routings',
|
||||
help="""Routings allow you to create and manage the manufacturing operations that should be followed
|
||||
within your work centers in order to produce a product. They are attached to bills of materials
|
||||
that will define the required raw materials."""),
|
||||
'group_mrp_properties': fields.boolean("Allow several bill of materials per product using properties",
|
||||
help='Routings allow you to create and manage the manufacturing operations that should be followed '
|
||||
'within your work centers in order to produce a product. They are attached to bills of materials '
|
||||
'that will define the required raw materials.'),
|
||||
'group_mrp_properties': fields.boolean("Allow several bill of materials per products using properties",
|
||||
implied_group='product.group_mrp_properties',
|
||||
help="""The selection of the right Bill of Material to use will depend on the properties specified on the sales order and the Bill of Material."""),
|
||||
'module_product_manufacturer': fields.boolean("Define manufacturers on products ",
|
||||
help="""This allows you to define the following for a product:
|
||||
* Manufacturer
|
||||
* Manufacturer Product Name
|
||||
* Manufacturer Product Code
|
||||
* Product Attributes.
|
||||
This installs the module product_manufacturer."""),
|
||||
help='This allows you to define the following for a product:\n'
|
||||
'* Manufacturer\n'
|
||||
'* Manufacturer Product Name\n'
|
||||
'* Manufacturer Product Code\n'
|
||||
'* Product Attributes.\n'
|
||||
'-This installs the module product_manufacturer.'),
|
||||
}
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -98,6 +98,7 @@ Main Features
|
||||
'static/src/js/widgets.js',
|
||||
'static/src/js/devices.js',
|
||||
'static/src/js/screens.js',
|
||||
'static/src/js/tests.js',
|
||||
'static/src/js/main.js',
|
||||
],
|
||||
'css': [
|
||||
|
||||
@@ -10,6 +10,8 @@ from openerp.addons.web import http
|
||||
from openerp.addons.web.http import request
|
||||
from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class PointOfSaleController(http.Controller):
|
||||
def __init__(self):
|
||||
self.scale = 'closed'
|
||||
@@ -71,7 +73,7 @@ class PointOfSaleController(http.Controller):
|
||||
|
||||
@http.route('/pos/test_connection', type='json', auth='admin')
|
||||
def test_connection(self):
|
||||
return
|
||||
_logger.info('Received Connection Test from the Point of Sale');
|
||||
|
||||
@http.route('/pos/scan_item_success', type='json', auth='admin')
|
||||
def scan_item_success(self, ean):
|
||||
@@ -79,15 +81,13 @@ class PointOfSaleController(http.Controller):
|
||||
A product has been scanned with success
|
||||
"""
|
||||
print 'scan_item_success: ' + str(ean)
|
||||
return
|
||||
|
||||
@http.route('/pos/scan_item_error_unrecognized')
|
||||
@http.route('/pos/scan_item_error_unrecognized', type='json', auth='admin')
|
||||
def scan_item_error_unrecognized(self, ean):
|
||||
"""
|
||||
A product has been scanned without success
|
||||
"""
|
||||
print 'scan_item_error_unrecognized: ' + str(ean)
|
||||
return
|
||||
|
||||
@http.route('/pos/help_needed', type='json', auth='admin')
|
||||
def help_needed(self):
|
||||
@@ -95,7 +95,6 @@ class PointOfSaleController(http.Controller):
|
||||
The user wants an help (ex: light is on)
|
||||
"""
|
||||
print "help_needed"
|
||||
return
|
||||
|
||||
@http.route('/pos/help_canceled', type='json', auth='admin')
|
||||
def help_canceled(self):
|
||||
@@ -103,7 +102,6 @@ class PointOfSaleController(http.Controller):
|
||||
The user stops the help request
|
||||
"""
|
||||
print "help_canceled"
|
||||
return
|
||||
|
||||
@http.route('/pos/weighting_start', type='json', auth='admin')
|
||||
def weighting_start(self):
|
||||
@@ -115,7 +113,6 @@ class PointOfSaleController(http.Controller):
|
||||
print "... Scale Open."
|
||||
else:
|
||||
print "WARNING: Scale already Connected !!!"
|
||||
return
|
||||
|
||||
@http.route('/pos/weighting_read_kg', type='json', auth='admin')
|
||||
def weighting_read_kg(self):
|
||||
@@ -156,41 +153,37 @@ class PointOfSaleController(http.Controller):
|
||||
@http.route('/pos/payment_cancel', type='json', auth='admin')
|
||||
def payment_cancel(self):
|
||||
print "payment_cancel"
|
||||
return
|
||||
|
||||
@http.route('/pos/transaction_start', type='json', auth='admin')
|
||||
def transaction_start(self):
|
||||
print 'transaction_start'
|
||||
return
|
||||
|
||||
@http.route('/pos/transaction_end', type='json', auth='admin')
|
||||
def transaction_end(self):
|
||||
print 'transaction_end'
|
||||
return
|
||||
|
||||
@http.route('/pos/cashier_mode_activated', type='json', auth='admin')
|
||||
def cashier_mode_activated(self):
|
||||
print 'cashier_mode_activated'
|
||||
return
|
||||
|
||||
@http.route('/pos/cashier_mode_deactivated', type='json', auth='admin')
|
||||
def cashier_mode_deactivated(self):
|
||||
print 'cashier_mode_deactivated'
|
||||
return
|
||||
|
||||
@http.route('/pos/open_cashbox', type='json', auth='admin')
|
||||
def open_cashbox(self):
|
||||
print 'open_cashbox'
|
||||
return
|
||||
|
||||
@http.route('/pos/print_receipt', type='json', auth='admin')
|
||||
def print_receipt(self, receipt):
|
||||
print 'print_receipt' + str(receipt)
|
||||
return
|
||||
|
||||
@http.route('/pos/log', type='json', auth='admin')
|
||||
def log(self, arguments):
|
||||
_logger.info(' '.join(str(v) for v in arguments))
|
||||
|
||||
@http.route('/pos/print_pdf_invoice', type='json', auth='admin')
|
||||
def print_pdf_invoice(self, pdfinvoice):
|
||||
print 'print_pdf_invoice' + str(pdfinvoice)
|
||||
return
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.point-of-sale .oe_hidden{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.point-of-sale ul, .point-of-sale li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 220 B |
@@ -43,6 +43,7 @@ function openerp_pos_db(instance, module){
|
||||
this.product_by_id = {};
|
||||
this.product_by_ean13 = {};
|
||||
this.product_by_category_id = {};
|
||||
this.product_by_reference = {};
|
||||
|
||||
this.category_by_id = {};
|
||||
this.root_category_id = 0;
|
||||
@@ -197,6 +198,9 @@ function openerp_pos_db(instance, module){
|
||||
if(product.ean13){
|
||||
this.product_by_ean13[product.ean13] = product;
|
||||
}
|
||||
if(product.default_code){
|
||||
this.product_by_reference[product.default_code] = product;
|
||||
}
|
||||
}
|
||||
},
|
||||
add_packagings: function(packagings){
|
||||
@@ -241,6 +245,9 @@ function openerp_pos_db(instance, module){
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
get_product_by_reference: function(ref){
|
||||
return this.product_by_reference[ref];
|
||||
},
|
||||
get_product_by_category: function(category_id){
|
||||
var product_ids = this.product_by_category_id[category_id];
|
||||
var list = [];
|
||||
|
||||
@@ -40,6 +40,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
});
|
||||
}else{
|
||||
running = false;
|
||||
scheduled_end_time = 0;
|
||||
end_of_queue.resolve();
|
||||
}
|
||||
};
|
||||
@@ -82,6 +83,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
|
||||
module.ProxyDevice = instance.web.Class.extend({
|
||||
init: function(options){
|
||||
var self = this;
|
||||
options = options || {};
|
||||
url = options.url || 'http://localhost:8069';
|
||||
|
||||
@@ -99,10 +101,13 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
};
|
||||
this.custom_payment_status = this.default_payment_status;
|
||||
|
||||
this.notifications = {};
|
||||
this.bypass_proxy = false;
|
||||
|
||||
this.connection = new instance.web.Session(undefined,url);
|
||||
this.connection.session_id = _.uniqueId('posproxy');
|
||||
this.bypass_proxy = false;
|
||||
this.notifications = {};
|
||||
this.test_connection();
|
||||
window.proxy = this;
|
||||
|
||||
},
|
||||
close: function(){
|
||||
@@ -113,7 +118,19 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
for(var i = 0; i < callbacks.length; i++){
|
||||
callbacks[i](params);
|
||||
}
|
||||
return this.connection.rpc('/pos/' + name, params || {});
|
||||
if(this.connected){
|
||||
return this.connection.rpc('/pos/' + name, params || {});
|
||||
}else{
|
||||
return (new $.Deferred()).reject();
|
||||
}
|
||||
},
|
||||
test_connection: function(){
|
||||
var self = this;
|
||||
this.connected = true;
|
||||
return this.message('test_connection').fail(function(){
|
||||
self.connected = false;
|
||||
console.error('Could not connect to the Proxy');
|
||||
});
|
||||
},
|
||||
|
||||
// this allows the client to be notified when a proxy call is made. The notification
|
||||
@@ -124,6 +141,8 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
}
|
||||
this.notifications[name].push(callback);
|
||||
},
|
||||
|
||||
|
||||
|
||||
//a product has been scanned and recognized with success
|
||||
// ean is a parsed ean object
|
||||
@@ -316,6 +335,11 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
return this.message('print_receipt',{receipt: receipt});
|
||||
},
|
||||
|
||||
// asks the proxy to log some information, as with the debug.log you can provide several arguments.
|
||||
log: function(){
|
||||
return this.message('log',{'arguments': _.toArray(arguments)});
|
||||
},
|
||||
|
||||
// asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server )
|
||||
print_pdf_invoice: function(pdfinvoice){
|
||||
return this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice});
|
||||
@@ -426,7 +450,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
// it will check its validity then return an object containing various
|
||||
// information about the ean.
|
||||
// most importantly :
|
||||
// - ean : the ean
|
||||
// - code : the ean
|
||||
// - type : the type of the ean:
|
||||
// 'price' | 'weight' | 'unit' | 'cashier' | 'client' | 'discount' | 'error'
|
||||
//
|
||||
@@ -436,13 +460,16 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
// - unit : if the encoded value has a unit, it will be put there.
|
||||
// not to be confused with the 'unit' type, which represent an unit of a
|
||||
// unique product
|
||||
// - base_code : the ean code with all the encoding parts set to zero; the one put on
|
||||
// the product in the backend
|
||||
|
||||
parse_ean: function(ean){
|
||||
var parse_result = {
|
||||
type:'unknown', //
|
||||
encoding: 'ean13',
|
||||
type:'unknown',
|
||||
prefix:'',
|
||||
ean:ean,
|
||||
base_ean: ean,
|
||||
code:ean,
|
||||
base_code: ean,
|
||||
id:'',
|
||||
value: 0,
|
||||
unit: 'none',
|
||||
@@ -463,13 +490,13 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
parse_result.type = 'error';
|
||||
} else if( match_prefix(this.price_prefix_set,'price')){
|
||||
parse_result.id = ean.substring(0,7);
|
||||
parse_result.base_ean = this.sanitize_ean(ean.substring(0,7));
|
||||
parse_result.base_code = this.sanitize_ean(ean.substring(0,7));
|
||||
parse_result.value = Number(ean.substring(7,12))/100.0;
|
||||
parse_result.unit = 'euro';
|
||||
} else if( match_prefix(this.weight_prefix_set,'weight')){
|
||||
parse_result.id = ean.substring(0,7);
|
||||
parse_result.value = Number(ean.substring(7,12))/1000.0;
|
||||
parse_result.base_ean = this.sanitize_ean(ean.substring(0,7));
|
||||
parse_result.base_code = this.sanitize_ean(ean.substring(0,7));
|
||||
parse_result.unit = 'Kg';
|
||||
} else if( match_prefix(this.client_prefix_set,'client')){
|
||||
parse_result.id = ean.substring(0,7);
|
||||
@@ -478,7 +505,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
parse_result.id = ean.substring(0,7);
|
||||
} else if( match_prefix(this.discount_prefix_set,'discount')){
|
||||
parse_result.id = ean.substring(0,7);
|
||||
parse_result.base_ean = this.sanitize_ean(ean.substring(0,7));
|
||||
parse_result.base_code = this.sanitize_ean(ean.substring(0,7));
|
||||
parse_result.value = Number(ean.substring(7,12))/100.0;
|
||||
parse_result.unit = '%';
|
||||
} else {
|
||||
@@ -488,9 +515,18 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
}
|
||||
return parse_result;
|
||||
},
|
||||
|
||||
on_ean: function(ean){
|
||||
var parse_result = this.parse_ean(ean);
|
||||
|
||||
scan: function(type,code){
|
||||
if (type === 'ean13'){
|
||||
var parse_result = this.parse_ean(code);
|
||||
}else if(type === 'reference'){
|
||||
var parse_result = {
|
||||
encoding: 'reference',
|
||||
type: 'unit',
|
||||
code: code,
|
||||
prefix: '',
|
||||
};
|
||||
}
|
||||
|
||||
if (parse_result.type === 'error') { //most likely a checksum error, raise warning
|
||||
console.warn('WARNING: barcode checksum error:',parse_result);
|
||||
@@ -498,7 +534,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
if(this.action_callback['product']){
|
||||
this.action_callback['product'](parse_result);
|
||||
}
|
||||
//this.trigger("codebar",parse_result );
|
||||
}else{
|
||||
if(this.action_callback[parse_result.type]){
|
||||
this.action_callback[parse_result.type](parse_result);
|
||||
@@ -506,49 +541,63 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
||||
}
|
||||
},
|
||||
|
||||
on_reference: function(code){
|
||||
if(this.action_callback['reference']){
|
||||
this.action_callback['reference'](code);
|
||||
}
|
||||
},
|
||||
|
||||
// starts catching keyboard events and tries to interpret codebar
|
||||
// calling the callbacks when needed.
|
||||
connect: function(){
|
||||
|
||||
var self = this;
|
||||
var codeNumbers = [];
|
||||
var timeStamp = 0;
|
||||
var lastTimeStamp = 0;
|
||||
var code = "";
|
||||
var timeStamp = 0;
|
||||
var onlynumbers = true;
|
||||
var timeout = null;
|
||||
|
||||
// The barcode readers acts as a keyboard, we catch all keyup events and try to find a
|
||||
// barcode sequence in the typed keys, then act accordingly.
|
||||
$('body').delegate('','keypress', function (e){
|
||||
//console.log('keyup:'+String.fromCharCode(e.keyCode)+' '+e.keyCode,e);
|
||||
//We only care about numbers
|
||||
if (e.keyCode >= 48 && e.keyCode < 58){
|
||||
this.handler = function(e){
|
||||
|
||||
// The barcode reader sends keystrokes with a specific interval.
|
||||
// We look if the typed keys fit in the interval.
|
||||
if (codeNumbers.length === 0) {
|
||||
timeStamp = new Date().getTime();
|
||||
} else {
|
||||
if (lastTimeStamp + 30 < new Date().getTime()) {
|
||||
// not a barcode reader
|
||||
codeNumbers = [];
|
||||
timeStamp = new Date().getTime();
|
||||
}
|
||||
}
|
||||
codeNumbers.push(e.keyCode - 48);
|
||||
lastTimeStamp = new Date().getTime();
|
||||
if (codeNumbers.length === 13) {
|
||||
//We have found what seems to be a valid codebar
|
||||
self.on_ean(codeNumbers.join(''));
|
||||
codeNumbers = [];
|
||||
}
|
||||
} else {
|
||||
// NaN
|
||||
codeNumbers = [];
|
||||
if(e.which === 13){ //ignore returns
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if(timeStamp + 50 < new Date().getTime()){
|
||||
code = "";
|
||||
onlynumbers = true;
|
||||
}
|
||||
|
||||
timeStamp = new Date().getTime();
|
||||
clearTimeout(timeout);
|
||||
|
||||
if( e.which < 48 || e.which >= 58 ){ // not a number
|
||||
onlynumbers = false;
|
||||
}
|
||||
|
||||
code += String.fromCharCode(e.which);
|
||||
|
||||
// we wait for a while after the last input to be sure that we are not mistakingly
|
||||
// returning a code which is a prefix of a bigger one :
|
||||
// Internal Ref 5449 vs EAN13 5449000...
|
||||
|
||||
timeout = setTimeout(function(){
|
||||
if(code.length === 13 && onlynumbers){
|
||||
self.scan('ean13',code);
|
||||
}else if(code.length >= 3 && self.pos.db.get_product_by_reference(code)){
|
||||
self.scan('reference',code);
|
||||
}
|
||||
code = "";
|
||||
onlynumbers = true;
|
||||
},100);
|
||||
};
|
||||
|
||||
$('body').on('keypress', this.handler);
|
||||
},
|
||||
|
||||
// stops catching keyboard events
|
||||
disconnect: function(){
|
||||
$('body').undelegate('', 'keyup')
|
||||
$('body').off('keypress', this.handler)
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ openerp.point_of_sale = function(instance) {
|
||||
openerp_pos_scrollbar(instance,module); // import pos_scrollbar_widget.js
|
||||
|
||||
openerp_pos_screens(instance,module); // import pos_screens.js
|
||||
|
||||
openerp_pos_devices(instance,module); // import pos_devices.js
|
||||
|
||||
openerp_pos_widgets(instance,module); // import pos_widgets.js
|
||||
|
||||
openerp_pos_devices(instance,module); // import pos_devices.js
|
||||
openerp_pos_tests(instance,module); // import pos_tests.js
|
||||
|
||||
instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
'pos_config': null,
|
||||
'units': null,
|
||||
'units_by_id': null,
|
||||
'pricelist': null,
|
||||
|
||||
'selectedOrder': null,
|
||||
});
|
||||
@@ -113,10 +114,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
}).then(function(company_partners){
|
||||
self.get('company').contact_address = company_partners[0].contact_address;
|
||||
|
||||
return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.get('company').currency_id[0]]]);
|
||||
}).then(function(currencies){
|
||||
self.set('currency',currencies[0]);
|
||||
|
||||
return self.fetch('product.uom', null, null);
|
||||
}).then(function(units){
|
||||
self.set('units',units);
|
||||
@@ -172,6 +169,14 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
}).then(function(shops){
|
||||
self.set('shop',shops[0]);
|
||||
|
||||
return self.fetch('product.pricelist',['currency_id'],[['id','=',self.get('pos_config').pricelist_id[0]]]);
|
||||
}).then(function(pricelists){
|
||||
self.set('pricelist',pricelists[0]);
|
||||
|
||||
return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.get('pricelist').currency_id[0]]]);
|
||||
}).then(function(currencies){
|
||||
self.set('currency',currencies[0]);
|
||||
|
||||
return self.fetch('product.packaging',['ean','product_id']);
|
||||
}).then(function(packagings){
|
||||
self.db.add_packagings(packagings);
|
||||
@@ -185,7 +190,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code',
|
||||
'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'],
|
||||
[['sale_ok','=',true],['available_in_pos','=',true]],
|
||||
{pricelist: self.get('pos_config').pricelist_id[0]} // context for price
|
||||
{pricelist: self.get('pricelist').id} // context for price
|
||||
);
|
||||
}).then(function(products){
|
||||
self.db.add_products(products);
|
||||
@@ -263,13 +268,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
//removes the current order
|
||||
delete_current_order: function(){
|
||||
this.get('selectedOrder').destroy({'reason':'abandon'});
|
||||
console.log('coucou!');
|
||||
},
|
||||
|
||||
// saves the order locally and try to send it to the backend.
|
||||
// it returns a deferred that succeeds after having tried to send the order and all the other pending orders.
|
||||
push_order: function(order) {
|
||||
var self = this;
|
||||
this.proxy.log('push_order',order.export_as_JSON());
|
||||
var order_id = this.db.add_order(order.export_as_JSON());
|
||||
var pushed = new $.Deferred();
|
||||
|
||||
@@ -423,19 +428,23 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
return tried_all;
|
||||
},
|
||||
|
||||
scan_product: function(parsed_ean){
|
||||
scan_product: function(parsed_code){
|
||||
var self = this;
|
||||
var product = this.db.get_product_by_ean13(parsed_ean.base_ean);
|
||||
var selectedOrder = this.get('selectedOrder');
|
||||
if(parsed_code.encoding === 'ean13'){
|
||||
var product = this.db.get_product_by_ean13(parsed_code.base_code);
|
||||
}else if(parsed_code.encoding === 'reference'){
|
||||
var product = this.db.get_product_by_reference(parsed_code.code);
|
||||
}
|
||||
|
||||
if(!product){
|
||||
return false;
|
||||
}
|
||||
|
||||
if(parsed_ean.type === 'price'){
|
||||
selectedOrder.addProduct(new module.Product(product), {price:parsed_ean.value});
|
||||
}else if(parsed_ean.type === 'weight'){
|
||||
selectedOrder.addProduct(new module.Product(product), {quantity:parsed_ean.value, merge:false});
|
||||
if(parsed_code.type === 'price'){
|
||||
selectedOrder.addProduct(new module.Product(product), {price:parsed_code.value});
|
||||
}else if(parsed_code.type === 'weight'){
|
||||
selectedOrder.addProduct(new module.Product(product), {quantity:parsed_code.value, merge:false});
|
||||
}else{
|
||||
selectedOrder.addProduct(new module.Product(product));
|
||||
}
|
||||
|
||||
@@ -160,29 +160,29 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
// what happens when a product is scanned :
|
||||
// it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if
|
||||
// there's an error.
|
||||
barcode_product_action: function(ean){
|
||||
barcode_product_action: function(code){
|
||||
var self = this;
|
||||
if(self.pos.scan_product(ean)){
|
||||
self.pos.proxy.scan_item_success(ean);
|
||||
if(self.pos.scan_product(code)){
|
||||
self.pos.proxy.scan_item_success(code);
|
||||
if(self.barcode_product_screen){
|
||||
self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
|
||||
}
|
||||
}else{
|
||||
self.pos.proxy.scan_item_error_unrecognized(ean);
|
||||
self.pos.proxy.scan_item_error_unrecognized(code);
|
||||
if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){
|
||||
self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// what happens when a cashier id barcode is scanned.
|
||||
// the default behavior is the following :
|
||||
// - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true
|
||||
// - else : do nothing and return false. You probably want to extend this to show and appropriate error popup...
|
||||
barcode_cashier_action: function(ean){
|
||||
barcode_cashier_action: function(code){
|
||||
var users = this.pos.get('user_list');
|
||||
for(var i = 0, len = users.length; i < len; i++){
|
||||
if(users[i].ean13 === ean.ean){
|
||||
if(users[i].ean13 === code.code){
|
||||
this.pos.set('cashier',users[i]);
|
||||
this.pos_widget.username.refresh();
|
||||
this.pos.proxy.cashier_mode_activated();
|
||||
@@ -190,7 +190,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this.pos.proxy.scan_item_error_unrecognized(ean);
|
||||
this.pos.proxy.scan_item_error_unrecognized(code);
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -198,28 +198,28 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
// the default behavior is the following :
|
||||
// - if there's a user with a matching ean, put it as the active 'client' and return true
|
||||
// - else : return false.
|
||||
barcode_client_action: function(ean){
|
||||
barcode_client_action: function(code){
|
||||
var partners = this.pos.get('partner_list');
|
||||
for(var i = 0, len = partners.length; i < len; i++){
|
||||
if(partners[i].ean13 === ean.ean){
|
||||
if(partners[i].ean13 === code.code){
|
||||
this.pos.get('selectedOrder').set_client(partners[i]);
|
||||
this.pos_widget.username.refresh();
|
||||
this.pos.proxy.scan_item_success(ean);
|
||||
this.pos.proxy.scan_item_success(code);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this.pos.proxy.scan_item_error_unrecognized(ean);
|
||||
this.pos.proxy.scan_item_error_unrecognized(code);
|
||||
return false;
|
||||
//TODO start the transaction
|
||||
},
|
||||
|
||||
// what happens when a discount barcode is scanned : the default behavior
|
||||
// is to set the discount on the last order.
|
||||
barcode_discount_action: function(ean){
|
||||
this.pos.proxy.scan_item_success(ean);
|
||||
barcode_discount_action: function(code){
|
||||
this.pos.proxy.scan_item_success(code);
|
||||
var last_orderline = this.pos.get('selectedOrder').getLastOrderline();
|
||||
if(last_orderline){
|
||||
last_orderline.set_discount(ean.value)
|
||||
last_orderline.set_discount(code.value)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -253,7 +253,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
|
||||
this.hidden = false;
|
||||
if(this.$el){
|
||||
this.$el.show();
|
||||
this.$el.removeClass('oe_hidden');
|
||||
}
|
||||
|
||||
if(this.pos_widget.action_bar.get_button_count() > 0){
|
||||
@@ -292,10 +292,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
|
||||
|
||||
this.pos.barcode_reader.set_action_callback({
|
||||
'cashier': self.barcode_cashier_action ? function(ean){ self.barcode_cashier_action(ean); } : undefined ,
|
||||
'product': self.barcode_product_action ? function(ean){ self.barcode_product_action(ean); } : undefined ,
|
||||
'client' : self.barcode_client_action ? function(ean){ self.barcode_client_action(ean); } : undefined ,
|
||||
'discount': self.barcode_discount_action ? function(ean){ self.barcode_discount_action(ean); } : undefined,
|
||||
'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
|
||||
'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
|
||||
'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined ,
|
||||
'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -314,7 +314,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
hide: function(){
|
||||
this.hidden = true;
|
||||
if(this.$el){
|
||||
this.$el.hide();
|
||||
this.$el.addClass('oe_hidden');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -326,7 +326,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
this._super();
|
||||
if(this.hidden){
|
||||
if(this.$el){
|
||||
this.$el.hide();
|
||||
this.$el.addClass('oe_hidden');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -335,7 +335,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
module.PopUpWidget = module.PosBaseWidget.extend({
|
||||
show: function(){
|
||||
if(this.$el){
|
||||
this.$el.show();
|
||||
this.$el.removeClass('oe_hidden');
|
||||
}
|
||||
},
|
||||
/* called before hide, when a popup is closed */
|
||||
@@ -345,7 +345,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
* pos instantiation, so you don't want to do anything fancy in here */
|
||||
hide: function(){
|
||||
if(this.$el){
|
||||
this.$el.hide();
|
||||
this.$el.addClass('oe_hidden');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -405,7 +405,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
this.pos.barcode_reader.save_callbacks();
|
||||
this.pos.barcode_reader.reset_action_callbacks();
|
||||
this.pos.barcode_reader.set_action_callback({
|
||||
'cashier': function(ean){
|
||||
'cashier': function(code){
|
||||
clearInterval(this.intervalID);
|
||||
self.pos.proxy.cashier_mode_activated();
|
||||
self.pos_widget.screen_selector.set_user_mode('cashier');
|
||||
@@ -455,7 +455,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
|
||||
queue.schedule(function(){
|
||||
return self.pos.proxy.weighting_start();
|
||||
},{ unclearable: true });
|
||||
},{ important: true });
|
||||
|
||||
queue.schedule(function(){
|
||||
return self.pos.proxy.weighting_read_kg().then(function(weight){
|
||||
@@ -479,7 +479,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
this.pos.proxy_queue.clear();
|
||||
this.pos.proxy_queue.schedule(function(){
|
||||
return self.pos.proxy.weighting_end();
|
||||
},{ unclearable: true });
|
||||
},{ important: true });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -516,7 +516,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
|
||||
queue.schedule(function(){
|
||||
return self.pos.proxy.weighting_start()
|
||||
},{ unclearable: true });
|
||||
},{ important: true });
|
||||
|
||||
queue.schedule(function(){
|
||||
return self.pos.proxy.weighting_read_kg().then(function(weight){
|
||||
@@ -542,8 +542,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
}
|
||||
},
|
||||
order_product: function(){
|
||||
var weight = this.pos.proxy.weighting_read_kg();
|
||||
this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity:weight });
|
||||
this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
|
||||
},
|
||||
get_product_name: function(){
|
||||
var product = this.get_product();
|
||||
@@ -567,7 +566,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
this.pos.proxy_queue.clear();
|
||||
this.pos.proxy_queue.schedule(function(){
|
||||
self.pos.proxy.weighting_end();
|
||||
},{ unclearable: true });
|
||||
},{ important: true });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -684,15 +683,15 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
|
||||
show_numpad: false,
|
||||
show_leftpane: false,
|
||||
barcode_product_action: function(ean){
|
||||
barcode_product_action: function(code){
|
||||
this.pos.proxy.transaction_start();
|
||||
this._super(ean);
|
||||
this._super(code);
|
||||
},
|
||||
|
||||
barcode_client_action: function(ean){
|
||||
barcode_client_action: function(code){
|
||||
this.pos.proxy.transaction_start();
|
||||
this._super(ean);
|
||||
$('.goodbye-message').hide();
|
||||
this._super(code);
|
||||
$('.goodbye-message').addClass('oe_hidden');
|
||||
this.pos_widget.screen_selector.show_popup('choose-receipt');
|
||||
},
|
||||
|
||||
@@ -704,14 +703,14 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
label: _t('Help'),
|
||||
icon: '/point_of_sale/static/src/img/icons/png48/help.png',
|
||||
click: function(){
|
||||
$('.goodbye-message').css({opacity:1}).hide();
|
||||
$('.goodbye-message').css({opacity:1}).addClass('oe_hidden');
|
||||
self.help_button_action();
|
||||
},
|
||||
});
|
||||
|
||||
$('.goodbye-message').css({opacity:1}).show();
|
||||
$('.goodbye-message').css({opacity:1}).removeClass('oe_hidden');
|
||||
setTimeout(function(){
|
||||
$('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').hide();});
|
||||
$('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').addClass('oe_hidden');});
|
||||
},5000);
|
||||
},
|
||||
});
|
||||
@@ -884,6 +883,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
label: _t('Back'),
|
||||
icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
|
||||
click: function(){
|
||||
_.each(self.paymentlinewidgets, function(widget){
|
||||
if( widget.payment_line.get_amount() === 0 ){
|
||||
widget.payment_line.destroy();
|
||||
}
|
||||
});
|
||||
self.pos_widget.screen_selector.set_current_screen(self.back_screen);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
function openerp_pos_tests(instance, module){ //module is instance.point_of_sale
|
||||
|
||||
// Various UI Tests to measure performance and memory leaks.
|
||||
module.UiTester = function(){
|
||||
var running = false;
|
||||
var queue = new module.JobQueue();
|
||||
|
||||
// stop the currently running test
|
||||
this.stop = function(){
|
||||
queue.clear();
|
||||
};
|
||||
|
||||
// randomly switch product categories
|
||||
this.category_switch = function(interval){
|
||||
queue.schedule(function(){
|
||||
var breadcrumbs = $('.breadcrumb a');
|
||||
var categories = $('li.category-button');
|
||||
if(categories.length > 0){
|
||||
var rnd = Math.floor(Math.random()*categories.length);
|
||||
categories.eq(rnd).click();
|
||||
}else{
|
||||
var rnd = Math.floor(Math.random()*breadcrumbs.length);
|
||||
breadcrumbs.eq(rnd).click();
|
||||
}
|
||||
},{repeat:true, duration:interval});
|
||||
};
|
||||
|
||||
// randomly order products then resets the order
|
||||
this.order_products = function(interval){
|
||||
|
||||
queue.schedule(function(){
|
||||
var def = new $.Deferred();
|
||||
var order_queue = new module.JobQueue();
|
||||
var order_size = 1 + Math.floor(Math.random()*10);
|
||||
|
||||
while(order_size--){
|
||||
order_queue.schedule(function(){
|
||||
var products = $('.product a');
|
||||
if(products.length > 0){
|
||||
var rnd = Math.floor(Math.random()*products.length);
|
||||
products.eq(rnd).click();
|
||||
}
|
||||
},{duration:250});
|
||||
}
|
||||
order_queue.finished().then(function(){
|
||||
$('.deleteorder-button').click();
|
||||
def.resolve();
|
||||
});
|
||||
return def;
|
||||
},{repeat:true, duration: interval});
|
||||
|
||||
};
|
||||
|
||||
// makes a complete product order cycle ( print via proxy must be activated, and scale deactivated )
|
||||
this.full_order_cycle = function(interval){
|
||||
queue.schedule(function(){
|
||||
var def = new $.Deferred();
|
||||
var order_queue = new module.JobQueue();
|
||||
var order_size = 1 + Math.floor(Math.random()*50);
|
||||
|
||||
while(order_size--){
|
||||
order_queue.schedule(function(){
|
||||
var products = $('.product a');
|
||||
if(products.length > 0){
|
||||
var rnd = Math.floor(Math.random()*products.length);
|
||||
products.eq(rnd).click();
|
||||
}
|
||||
},{duration:250});
|
||||
}
|
||||
order_queue.schedule(function(){
|
||||
$('.paypad-button:first').click();
|
||||
},{duration:250});
|
||||
order_queue.schedule(function(){
|
||||
$('.paymentline-amount input:first').val(10000);
|
||||
$('.paymentline-amount input:first').keyup();
|
||||
},{duration:250});
|
||||
order_queue.schedule(function(){
|
||||
$('.pos-actionbar-button-list .button:eq(2)').click();
|
||||
},{duration:250});
|
||||
order_queue.schedule(function(){
|
||||
def.resolve();
|
||||
});
|
||||
return def;
|
||||
},{repeat: true, duration: interval});
|
||||
};
|
||||
};
|
||||
|
||||
if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
|
||||
window.pos_test_ui = new module.UiTester();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,10 +41,10 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of
|
||||
|
||||
},
|
||||
show: function(){
|
||||
this.$el.show();
|
||||
this.$el.removeClass('oe_hidden');
|
||||
},
|
||||
hide: function(){
|
||||
this.$el.hide();
|
||||
this.$el.addClass('oe_hidden');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -43,10 +43,15 @@ function openerp_pos_keyboard(instance, module){ //module is instance.point_of_s
|
||||
|
||||
// Write a character to the input zone
|
||||
writeCharacter: function(character){
|
||||
var $input = this.$target
|
||||
$input[0].value += character;
|
||||
$input.keydown();
|
||||
$input.keyup();
|
||||
var $input = this.$target;
|
||||
if(character === '\n'){
|
||||
$input.trigger($.Event('keydown',{which:13}));
|
||||
$input.trigger($.Event('keyup',{which:13}));
|
||||
}else{
|
||||
$input[0].value += character;
|
||||
$input.keydown();
|
||||
$input.keyup();
|
||||
}
|
||||
},
|
||||
|
||||
// Sends a 'return' character to the input zone. TODO
|
||||
|
||||
@@ -81,7 +81,7 @@ function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_
|
||||
$(window).unbind('resize',this.resize_handler);
|
||||
$(window).bind('resize',this.resize_handler);
|
||||
|
||||
this.target().unbind('mousewheel',this.target_mousweheel_handler);
|
||||
this.target().unbind('mousewheel',this.target_mousewheel_handler);
|
||||
this.target().bind('mousewheel',this.target_mousewheel_handler);
|
||||
|
||||
// because the rendering is asynchronous we must wait for the next javascript update
|
||||
@@ -93,22 +93,18 @@ function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_
|
||||
},0);
|
||||
},
|
||||
|
||||
// binds the window resize and the target scrolling events.
|
||||
// it is good advice not to bind these multiple_times
|
||||
bind_events:function(){
|
||||
$(window).resize(function(){
|
||||
});
|
||||
this.target().bind('mousewheel',function(event,delta){
|
||||
self.scroll(delta*self.wheel_step);
|
||||
});
|
||||
destroy: function(){
|
||||
$(window).unbind('resize',this.resize_handler);
|
||||
this.target().unbind('mousewheel',this.target_mousewheel_handler);
|
||||
this._super();
|
||||
},
|
||||
|
||||
// shows the scrollbar. if animated is true, it will do it in an animated fashion
|
||||
show: function(animated){ //FIXME: animated show and hide don't work ... ?
|
||||
if(animated){
|
||||
this.$el.show().animate({'width':'48px'}, 500, 'swing');
|
||||
this.$el.removeClass('oe_hidden').animate({'width':'48px'}, 500, 'swing');
|
||||
}else{
|
||||
this.$el.show().css('width','48px');
|
||||
this.$el.removeClass('oe_hidden').css('width','48px');
|
||||
}
|
||||
this.on_show(this);
|
||||
},
|
||||
@@ -117,9 +113,9 @@ function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_
|
||||
hide: function(animated){
|
||||
var self = this;
|
||||
if(animated){
|
||||
this.$el.animate({'width':'0px'}, 500, 'swing', function(){ self.$el.hide();});
|
||||
this.$el.animate({'width':'0px'}, 500, 'swing', function(){ self.$el.addClass('oe_hidden');});
|
||||
}else{
|
||||
this.$el.hide().css('width','0px');
|
||||
this.$el.addClass('oe_hidden').css('width','0px');
|
||||
}
|
||||
this.on_hide(this);
|
||||
},
|
||||
|
||||
@@ -137,17 +137,15 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
this.model = options.model;
|
||||
this.order = options.order;
|
||||
|
||||
this.model.bind('change', _.bind( function() {
|
||||
this.refresh();
|
||||
}, this));
|
||||
},
|
||||
click_handler: function() {
|
||||
this.order.selectLine(this.model);
|
||||
this.trigger('order_line_selected');
|
||||
this.model.bind('change', this.refresh, this);
|
||||
},
|
||||
renderElement: function() {
|
||||
var self = this;
|
||||
this._super();
|
||||
this.$el.click(_.bind(this.click_handler, this));
|
||||
this.$el.click(function(){
|
||||
self.order.selectLine(this.model);
|
||||
self.trigger('order_line_selected');
|
||||
});
|
||||
if(this.model.is_selected()){
|
||||
this.$el.addClass('selected');
|
||||
}
|
||||
@@ -156,6 +154,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
this.renderElement();
|
||||
this.trigger('order_line_refreshed');
|
||||
},
|
||||
destroy: function(){
|
||||
this.model.unbind('change',this.refresh,this);
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
module.OrderWidget = module.PosBaseWidget.extend({
|
||||
@@ -281,27 +283,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
},
|
||||
});
|
||||
|
||||
module.ProductWidget = module.PosBaseWidget.extend({
|
||||
template: 'ProductWidget',
|
||||
init: function(parent, options) {
|
||||
this._super(parent,options);
|
||||
this.model = options.model;
|
||||
this.model.attributes.weight = options.weight;
|
||||
this.next_screen = options.next_screen; //when a product is clicked, this screen is set
|
||||
this.click_product_action = options.click_product_action;
|
||||
},
|
||||
// returns the url of the product thumbnail
|
||||
renderElement: function() {
|
||||
this._super();
|
||||
this.$('img').replaceWith(this.pos_widget.image_cache.get_image(this.model.get_image_url()));
|
||||
var self = this;
|
||||
$("a", this.$el).click(function(e){
|
||||
if(self.click_product_action){
|
||||
self.click_product_action(self.model);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
module.PaymentlineWidget = module.PosBaseWidget.extend({
|
||||
template: 'PaymentlineWidget',
|
||||
@@ -358,16 +339,16 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
var self = this;
|
||||
|
||||
this.order = options.order;
|
||||
this.order.bind('destroy',function(){ self.destroy(); });
|
||||
this.order.bind('change', function(){ self.renderElement(); });
|
||||
this.pos.bind('change:selectedOrder', function() {
|
||||
self.renderElement();
|
||||
}, this);
|
||||
this.order.bind('destroy',this.destroy, this );
|
||||
this.order.bind('change', this.renderElement, this );
|
||||
this.pos.bind('change:selectedOrder', this.renderElement,this );
|
||||
},
|
||||
renderElement:function(){
|
||||
this._super();
|
||||
this.$('button.select-order').off('click').click(_.bind(this.selectOrder, this));
|
||||
this.$('button.close-order').off('click').click(_.bind(this.closeOrder, this));
|
||||
var self = this;
|
||||
this.$el.click(function(){
|
||||
self.selectOrder();
|
||||
});
|
||||
if( this.order === this.pos.get('selectedOrder') ){
|
||||
this.$el.addClass('selected-order');
|
||||
}
|
||||
@@ -377,8 +358,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
selectedOrder: this.order
|
||||
});
|
||||
},
|
||||
closeOrder: function(event) {
|
||||
this.order.destroy();
|
||||
destroy: function(){
|
||||
this.order.unbind('destroy', this.destroy, this);
|
||||
this.order.unbind('change', this.renderElement, this);
|
||||
this.pos.unbind('change:selectedOrder', this.renderElement, this);
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -422,9 +406,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
if(visible != this.visibility[element]){
|
||||
this.visibility[element] = !!visible;
|
||||
if(visible){
|
||||
this.$('.'+element).show();
|
||||
this.$('.'+element).removeClass('oe_hidden');
|
||||
}else{
|
||||
this.$('.'+element).hide();
|
||||
this.$('.'+element).addClass('oe_hidden');
|
||||
}
|
||||
}
|
||||
if(visible && action){
|
||||
@@ -459,10 +443,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
return button;
|
||||
},
|
||||
show:function(){
|
||||
this.$el.show();
|
||||
this.$el.removeClass('oe_hidden');
|
||||
},
|
||||
hide:function(){
|
||||
this.$el.hide();
|
||||
this.$el.addClass('oe_hidden');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -532,7 +516,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
var cat = self.pos.db.get_category_by_id(id);
|
||||
self.set_category(cat);
|
||||
self.renderElement();
|
||||
self.search_and_categories(cat);
|
||||
});
|
||||
});
|
||||
// breadcrumb click actions
|
||||
@@ -541,12 +524,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
var category = self.pos.db.get_category_by_id(id);
|
||||
self.set_category(category);
|
||||
self.renderElement();
|
||||
self.search_and_categories(category);
|
||||
});
|
||||
|
||||
this.search_and_categories();
|
||||
|
||||
if(this.pos.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
|
||||
this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input'));
|
||||
}
|
||||
this.search_and_categories();
|
||||
},
|
||||
|
||||
set_product_type: function(type){ // 'all' | 'weightable'
|
||||
@@ -558,7 +542,14 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
reset_category: function(){
|
||||
this.set_category();
|
||||
this.renderElement();
|
||||
this.search_and_categories();
|
||||
},
|
||||
|
||||
// empties the content of the search box
|
||||
clear_search: function(){
|
||||
var products = this.pos.db.get_product_by_category(this.category.id);
|
||||
this.pos.get('products').reset(products);
|
||||
this.$('.searchbox input').val('').focus();
|
||||
this.$('.search-clear').fadeOut();
|
||||
},
|
||||
|
||||
// filters the products, and sets up the search callbacks
|
||||
@@ -570,12 +561,20 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
self.pos.get('products').reset(products);
|
||||
|
||||
// filter the products according to the search string
|
||||
this.$('.searchbox input').keyup(function(){
|
||||
this.$('.searchbox input').keyup(function(event){
|
||||
console.log('event',event);
|
||||
query = $(this).val().toLowerCase();
|
||||
if(query){
|
||||
var products = self.pos.db.search_product_in_category(self.category.id, query);
|
||||
self.pos.get('products').reset(products);
|
||||
self.$('.search-clear').fadeIn();
|
||||
if(event.which === 13){
|
||||
if( self.pos.get('products').size() === 1 ){
|
||||
self.pos.get('selectedOrder').addProduct(self.pos.get('products').at(0));
|
||||
self.clear_search();
|
||||
}
|
||||
}else{
|
||||
var products = self.pos.db.search_product_in_category(self.category.id, query);
|
||||
self.pos.get('products').reset(products);
|
||||
self.$('.search-clear').fadeIn();
|
||||
}
|
||||
}else{
|
||||
var products = self.pos.db.get_product_by_category(self.category.id);
|
||||
self.pos.get('products').reset(products);
|
||||
@@ -583,14 +582,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
}
|
||||
});
|
||||
|
||||
this.$('.searchbox input').click(function(){}); //Why ???
|
||||
|
||||
//reset the search when clicking on reset
|
||||
this.$('.search-clear').click(function(){
|
||||
var products = self.pos.db.get_product_by_category(self.category.id);
|
||||
self.pos.get('products').reset(products);
|
||||
self.$('.searchbox input').val('').focus();
|
||||
self.$('.search-clear').fadeOut();
|
||||
self.clear_search();
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -614,25 +608,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
renderElement: function() {
|
||||
var self = this;
|
||||
this._super();
|
||||
|
||||
// free subwidgets memory from previous renders
|
||||
|
||||
for(var i = 0, len = this.productwidgets.length; i < len; i++){
|
||||
this.productwidgets[i].destroy();
|
||||
}
|
||||
this.productwidgets = [];
|
||||
if(this.scrollbar){
|
||||
this.scrollbar.destroy();
|
||||
}
|
||||
var products = this.pos.get('products').models || [];
|
||||
for(var i = 0, len = products.length; i < len; i++){
|
||||
var product = new module.ProductWidget(self, {
|
||||
model: products[i],
|
||||
click_product_action: this.click_product_action,
|
||||
});
|
||||
this.productwidgets.push(product);
|
||||
product.appendTo(this.$('.product-list'));
|
||||
}
|
||||
|
||||
_.each(products,function(product,i){
|
||||
var $product = $(QWeb.render('Product',{ widget:self, product: products[i] }));
|
||||
$product.find('img').replaceWith(self.pos_widget.image_cache.get_image(products[i].get_image_url()));
|
||||
$product.find('a').click(function(){ self.click_product_action(product); });
|
||||
$product.appendTo(self.$('.product-list'));
|
||||
});
|
||||
|
||||
this.scrollbar = new module.ScrollbarWidget(this,{
|
||||
target_widget: this,
|
||||
target_selector: '.product-list-scroller',
|
||||
@@ -690,11 +678,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
var self = this;
|
||||
this._super();
|
||||
if(this.action){
|
||||
this.$el.click(function(){ self.action(); });
|
||||
this.$el.click(function(){
|
||||
self.action();
|
||||
});
|
||||
}
|
||||
},
|
||||
show: function(){ this.$el.show(); },
|
||||
hide: function(){ this.$el.hide(); },
|
||||
show: function(){ this.$el.removeClass('oe_hidden'); },
|
||||
hide: function(){ this.$el.addClass('oe_hidden'); },
|
||||
});
|
||||
|
||||
// The debug widget lets the user control and monitor the hardware and software status
|
||||
@@ -754,12 +744,15 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
this.$('.button.custom_ean').click(function(){
|
||||
var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
|
||||
self.$('input.ean').val(ean);
|
||||
self.pos.barcode_reader.on_ean(ean);
|
||||
self.pos.barcode_reader.scan('ean13',ean);
|
||||
});
|
||||
this.$('.button.reference').click(function(){
|
||||
self.pos.barcode_reader.scan('reference',self.$('input.ean').val());
|
||||
});
|
||||
_.each(this.eans, function(ean, name){
|
||||
self.$('.button.'+name).click(function(){
|
||||
self.$('input.ean').val(ean);
|
||||
self.pos.barcode_reader.on_ean(ean);
|
||||
self.pos.barcode_reader.scan('ean13',ean);
|
||||
});
|
||||
});
|
||||
_.each(this.events, function(name){
|
||||
@@ -873,13 +866,12 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
new_order_button.selectOrder();
|
||||
}, self);
|
||||
|
||||
self.pos.get('orders').add(new module.Order({ pos: self.pos }));
|
||||
self.pos.add_new_order();
|
||||
|
||||
self.build_widgets();
|
||||
|
||||
self.screen_selector.set_default_screen();
|
||||
|
||||
window.screen_selector = self.screen_selector;
|
||||
|
||||
self.pos.barcode_reader.connect();
|
||||
|
||||
@@ -892,7 +884,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
}
|
||||
|
||||
instance.web.unblockUI();
|
||||
self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').hide();});
|
||||
self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
|
||||
|
||||
self.pos.flush();
|
||||
|
||||
@@ -1081,11 +1073,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
if(visible !== this.leftpane_visible){
|
||||
this.leftpane_visible = visible;
|
||||
if(visible){
|
||||
$('#leftpane').show().animate({'width':this.leftpane_width},500,'swing');
|
||||
$('#leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing');
|
||||
$('#rightpane').animate({'left':this.leftpane_width},500,'swing');
|
||||
}else{
|
||||
var leftpane = $('#leftpane');
|
||||
$('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.hide(); });
|
||||
$('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); });
|
||||
$('#rightpane').animate({'left':'0px'},500,'swing');
|
||||
}
|
||||
}
|
||||
@@ -1095,25 +1087,39 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
if(visible !== this.cashier_controls_visible){
|
||||
this.cashier_controls_visible = visible;
|
||||
if(visible){
|
||||
$('#loggedas').show();
|
||||
$('#rightheader').show();
|
||||
$('#loggedas').removeClass('oe_hidden');
|
||||
$('#rightheader').removeClass('oe_hidden');
|
||||
}else{
|
||||
$('#loggedas').hide();
|
||||
$('#rightheader').hide();
|
||||
$('#loggedas').addClass('oe_hidden');
|
||||
$('#rightheader').addClass('oe_hidden');
|
||||
}
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
var self = this;
|
||||
return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(
|
||||
_.bind(function(res) {
|
||||
return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
|
||||
var action = result;
|
||||
action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}});
|
||||
//self.destroy();
|
||||
this.do_action(action);
|
||||
}, this));
|
||||
}, this));
|
||||
|
||||
function close(){
|
||||
return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(
|
||||
_.bind(function(res) {
|
||||
return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
|
||||
var action = result;
|
||||
action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}});
|
||||
//self.destroy();
|
||||
this.do_action(action);
|
||||
}, this));
|
||||
}, self));
|
||||
}
|
||||
|
||||
var draft_order = _.find( self.pos.get('orders').models, function(order){
|
||||
return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0;
|
||||
});
|
||||
if(draft_order){
|
||||
if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) {
|
||||
return close();
|
||||
}
|
||||
}else{
|
||||
return close();
|
||||
}
|
||||
},
|
||||
destroy: function() {
|
||||
this.pos.destroy();
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
</div>
|
||||
<div id="rightheader">
|
||||
<div id="order-selector">
|
||||
<button class="neworder-button square">➕</button>
|
||||
<button class="deleteorder-button square">➖</button>
|
||||
<button class="neworder-button square"><img src='/point_of_sale/static/src/img/plus.png' /></button>
|
||||
<button class="deleteorder-button square"><img src='/point_of_sale/static/src/img/minus.png' /></button>
|
||||
<ol id="orders"></ol>
|
||||
</div>
|
||||
<!-- here goes header buttons -->
|
||||
@@ -399,24 +399,24 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="ProductWidget">
|
||||
<t t-name="Product">
|
||||
<li class='product'>
|
||||
<a href="#">
|
||||
<div class="product-img">
|
||||
<img src='' /> <!-- the product thumbnail -->
|
||||
<t t-if="!widget.model.get('to_weight')">
|
||||
<t t-if="!product.get('to_weight')">
|
||||
<span class="price-tag">
|
||||
<t t-esc="widget.format_currency(widget.model.get('price'))"/>
|
||||
<t t-esc="widget.format_currency(product.get('price'))"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="widget.model.get('to_weight')">
|
||||
<t t-if="product.get('to_weight')">
|
||||
<span class="price-tag">
|
||||
<t t-esc="widget.format_currency(widget.model.get('price'))+'/Kg'"/>
|
||||
<t t-esc="widget.format_currency(product.get('price'))+'/Kg'"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="product-name">
|
||||
<t t-esc="widget.model.get('name')"/>
|
||||
<t t-esc="product.get('name')"/>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -494,6 +494,7 @@
|
||||
<li class="button lemon_price">1.54€ Lemon</li>
|
||||
<li class="button unknown_product">Unknown Product</li>
|
||||
<li class="button invalid_ean">Invalid Ean</li>
|
||||
<li class="button reference">Reference</li>
|
||||
</ul>
|
||||
|
||||
<p class="category">Hardware Status</p>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<field name="pos_state" invisible="1" />
|
||||
</group>
|
||||
<field name="pos_session_id" invisible="1"/>
|
||||
<button name="open_ui" type="object" string="Start Selling"
|
||||
<button name="open_ui" type="object" string="Resume Session"
|
||||
attrs="{'invisible' : [('pos_state', 'not in', ('opened',))]}"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.addons.mail.tests.test_mail_base import TestMailBase
|
||||
from openerp.addons.mail.tests.common import TestMail
|
||||
from openerp.osv.orm import except_orm
|
||||
from openerp.tools.misc import mute_logger
|
||||
|
||||
|
||||
class test_portal(TestMailBase):
|
||||
class test_portal(TestMail):
|
||||
|
||||
def setUp(self):
|
||||
super(test_portal, self).setUp()
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Croatian 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 13:17+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Croatian <hr@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: portal_anonymous
|
||||
#. openerp-web
|
||||
#: code:addons/portal_anonymous/static/src/xml/portal_anonymous.xml:8
|
||||
#, python-format
|
||||
msgid "Login"
|
||||
msgstr "Prijava"
|
||||
@@ -0,0 +1,554 @@
|
||||
# Russian translation for openobject-addons
|
||||
# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013
|
||||
# This file is distributed under the same license as the openobject-addons package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2013.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: openobject-addons\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2012-12-21 17:06+0000\n"
|
||||
"PO-Revision-Date: 2013-09-16 07:59+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Russian <ru@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2013-09-17 04:37+0000\n"
|
||||
"X-Generator: Launchpad (build 16765)\n"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.model,name:portal_sale.model_account_config_settings
|
||||
msgid "account.config.settings"
|
||||
msgstr ""
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.actions.act_window,help:portal_sale.portal_action_invoices
|
||||
msgid "We haven't sent you any invoice."
|
||||
msgstr "У Вас нет никаких счетов."
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:email.template,report_name:portal_sale.email_template_edi_sale
|
||||
msgid ""
|
||||
"${(object.name or '').replace('/','_')}_${object.state == 'draft' and "
|
||||
"'draft' or ''}"
|
||||
msgstr ""
|
||||
"${(object.name or '').replace('/','_')}_${object.state == 'draft' and "
|
||||
"'проект' or ''}"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:res.groups,name:portal_sale.group_payment_options
|
||||
msgid "View Online Payment Options"
|
||||
msgstr "Посмотреть варианты оплаты онлайн"
|
||||
|
||||
#. module: portal_sale
|
||||
#: field:account.config.settings,group_payment_options:0
|
||||
msgid "Show payment buttons to employees too"
|
||||
msgstr "Показывать кнопки оплаты сотрудникам"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:email.template,subject:portal_sale.email_template_edi_sale
|
||||
msgid ""
|
||||
"${object.company_id.name} ${object.state in ('draft', 'sent') and "
|
||||
"'Quotation' or 'Order'} (Ref ${object.name or 'n/a' })"
|
||||
msgstr ""
|
||||
"${object.company_id.name} ${object.state in ('draft', 'sent') and "
|
||||
"'Предложение' or 'Заказ'} № ${object.name or 'б/н' }"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.actions.act_window,help:portal_sale.action_quotations_portal
|
||||
msgid "We haven't sent you any quotation."
|
||||
msgstr "У Вас нет никаких предложений цен"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.ui.menu,name:portal_sale.portal_sales_orders
|
||||
msgid "Sales Orders"
|
||||
msgstr "Заказы продаж"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:res.groups,comment:portal_sale.group_payment_options
|
||||
msgid ""
|
||||
"Members of this group see the online payment options\n"
|
||||
"on Sale Orders and Customer Invoices. These options are meant for customers "
|
||||
"who are accessing\n"
|
||||
"their documents through the portal."
|
||||
msgstr ""
|
||||
"Члены этой группы видят варианты оплаты счетов онлайн\n"
|
||||
"по заказам продаж и счетам заказчиков. Эти варианты предназначены для "
|
||||
"заказчиков, имеющих доступ\n"
|
||||
"к своим документам через портал."
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:email.template,body_html:portal_sale.email_template_edi_sale
|
||||
msgid ""
|
||||
"\n"
|
||||
"<div style=\"font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-"
|
||||
"serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, "
|
||||
"255, 255); \">\n"
|
||||
"\n"
|
||||
" <p>Hello ${object.partner_id.name},</p>\n"
|
||||
" \n"
|
||||
" <p>Here is your ${object.state in ('draft', 'sent') and 'quotation' or "
|
||||
"'order confirmation'} from ${object.company_id.name}: </p>\n"
|
||||
"\n"
|
||||
" <p style=\"border-left: 1px solid #8e0000; margin-left: 30px;\">\n"
|
||||
" <strong>REFERENCES</strong><br />\n"
|
||||
" Order number: <strong>${object.name}</strong><br />\n"
|
||||
" Order total: <strong>${object.amount_total} "
|
||||
"${object.pricelist_id.currency_id.name}</strong><br />\n"
|
||||
" Order date: ${object.date_order}<br />\n"
|
||||
" % if object.origin:\n"
|
||||
" Order reference: ${object.origin}<br />\n"
|
||||
" % endif\n"
|
||||
" % if object.client_order_ref:\n"
|
||||
" Your reference: ${object.client_order_ref}<br />\n"
|
||||
" % endif\n"
|
||||
" % if object.user_id:\n"
|
||||
" Your contact: <a href=\"mailto:${object.user_id.email or "
|
||||
"''}?subject=Order%20${object.name}\">${object.user_id.name}</a>\n"
|
||||
" % endif\n"
|
||||
" </p>\n"
|
||||
"\n"
|
||||
" <% set signup_url = object.get_signup_url() %>\n"
|
||||
" % if signup_url:\n"
|
||||
" <p>\n"
|
||||
" You can access this document and pay online via our Customer Portal:\n"
|
||||
" </p>\n"
|
||||
" <a style=\"display:block; width: 150px; height:20px; margin-left: "
|
||||
"120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-"
|
||||
"serif; font-size: 13px; font-weight: bold; text-align: center; text-"
|
||||
"decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; "
|
||||
"background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat "
|
||||
"no-repeat;\"\n"
|
||||
" href=\"${signup_url}\">View ${object.state in ('draft', 'sent') "
|
||||
"and 'Quotation' or 'Order'}</a>\n"
|
||||
" % endif\n"
|
||||
"\n"
|
||||
" % if object.paypal_url:\n"
|
||||
" <br/>\n"
|
||||
" <p>It is also possible to directly pay with Paypal:</p>\n"
|
||||
" <a style=\"margin-left: 120px;\" href=\"${object.paypal_url}\">\n"
|
||||
" <img class=\"oe_edi_paypal_button\" "
|
||||
"src=\"https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif\"/>\n"
|
||||
" </a>\n"
|
||||
" % endif\n"
|
||||
"\n"
|
||||
" <br/>\n"
|
||||
" <p>If you have any question, do not hesitate to contact us.</p>\n"
|
||||
" <p>Thank you for choosing ${object.company_id.name or 'us'}!</p>\n"
|
||||
" <br/>\n"
|
||||
" <br/>\n"
|
||||
" <div style=\"width: 375px; margin: 0px; padding: 0px; background-color: "
|
||||
"#8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; "
|
||||
"background-repeat: repeat no-repeat;\">\n"
|
||||
" <h3 style=\"margin: 0px; padding: 2px 14px; font-size: 12px; color: "
|
||||
"#DDD;\">\n"
|
||||
" <strong style=\"text-"
|
||||
"transform:uppercase;\">${object.company_id.name}</strong></h3>\n"
|
||||
" </div>\n"
|
||||
" <div style=\"width: 347px; margin: 0px; padding: 5px 14px; line-height: "
|
||||
"16px; background-color: #F2F2F2;\">\n"
|
||||
" <span style=\"color: #222; margin-bottom: 5px; display: block; \">\n"
|
||||
" % if object.company_id.street:\n"
|
||||
" ${object.company_id.street}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.street2:\n"
|
||||
" ${object.company_id.street2}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.city or object.company_id.zip:\n"
|
||||
" ${object.company_id.zip} ${object.company_id.city}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.country_id:\n"
|
||||
" ${object.company_id.state_id and ('%s, ' % "
|
||||
"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name "
|
||||
"or ''}<br/>\n"
|
||||
" % endif\n"
|
||||
" </span>\n"
|
||||
" % if object.company_id.phone:\n"
|
||||
" <div style=\"margin-top: 0px; margin-right: 0px; margin-bottom: "
|
||||
"0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: "
|
||||
"0px; padding-left: 0px; \">\n"
|
||||
" Phone: ${object.company_id.phone}\n"
|
||||
" </div>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.website:\n"
|
||||
" <div>\n"
|
||||
" Web : <a "
|
||||
"href=\"${object.company_id.website}\">${object.company_id.website}</a>\n"
|
||||
" </div>\n"
|
||||
" % endif\n"
|
||||
" <p></p>\n"
|
||||
" </div>\n"
|
||||
"</div>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"<div style=\"font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-"
|
||||
"serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, "
|
||||
"255, 255); \">\n"
|
||||
"\n"
|
||||
" <p>Здравствуйте, ${object.partner_id.name}!</p>\n"
|
||||
" \n"
|
||||
" <p>Направляем Вам ${object.state in ('draft', 'sent') and 'предложение' "
|
||||
"or 'подтверждение заказа'} от ${object.company_id.name}: </p>\n"
|
||||
"\n"
|
||||
" <p style=\"border-left: 1px solid #8e0000; margin-left: 30px;\">\n"
|
||||
" <strong>СВОДКА</strong><br />\n"
|
||||
" Заказ: <strong>${object.name}</strong><br />\n"
|
||||
" Итог: <strong>${object.amount_total} "
|
||||
"${object.pricelist_id.currency_id.name}</strong><br />\n"
|
||||
" Дата: ${object.date_order}<br />\n"
|
||||
" % if object.origin:\n"
|
||||
" Основание заказа: ${object.origin}<br />\n"
|
||||
" % endif\n"
|
||||
" % if object.client_order_ref:\n"
|
||||
" Ваша ссылка: ${object.client_order_ref}<br />\n"
|
||||
" % endif\n"
|
||||
" % if object.user_id:\n"
|
||||
" Ваш контакт: <a href=\"mailto:${object.user_id.email or "
|
||||
"''}?subject=Order%20${object.name}\">${object.user_id.name}</a>\n"
|
||||
" % endif\n"
|
||||
" </p>\n"
|
||||
"\n"
|
||||
" <% set signup_url = object.get_signup_url() %>\n"
|
||||
" % if signup_url:\n"
|
||||
" <p>\n"
|
||||
" Вы можете ознакомиться с этим документом онлайн и узнать о вариантах "
|
||||
"оплаты через наш Портал:\n"
|
||||
" </p>\n"
|
||||
" <a style=\"display:block; width: 150px; height:20px; margin-left: "
|
||||
"120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-"
|
||||
"serif; font-size: 13px; font-weight: bold; text-align: center; text-"
|
||||
"decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; "
|
||||
"background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat "
|
||||
"no-repeat;\"\n"
|
||||
" href=\"${signup_url}\">Посмотреть ${object.state in ('draft', "
|
||||
"'sent') and 'Предложение' or 'Заказ'}</a>\n"
|
||||
" % endif\n"
|
||||
"\n"
|
||||
" % if object.paypal_url:\n"
|
||||
" <br/>\n"
|
||||
" <p>Также возможно произвести оплату непосредственно через Paypal:</p>\n"
|
||||
" <a style=\"margin-left: 120px;\" href=\"${object.paypal_url}\">\n"
|
||||
" <img class=\"oe_edi_paypal_button\" "
|
||||
"src=\"https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif\"/>\n"
|
||||
" </a>\n"
|
||||
" % endif\n"
|
||||
"\n"
|
||||
" <br/>\n"
|
||||
" <p>Если у Вас есть вопросы - пожалуйста, свяжитесь с нами.</p>\n"
|
||||
" <p>Спасибо, что выбрали ${object.company_id.name or 'нас'}!</p>\n"
|
||||
" <br/>\n"
|
||||
" <br/>\n"
|
||||
" <div style=\"width: 375px; margin: 0px; padding: 0px; background-color: "
|
||||
"#8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; "
|
||||
"background-repeat: repeat no-repeat;\">\n"
|
||||
" <h3 style=\"margin: 0px; padding: 2px 14px; font-size: 12px; color: "
|
||||
"#DDD;\">\n"
|
||||
" <strong style=\"text-"
|
||||
"transform:uppercase;\">${object.company_id.name}</strong></h3>\n"
|
||||
" </div>\n"
|
||||
" <div style=\"width: 347px; margin: 0px; padding: 5px 14px; line-height: "
|
||||
"16px; background-color: #F2F2F2;\">\n"
|
||||
" <span style=\"color: #222; margin-bottom: 5px; display: block; \">\n"
|
||||
" % if object.company_id.street:\n"
|
||||
" ${object.company_id.street}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.street2:\n"
|
||||
" ${object.company_id.street2}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.city or object.company_id.zip:\n"
|
||||
" ${object.company_id.zip} ${object.company_id.city}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.country_id:\n"
|
||||
" ${object.company_id.state_id and ('%s, ' % "
|
||||
"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name "
|
||||
"or ''}<br/>\n"
|
||||
" % endif\n"
|
||||
" </span>\n"
|
||||
" % if object.company_id.phone:\n"
|
||||
" <div style=\"margin-top: 0px; margin-right: 0px; margin-bottom: "
|
||||
"0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: "
|
||||
"0px; padding-left: 0px; \">\n"
|
||||
" Phone: ${object.company_id.phone}\n"
|
||||
" </div>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.website:\n"
|
||||
" <div>\n"
|
||||
" Сайт : <a "
|
||||
"href=\"${object.company_id.website}\">${object.company_id.website}</a>\n"
|
||||
" </div>\n"
|
||||
" % endif\n"
|
||||
" <p></p>\n"
|
||||
" </div>\n"
|
||||
"</div>\n"
|
||||
" "
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:email.template,report_name:portal_sale.email_template_edi_invoice
|
||||
msgid ""
|
||||
"Invoice_${(object.number or '').replace('/','_')}_${object.state == 'draft' "
|
||||
"and 'draft' or ''}"
|
||||
msgstr ""
|
||||
"Счёт_${(object.number or '').replace('/','_')}_${object.state == 'draft' and "
|
||||
"'проект' or ''}"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:email.template,subject:portal_sale.email_template_edi_invoice
|
||||
msgid "${object.company_id.name} Invoice (Ref ${object.number or 'n/a' })"
|
||||
msgstr "${object.company_id.name} Счёт № ${object.number or 'n/a' }"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.model,name:portal_sale.model_mail_mail
|
||||
msgid "Outgoing Mails"
|
||||
msgstr "Исходящая почта"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.actions.act_window,name:portal_sale.action_quotations_portal
|
||||
#: model:ir.ui.menu,name:portal_sale.portal_quotations
|
||||
msgid "Quotations"
|
||||
msgstr "Предложения цен"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.model,name:portal_sale.model_sale_order
|
||||
msgid "Sales Order"
|
||||
msgstr "Заказ продаж"
|
||||
|
||||
#. module: portal_sale
|
||||
#: field:account.invoice,portal_payment_options:0
|
||||
#: field:sale.order,portal_payment_options:0
|
||||
msgid "Portal Payment Options"
|
||||
msgstr "Варианты оплаты через портал"
|
||||
|
||||
#. module: portal_sale
|
||||
#: help:account.config.settings,group_payment_options:0
|
||||
msgid ""
|
||||
"Show online payment options on Sale Orders and Customer Invoices to "
|
||||
"employees. If not checked, these options are only visible to portal users."
|
||||
msgstr ""
|
||||
"Показывать варианты оплаты по заказам продаж и счетам заказчиков "
|
||||
"сотрудникам. Если не отмечено, то эти варианты видны только пользователям "
|
||||
"портала."
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.actions.act_window,name:portal_sale.portal_action_invoices
|
||||
#: model:ir.ui.menu,name:portal_sale.portal_invoices
|
||||
msgid "Invoices"
|
||||
msgstr "Счета"
|
||||
|
||||
#. module: portal_sale
|
||||
#: view:account.config.settings:0
|
||||
msgid "Configure payment acquiring methods"
|
||||
msgstr "Настроить методы получения платежей"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:email.template,body_html:portal_sale.email_template_edi_invoice
|
||||
msgid ""
|
||||
"\n"
|
||||
"<div style=\"font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-"
|
||||
"serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, "
|
||||
"255, 255); \">\n"
|
||||
"\n"
|
||||
" <p>Hello ${object.partner_id.name},</p>\n"
|
||||
"\n"
|
||||
" <p>A new invoice is available for you: </p>\n"
|
||||
" \n"
|
||||
" <p style=\"border-left: 1px solid #8e0000; margin-left: 30px;\">\n"
|
||||
" <strong>REFERENCES</strong><br />\n"
|
||||
" Invoice number: <strong>${object.number}</strong><br />\n"
|
||||
" Invoice total: <strong>${object.amount_total} "
|
||||
"${object.currency_id.name}</strong><br />\n"
|
||||
" Invoice date: ${object.date_invoice}<br />\n"
|
||||
" % if object.origin:\n"
|
||||
" Order reference: ${object.origin}<br />\n"
|
||||
" % endif\n"
|
||||
" % if object.user_id:\n"
|
||||
" Your contact: <a href=\"mailto:${object.user_id.email or "
|
||||
"''}?subject=Invoice%20${object.number}\">${object.user_id.name}</a>\n"
|
||||
" % endif\n"
|
||||
" </p> \n"
|
||||
"\n"
|
||||
" <% set signup_url = object.get_signup_url() %>\n"
|
||||
" % if signup_url:\n"
|
||||
" <p>\n"
|
||||
" You can access the invoice document and pay online via our Customer "
|
||||
"Portal:\n"
|
||||
" </p>\n"
|
||||
" <a style=\"display:block; width: 150px; height:20px; margin-left: "
|
||||
"120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-"
|
||||
"serif; font-size: 13px; font-weight: bold; text-align: center; text-"
|
||||
"decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; "
|
||||
"background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat "
|
||||
"no-repeat;\"\n"
|
||||
" href=\"${signup_url}\">View Invoice</a>\n"
|
||||
" % endif\n"
|
||||
" \n"
|
||||
" % if object.paypal_url:\n"
|
||||
" <br/>\n"
|
||||
" <p>It is also possible to directly pay with Paypal:</p>\n"
|
||||
" <a style=\"margin-left: 120px;\" href=\"${object.paypal_url}\">\n"
|
||||
" <img class=\"oe_edi_paypal_button\" "
|
||||
"src=\"https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif\"/>\n"
|
||||
" </a>\n"
|
||||
" % endif\n"
|
||||
" \n"
|
||||
" <br/>\n"
|
||||
" <p>If you have any question, do not hesitate to contact us.</p>\n"
|
||||
" <p>Thank you for choosing ${object.company_id.name or 'us'}!</p>\n"
|
||||
" <br/>\n"
|
||||
" <br/>\n"
|
||||
" <div style=\"width: 375px; margin: 0px; padding: 0px; background-color: "
|
||||
"#8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; "
|
||||
"background-repeat: repeat no-repeat;\">\n"
|
||||
" <h3 style=\"margin: 0px; padding: 2px 14px; font-size: 12px; color: "
|
||||
"#DDD;\">\n"
|
||||
" <strong style=\"text-"
|
||||
"transform:uppercase;\">${object.company_id.name}</strong></h3>\n"
|
||||
" </div>\n"
|
||||
" <div style=\"width: 347px; margin: 0px; padding: 5px 14px; line-height: "
|
||||
"16px; background-color: #F2F2F2;\">\n"
|
||||
" <span style=\"color: #222; margin-bottom: 5px; display: block; \">\n"
|
||||
" % if object.company_id.street:\n"
|
||||
" ${object.company_id.street}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.street2:\n"
|
||||
" ${object.company_id.street2}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.city or object.company_id.zip:\n"
|
||||
" ${object.company_id.zip} ${object.company_id.city}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.country_id:\n"
|
||||
" ${object.company_id.state_id and ('%s, ' % "
|
||||
"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name "
|
||||
"or ''}<br/>\n"
|
||||
" % endif\n"
|
||||
" </span>\n"
|
||||
" % if object.company_id.phone:\n"
|
||||
" <div style=\"margin-top: 0px; margin-right: 0px; margin-bottom: "
|
||||
"0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: "
|
||||
"0px; padding-left: 0px; \">\n"
|
||||
" Phone: ${object.company_id.phone}\n"
|
||||
" </div>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.website:\n"
|
||||
" <div>\n"
|
||||
" Web : <a "
|
||||
"href=\"${object.company_id.website}\">${object.company_id.website}</a>\n"
|
||||
" </div>\n"
|
||||
" % endif\n"
|
||||
" <p></p>\n"
|
||||
" </div>\n"
|
||||
"</div>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"<div style=\"font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-"
|
||||
"serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, "
|
||||
"255, 255); \">\n"
|
||||
"\n"
|
||||
" <p>Здравствуйте, ${object.partner_id.name},</p>\n"
|
||||
"\n"
|
||||
" <p>Вам выставлен счёт: </p>\n"
|
||||
" \n"
|
||||
" <p style=\"border-left: 1px solid #8e0000; margin-left: 30px;\">\n"
|
||||
" <strong>СВОДКА</strong><br />\n"
|
||||
" Счёт: <strong>${object.number}</strong><br />\n"
|
||||
" Итог: <strong>${object.amount_total} "
|
||||
"${object.currency_id.name}</strong><br />\n"
|
||||
" Дата: ${object.date_invoice}<br />\n"
|
||||
" % if object.origin:\n"
|
||||
" Основание: ${object.origin}<br />\n"
|
||||
" % endif\n"
|
||||
" % if object.user_id:\n"
|
||||
" Ваш контакт: <a href=\"mailto:${object.user_id.email or "
|
||||
"''}?subject=Счёт%20${object.number}\">${object.user_id.name}</a>\n"
|
||||
" % endif\n"
|
||||
" </p> \n"
|
||||
"\n"
|
||||
" <% set signup_url = object.get_signup_url() %>\n"
|
||||
" % if signup_url:\n"
|
||||
" <p>\n"
|
||||
" Вы можете ознакомиться с документом онлайн и узнать о вариантах оплаты "
|
||||
"через наш Портал:\n"
|
||||
" </p>\n"
|
||||
" <a style=\"display:block; width: 150px; height:20px; margin-left: "
|
||||
"120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-"
|
||||
"serif; font-size: 13px; font-weight: bold; text-align: center; text-"
|
||||
"decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; "
|
||||
"background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat "
|
||||
"no-repeat;\"\n"
|
||||
" href=\"${signup_url}\">Посмотреть счёт</a>\n"
|
||||
" % endif\n"
|
||||
" \n"
|
||||
" % if object.paypal_url:\n"
|
||||
" <br/>\n"
|
||||
" <p>Также возможно произвести оплату через Paypal:</p>\n"
|
||||
" <a style=\"margin-left: 120px;\" href=\"${object.paypal_url}\">\n"
|
||||
" <img class=\"oe_edi_paypal_button\" "
|
||||
"src=\"https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif\"/>\n"
|
||||
" </a>\n"
|
||||
" % endif\n"
|
||||
" \n"
|
||||
" <br/>\n"
|
||||
" <p>Если у Вас есть вопросы - пожалуйста, свяжитесь с нами.</p>\n"
|
||||
" <p>Спасибо, что выбрали ${object.company_id.name or 'нас'}!</p>\n"
|
||||
" <br/>\n"
|
||||
" <br/>\n"
|
||||
" <div style=\"width: 375px; margin: 0px; padding: 0px; background-color: "
|
||||
"#8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; "
|
||||
"background-repeat: repeat no-repeat;\">\n"
|
||||
" <h3 style=\"margin: 0px; padding: 2px 14px; font-size: 12px; color: "
|
||||
"#DDD;\">\n"
|
||||
" <strong style=\"text-"
|
||||
"transform:uppercase;\">${object.company_id.name}</strong></h3>\n"
|
||||
" </div>\n"
|
||||
" <div style=\"width: 347px; margin: 0px; padding: 5px 14px; line-height: "
|
||||
"16px; background-color: #F2F2F2;\">\n"
|
||||
" <span style=\"color: #222; margin-bottom: 5px; display: block; \">\n"
|
||||
" % if object.company_id.street:\n"
|
||||
" ${object.company_id.street}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.street2:\n"
|
||||
" ${object.company_id.street2}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.city or object.company_id.zip:\n"
|
||||
" ${object.company_id.zip} ${object.company_id.city}<br/>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.country_id:\n"
|
||||
" ${object.company_id.state_id and ('%s, ' % "
|
||||
"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name "
|
||||
"or ''}<br/>\n"
|
||||
" % endif\n"
|
||||
" </span>\n"
|
||||
" % if object.company_id.phone:\n"
|
||||
" <div style=\"margin-top: 0px; margin-right: 0px; margin-bottom: "
|
||||
"0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: "
|
||||
"0px; padding-left: 0px; \">\n"
|
||||
" Phone: ${object.company_id.phone}\n"
|
||||
" </div>\n"
|
||||
" % endif\n"
|
||||
" % if object.company_id.website:\n"
|
||||
" <div>\n"
|
||||
" Сайт: <a "
|
||||
"href=\"${object.company_id.website}\">${object.company_id.website}</a>\n"
|
||||
" </div>\n"
|
||||
" % endif\n"
|
||||
" <p></p>\n"
|
||||
" </div>\n"
|
||||
"</div>\n"
|
||||
" "
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.actions.act_window,help:portal_sale.action_orders_portal
|
||||
msgid "We haven't sent you any sales order."
|
||||
msgstr "У Вас нет никаких заказов."
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.model,name:portal_sale.model_account_invoice
|
||||
msgid "Invoice"
|
||||
msgstr "Счёт"
|
||||
|
||||
#. module: portal_sale
|
||||
#: model:ir.actions.act_window,name:portal_sale.action_orders_portal
|
||||
msgid "Sale Orders"
|
||||
msgstr "Заказы продаж"
|
||||
@@ -804,7 +804,7 @@ class task(osv.osv):
|
||||
_defaults = {
|
||||
'stage_id': _get_default_stage_id,
|
||||
'project_id': _get_default_project_id,
|
||||
'date_last_stage_update': lambda *a: fields.datetime.now(),
|
||||
'date_last_stage_update': fields.datetime.now,
|
||||
'kanban_state': 'normal',
|
||||
'priority': '2',
|
||||
'progress': 0,
|
||||
|
||||
@@ -28,30 +28,30 @@ class project_configuration(osv.osv_memory):
|
||||
|
||||
_columns = {
|
||||
'module_project_mrp': fields.boolean('Generate tasks from sale orders',
|
||||
help ="""This feature automatically creates project tasks from service products in sale orders.
|
||||
More precisely, tasks are created for procurement lines with product of type 'Service',
|
||||
procurement method 'Make to Order', and supply method 'Manufacture'.
|
||||
This installs the module project_mrp."""),
|
||||
help='This feature automatically creates project tasks from service products in sale orders. '
|
||||
'More precisely, tasks are created for procurement lines with product of type \'Service\', '
|
||||
'procurement method \'Make to Order\', and supply method \'Manufacture\'.\n'
|
||||
'-This installs the module project_mrp.'),
|
||||
'module_pad': fields.boolean("Use integrated collaborative note pads on task",
|
||||
help="""Lets the company customize which Pad installation should be used to link to new pads
|
||||
(for example: http://ietherpad.com/).
|
||||
This installs the module pad."""),
|
||||
help='Lets the company customize which Pad installation should be used to link to new pads '
|
||||
'(for example: http://ietherpad.com/).\n'
|
||||
'-This installs the module pad.'),
|
||||
'module_project_timesheet': fields.boolean("Record timesheet lines per tasks",
|
||||
help="""This allows you to transfer the entries under tasks defined for Project Management to
|
||||
the timesheet line entries for particular date and user, with the effect of creating,
|
||||
editing and deleting either ways.
|
||||
This installs the module project_timesheet."""),
|
||||
help='This allows you to transfer the entries under tasks defined for Project Management to '
|
||||
'the timesheet line entries for particular date and user, with the effect of creating, '
|
||||
'editing and deleting either ways.\n'
|
||||
'-This installs the module project_timesheet.'),
|
||||
'module_project_long_term': fields.boolean("Manage resources planning on gantt view",
|
||||
help="""A long term project management module that tracks planning, scheduling, and resource allocation.
|
||||
This installs the module project_long_term."""),
|
||||
help='A long term project management module that tracks planning, scheduling, and resource allocation.\n'
|
||||
'-This installs the module project_long_term.'),
|
||||
'module_project_issue': fields.boolean("Track issues and bugs",
|
||||
help="""Provides management of issues/bugs in projects.
|
||||
This installs the module project_issue."""),
|
||||
help='Provides management of issues/bugs in projects.\n'
|
||||
'-This installs the module project_issue.'),
|
||||
'time_unit': fields.many2one('product.uom', 'Working time unit', required=True,
|
||||
help="""This will set the unit of measure used in projects and tasks."""),
|
||||
'module_project_issue_sheet': fields.boolean("Invoice working time on issues",
|
||||
help="""Provides timesheet support for the issues/bugs management in project.
|
||||
This installs the module project_issue_sheet."""),
|
||||
help='Provides timesheet support for the issues/bugs management in project.\n'
|
||||
'-This installs the module project_issue_sheet.'),
|
||||
'group_tasks_work_on_tasks': fields.boolean("Log work activities on tasks",
|
||||
implied_group='project.group_tasks_work_on_tasks',
|
||||
help="Allows you to compute work on tasks."),
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.addons.mail.tests.test_mail_base import TestMailBase
|
||||
from openerp.addons.mail.tests.common import TestMail
|
||||
|
||||
|
||||
class TestProjectBase(TestMailBase):
|
||||
class TestProjectBase(TestMail):
|
||||
|
||||
def setUp(self):
|
||||
super(TestProjectBase, self).setUp()
|
||||
|
||||
@@ -297,7 +297,7 @@ class project_issue(osv.Model):
|
||||
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
|
||||
'priority': crm.AVAILABLE_PRIORITIES[2][0],
|
||||
'kanban_state': 'normal',
|
||||
'date_last_stage_update': fields.datetime.now(),
|
||||
'date_last_stage_update': fields.datetime.now,
|
||||
'user_id': lambda obj, cr, uid, context: uid,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
<menuitem id="menu_purchase_config_purchase" name="Configuration"
|
||||
groups="group_purchase_manager"
|
||||
parent="base.menu_purchase_root" sequence="100"/>
|
||||
<menuitem
|
||||
action="product.product_pricelist_action_for_purchase" id="menu_product_pricelist_action2_purchase"
|
||||
parent="menu_purchase_config_purchase" sequence="10" groups="product.group_purchase_pricelist" />
|
||||
|
||||
<record id="purchase_pricelist_version_action" model="ir.actions.act_window">
|
||||
<field name="name">Pricelist Versions</field>
|
||||
@@ -40,10 +37,13 @@
|
||||
id="menu_purchase_config_pricelist" name="Pricelists"
|
||||
parent="menu_purchase_config_purchase" sequence="50" groups="product.group_purchase_pricelist"/>
|
||||
|
||||
<menuitem
|
||||
action="product.product_pricelist_action_for_purchase" id="menu_product_pricelist_action2_purchase"
|
||||
parent="menu_purchase_config_pricelist" sequence="1" groups="product.group_purchase_pricelist" />
|
||||
|
||||
<menuitem
|
||||
action="purchase_pricelist_version_action" id="menu_purchase_pricelist_version_action"
|
||||
parent="menu_purchase_config_pricelist" sequence="2" groups="product.group_purchase_pricelist"/>
|
||||
|
||||
|
||||
<menuitem
|
||||
action="product.product_price_type_action" id="menu_product_pricelist_action2_purchase_type"
|
||||
@@ -73,7 +73,7 @@
|
||||
parent="menu_purchase_config_purchase"/>
|
||||
|
||||
<menuitem
|
||||
action="base.action_partner_category_form" id="menu_partner_categories_in_form" name="Partner Categories"
|
||||
action="base.action_partner_category_form" id="menu_partner_categories_in_form" name="Partner Tags"
|
||||
parent="purchase.menu_purchase_partner_cat" groups="base.group_no_one"/>
|
||||
|
||||
<!--Supplier menu-->
|
||||
|
||||
@@ -34,8 +34,8 @@ class purchase_config_settings(osv.osv_memory):
|
||||
], 'Default invoicing control method', required=True, default_model='purchase.order'),
|
||||
'group_purchase_pricelist':fields.boolean("Manage pricelist per supplier",
|
||||
implied_group='product.group_purchase_pricelist',
|
||||
help="""Allows to manage different prices based on rules per category of Supplier.
|
||||
Example: 10% for retailers, promotion of 5 EUR on this product, etc."""),
|
||||
help='Allows to manage different prices based on rules per category of Supplier.\n'
|
||||
'Example: 10% for retailers, promotion of 5 EUR on this product, etc.'),
|
||||
'group_uom':fields.boolean("Manage different units of measure for products",
|
||||
implied_group='product.group_uom',
|
||||
help="""Allows you to select and maintain different units of measure for products."""),
|
||||
@@ -43,19 +43,19 @@ class purchase_config_settings(osv.osv_memory):
|
||||
implied_group='product.group_costing_method',
|
||||
help="""Allows you to compute product cost price based on average cost."""),
|
||||
'module_warning': fields.boolean("Alerts by products or supplier",
|
||||
help="""Allow to configure notification on products and trigger them when a user wants to purchase a given product or a given supplier.
|
||||
Example: Product: this product is deprecated, do not purchase more than 5.
|
||||
Supplier: don't forget to ask for an express delivery."""),
|
||||
help='Allow to configure notification on products and trigger them when a user wants to purchase a given product or a given supplier.\n'
|
||||
'Example: Product: this product is deprecated, do not purchase more than 5.\n'
|
||||
'Supplier: don\'t forget to ask for an express delivery.'),
|
||||
'module_purchase_double_validation': fields.boolean("Force two levels of approvals",
|
||||
help="""Provide a double validation mechanism for purchases exceeding minimum amount.
|
||||
This installs the module purchase_double_validation."""),
|
||||
help='Provide a double validation mechanism for purchases exceeding minimum amount.\n'
|
||||
'-This installs the module purchase_double_validation.'),
|
||||
'module_purchase_requisition': fields.boolean("Manage purchase requisitions",
|
||||
help="""Purchase Requisitions are used when you want to request quotations from several suppliers for a given set of products.
|
||||
You can configure per product if you directly do a Request for Quotation
|
||||
to one supplier or if you want a purchase requisition to negotiate with several suppliers."""),
|
||||
help='Purchase Requisitions are used when you want to request quotations from several suppliers for a given set of products.\n'
|
||||
'You can configure per product if you directly do a Request for Quotation '
|
||||
'to one supplier or if you want a purchase requisition to negotiate with several suppliers.'),
|
||||
'module_purchase_analytic_plans': fields.boolean('Use multiple analytic accounts on purchase orders',
|
||||
help ="""Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.
|
||||
This installs the module purchase_analytic_plans."""),
|
||||
help='Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.\n'
|
||||
'-This installs the module purchase_analytic_plans.'),
|
||||
'group_analytic_account_for_purchases': fields.boolean('Analytic accounting for purchases',
|
||||
implied_group='purchase.group_analytic_accounting',
|
||||
help="Allows you to specify an analytic account on purchase orders."),
|
||||
@@ -77,8 +77,8 @@ class account_config_settings(osv.osv_memory):
|
||||
_inherit = 'account.config.settings'
|
||||
_columns = {
|
||||
'module_purchase_analytic_plans': fields.boolean('Use multiple analytic accounts on orders',
|
||||
help ="""Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.
|
||||
This installs the module purchase_analytic_plans."""),
|
||||
help='Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans.\n'
|
||||
'-This installs the module purchase_analytic_plans.'),
|
||||
'group_analytic_account_for_purchases': fields.boolean('Analytic accounting for purchases',
|
||||
implied_group='purchase.group_analytic_accounting',
|
||||
help="Allows you to specify an analytic account on purchase orders."),
|
||||
|
||||