[MERGE]: Merged with addons

bzr revid: atp@tinyerp.com-20130813154002-5u1vhoavze1nguza
This commit is contained in:
Atul Patel (OpenERP)
2013-08-13 21:10:02 +05:30
114 changed files with 8062 additions and 2383 deletions
+5 -5
View File
@@ -8,19 +8,19 @@ msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n"
"PO-Revision-Date: 2012-04-13 22:35+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"PO-Revision-Date: 2013-07-30 22:25+0000\n"
"Last-Translator: Masaki Yamaya <Unknown>\n"
"Language-Team: Japanese <ja@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-03-16 05:47+0000\n"
"X-Generator: Launchpad (build 16532)\n"
"X-Launchpad-Export-Date: 2013-07-31 05:16+0000\n"
"X-Generator: Launchpad (build 16718)\n"
#. module: account_accountant
#: model:ir.actions.client,name:account_accountant.action_client_account_menu
msgid "Open Accounting Menu"
msgstr ""
msgstr "会計メニューを開く"
#~ msgid ""
#~ "\n"
+43 -43
View File
@@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n"
"PO-Revision-Date: 2013-05-15 10:09+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"PO-Revision-Date: 2013-07-24 08:53+0000\n"
"Last-Translator: Sumonchai ( เหลา ) <sumonchai@gmail.com>\n"
"Language-Team: Thai <th@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-05-16 05:12+0000\n"
"X-Generator: Launchpad (build 16626)\n"
"X-Launchpad-Export-Date: 2013-07-25 05:13+0000\n"
"X-Generator: Launchpad (build 16700)\n"
#. module: account_asset
#: view:account.asset.asset:0
@@ -42,7 +42,7 @@ msgstr ""
#. module: account_asset
#: view:asset.asset.report:0
msgid "Group By..."
msgstr ""
msgstr "จัดกลุ่มตาม..."
#. module: account_asset
#: field:asset.asset.report,gross_value:0
@@ -58,7 +58,7 @@ msgstr ""
#: field:asset.asset.report,asset_id:0
#: model:ir.model,name:account_asset.model_account_asset_asset
msgid "Asset"
msgstr ""
msgstr "สินทรัพย์"
#. module: account_asset
#: help:account.asset.asset,prorata:0
@@ -72,7 +72,7 @@ msgstr ""
#: selection:account.asset.asset,method:0
#: selection:account.asset.category,method:0
msgid "Linear"
msgstr ""
msgstr "เชิงเส้น"
#. module: account_asset
#: field:account.asset.asset,company_id:0
@@ -80,24 +80,24 @@ msgstr ""
#: view:asset.asset.report:0
#: field:asset.asset.report,company_id:0
msgid "Company"
msgstr ""
msgstr "บริษัท"
#. module: account_asset
#: view:asset.modify:0
msgid "Modify"
msgstr ""
msgstr "ปรับเปลี่ยน"
#. module: account_asset
#: selection:account.asset.asset,state:0
#: view:asset.asset.report:0
#: selection:asset.asset.report,state:0
msgid "Running"
msgstr ""
msgstr "กำลังทำงานอยู่"
#. module: account_asset
#: view:account.asset.asset:0
msgid "Set to Draft"
msgstr ""
msgstr "กำหนดให้เป็นแบบร่าง"
#. module: account_asset
#: view:asset.asset.report:0
@@ -110,7 +110,7 @@ msgstr ""
#. module: account_asset
#: field:asset.modify,name:0
msgid "Reason"
msgstr ""
msgstr "เหตุผล"
#. module: account_asset
#: field:account.asset.asset,method_progress_factor:0
@@ -122,7 +122,7 @@ msgstr ""
#: model:ir.actions.act_window,name:account_asset.action_account_asset_asset_list_normal
#: model:ir.ui.menu,name:account_asset.menu_action_account_asset_asset_list_normal
msgid "Asset Categories"
msgstr ""
msgstr "หมวดหมู่สินทรัพย์"
#. module: account_asset
#: view:account.asset.asset:0
@@ -173,7 +173,7 @@ msgstr ""
#: model:ir.ui.menu,name:account_asset.menu_finance_assets
#: model:ir.ui.menu,name:account_asset.menu_finance_config_assets
msgid "Assets"
msgstr ""
msgstr "สินทรัพย์"
#. module: account_asset
#: field:account.asset.category,account_depreciation_id:0
@@ -187,7 +187,7 @@ msgstr ""
#: view:asset.modify:0
#: field:asset.modify,note:0
msgid "Notes"
msgstr ""
msgstr "บันทึกย่อ"
#. module: account_asset
#: field:account.asset.depreciation.line,move_id:0
@@ -221,7 +221,7 @@ msgstr ""
#. module: account_asset
#: field:account.asset.asset,code:0
msgid "Reference"
msgstr ""
msgstr "อ้างถึง"
#. module: account_asset
#: view:account.asset.asset:0
@@ -246,7 +246,7 @@ msgstr ""
#: view:asset.asset.report:0
#: selection:asset.asset.report,state:0
msgid "Draft"
msgstr ""
msgstr "ฉบับร่าง"
#. module: account_asset
#: view:asset.asset.report:0
@@ -273,13 +273,13 @@ msgstr ""
#. module: account_asset
#: field:account.asset.category,account_analytic_id:0
msgid "Analytic account"
msgstr ""
msgstr "วิเคราะห์บัญชี"
#. module: account_asset
#: field:account.asset.asset,method:0
#: field:account.asset.category,method:0
msgid "Computation Method"
msgstr ""
msgstr "วิธีการคำนวณ"
#. module: account_asset
#: constraint:account.asset.asset:0
@@ -308,7 +308,7 @@ msgstr ""
#. module: account_asset
#: field:account.asset.asset,salvage_value:0
msgid "Salvage Value"
msgstr ""
msgstr "มูลค่าซาก"
#. module: account_asset
#: field:account.asset.asset,category_id:0
@@ -364,20 +364,20 @@ msgstr ""
#: field:account.asset.category,method_time:0
#: field:account.asset.history,method_time:0
msgid "Time Method"
msgstr ""
msgstr "วิธีการจัดการเวลา"
#. module: account_asset
#: view:asset.depreciation.confirmation.wizard:0
#: view:asset.modify:0
msgid "or"
msgstr ""
msgstr "หรือ"
#. module: account_asset
#: field:account.asset.asset,note:0
#: field:account.asset.category,note:0
#: field:account.asset.history,note:0
msgid "Note"
msgstr ""
msgstr "บันทึกย่อ"
#. module: account_asset
#: help:account.asset.history,method_time:0
@@ -409,7 +409,7 @@ msgstr ""
#. module: account_asset
#: view:account.asset.asset:0
msgid "Closed"
msgstr ""
msgstr "ปิด"
#. module: account_asset
#: help:account.asset.asset,state:0
@@ -425,13 +425,13 @@ msgstr ""
#: field:account.asset.asset,state:0
#: field:asset.asset.report,state:0
msgid "Status"
msgstr ""
msgstr "สถานะ"
#. module: account_asset
#: field:account.asset.asset,partner_id:0
#: field:asset.asset.report,partner_id:0
msgid "Partner"
msgstr ""
msgstr "พาร์ทเนอร์"
#. module: account_asset
#: view:asset.asset.report:0
@@ -451,7 +451,7 @@ msgstr ""
#. module: account_asset
#: field:account.asset.history,user_id:0
msgid "User"
msgstr ""
msgstr "ผู้ใช้"
#. module: account_asset
#: field:account.asset.category,account_asset_id:0
@@ -482,12 +482,12 @@ msgstr ""
#. module: account_asset
#: field:account.asset.asset,active:0
msgid "Active"
msgstr ""
msgstr "เปิดใช้งาน"
#. module: account_asset
#: field:account.asset.depreciation.line,parent_state:0
msgid "State of Asset"
msgstr ""
msgstr "สถานะของสินทรัพย์"
#. module: account_asset
#: field:account.asset.depreciation.line,name:0
@@ -498,12 +498,12 @@ msgstr ""
#: view:account.asset.asset:0
#: field:account.asset.asset,history_ids:0
msgid "History"
msgstr ""
msgstr "ประวัติ"
#. module: account_asset
#: view:asset.depreciation.confirmation.wizard:0
msgid "Compute Asset"
msgstr ""
msgstr "คำนวณสินทรัพย์"
#. module: account_asset
#: field:asset.depreciation.confirmation.wizard,period_id:0
@@ -513,7 +513,7 @@ msgstr ""
#. module: account_asset
#: view:account.asset.asset:0
msgid "General"
msgstr ""
msgstr "ทั่วไป"
#. module: account_asset
#: field:account.asset.asset,prorata:0
@@ -524,7 +524,7 @@ msgstr ""
#. module: account_asset
#: model:ir.model,name:account_asset.model_account_invoice
msgid "Invoice"
msgstr ""
msgstr "ใบแจ้งหนี้"
#. module: account_asset
#: view:account.asset.asset:0
@@ -535,13 +535,13 @@ msgstr ""
#: view:asset.depreciation.confirmation.wizard:0
#: view:asset.modify:0
msgid "Cancel"
msgstr ""
msgstr "ยกเลิก"
#. module: account_asset
#: selection:account.asset.asset,state:0
#: selection:asset.asset.report,state:0
msgid "Close"
msgstr ""
msgstr "ปิด"
#. module: account_asset
#: model:ir.model,name:account_asset.model_account_move_line
@@ -558,7 +558,7 @@ msgstr ""
#: view:asset.asset.report:0
#: field:asset.asset.report,purchase_date:0
msgid "Purchase Date"
msgstr ""
msgstr "วันที่ซื้อ"
#. module: account_asset
#: selection:account.asset.asset,method:0
@@ -606,7 +606,7 @@ msgstr ""
#. module: account_asset
#: field:account.asset.asset,currency_id:0
msgid "Currency"
msgstr ""
msgstr "สกุลเงิน"
#. module: account_asset
#: field:account.asset.category,journal_id:0
@@ -637,7 +637,7 @@ msgstr ""
#: view:asset.asset.report:0
#: field:asset.asset.report,move_check:0
msgid "Posted"
msgstr ""
msgstr "ลงบัญชีแล้ว"
#. module: account_asset
#: model:ir.actions.act_window,help:account_asset.action_asset_asset_report
@@ -660,7 +660,7 @@ msgstr ""
#. module: account_asset
#: field:account.asset.category,name:0
msgid "Name"
msgstr ""
msgstr "ชื่อ"
#. module: account_asset
#: help:account.asset.category,open_asset:0
@@ -672,7 +672,7 @@ msgstr ""
#. module: account_asset
#: field:asset.asset.report,name:0
msgid "Year"
msgstr ""
msgstr "ปี"
#. module: account_asset
#: model:ir.model,name:account_asset.model_account_asset_depreciation_line
@@ -693,7 +693,7 @@ msgid "Amount of Depreciation Lines"
msgstr ""
#. module: account_asset
#: code:addons/account_asset/wizard/wizard_asset_compute.py:49
#: code:addons/account_asset/wizard/wizard_asset_compute.py:50
#, python-format
msgid "Created Asset Moves"
msgstr ""
@@ -701,7 +701,7 @@ msgstr ""
#. module: account_asset
#: field:account.asset.depreciation.line,sequence:0
msgid "Sequence"
msgstr ""
msgstr "ลำดับ"
#. module: account_asset
#: help:account.asset.category,method_period:0
@@ -711,7 +711,7 @@ msgstr ""
#. module: account_asset
#: field:account.asset.history,date:0
msgid "Date"
msgstr ""
msgstr "วันที่"
#. module: account_asset
#: field:account.asset.asset,method_number:0
@@ -0,0 +1,361 @@
# 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:05+0000\n"
"PO-Revision-Date: 2013-08-05 10:40+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-08-06 04:47+0000\n"
"X-Generator: Launchpad (build 16718)\n"
#. module: account_bank_statement_extensions
#: help:account.bank.statement.line.global,name:0
msgid "Originator to Beneficiary Information"
msgstr "Информация от плательщика получателю"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
#: selection:account.bank.statement.line,state:0
msgid "Confirmed"
msgstr "Подтверждено"
#. module: account_bank_statement_extensions
#: view:account.bank.statement:0
#: view:account.bank.statement.line:0
msgid "Glob. Id"
msgstr ""
#. module: account_bank_statement_extensions
#: selection:account.bank.statement.line.global,type:0
msgid "CODA"
msgstr ""
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line.global,parent_id:0
msgid "Parent Code"
msgstr "Основной код"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Debit"
msgstr "Дебет"
#. module: account_bank_statement_extensions
#: view:cancel.statement.line:0
#: model:ir.actions.act_window,name:account_bank_statement_extensions.action_cancel_statement_line
#: model:ir.model,name:account_bank_statement_extensions.model_cancel_statement_line
msgid "Cancel selected statement lines"
msgstr "Отмена выбранных позиций выписки"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line,val_date:0
msgid "Value Date"
msgstr "Дата зачисления"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Group By..."
msgstr "Группировать по ..."
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
#: selection:account.bank.statement.line,state:0
msgid "Draft"
msgstr "Черновик"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Statement"
msgstr "Выписка"
#. module: account_bank_statement_extensions
#: view:confirm.statement.line:0
#: model:ir.actions.act_window,name:account_bank_statement_extensions.action_confirm_statement_line
#: model:ir.model,name:account_bank_statement_extensions.model_confirm_statement_line
msgid "Confirm selected statement lines"
msgstr "Подтверждение выбранных позиций выписки"
#. module: account_bank_statement_extensions
#: report:bank.statement.balance.report:0
#: model:ir.actions.report.xml,name:account_bank_statement_extensions.bank_statement_balance_report
msgid "Bank Statement Balances Report"
msgstr "Отчет об остатках банковской выписки"
#. module: account_bank_statement_extensions
#: view:cancel.statement.line:0
msgid "Cancel Lines"
msgstr "Отменить позиции"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line.global:0
#: model:ir.model,name:account_bank_statement_extensions.model_account_bank_statement_line_global
msgid "Batch Payment Info"
msgstr ""
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line,state:0
msgid "Status"
msgstr "Статус"
#. module: account_bank_statement_extensions
#: code:addons/account_bank_statement_extensions/account_bank_statement.py:129
#, python-format
msgid ""
"Delete operation not allowed. Please go to the associated bank "
"statement in order to delete and/or modify bank statement line."
msgstr ""
"Операция удаления запрещена. Пожалуйста, обратитесь к соответствующей "
"банковской выписке для удаления/изменения позиции."
#. module: account_bank_statement_extensions
#: view:confirm.statement.line:0
msgid "or"
msgstr "или"
#. module: account_bank_statement_extensions
#: view:confirm.statement.line:0
msgid "Confirm Lines"
msgstr "Подтвердить позиции"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line.global:0
msgid "Transactions"
msgstr "Операции"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line.global,type:0
msgid "Type"
msgstr "Тип"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
#: report:bank.statement.balance.report:0
msgid "Journal"
msgstr "Журнал"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Confirmed Statement Lines."
msgstr "Подтвержденные позиции выписки."
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Credit Transactions."
msgstr "Операции по кредиту"
#. module: account_bank_statement_extensions
#: model:ir.actions.act_window,help:account_bank_statement_extensions.action_cancel_statement_line
msgid "cancel selected statement lines."
msgstr "отменить выбранные позиции выписки"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line,counterparty_number:0
msgid "Counterparty Number"
msgstr "Число контрагентов"
#. module: account_bank_statement_extensions
#: report:bank.statement.balance.report:0
msgid "Closing Balance"
msgstr "Итоговый баланс"
#. module: account_bank_statement_extensions
#: report:bank.statement.balance.report:0
msgid "Date"
msgstr "Дата"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
#: field:account.bank.statement.line,globalisation_amount:0
msgid "Glob. Amount"
msgstr ""
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Debit Transactions."
msgstr "Операции по дебету"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Extended Filters..."
msgstr "Расширенные фильтры..."
#. module: account_bank_statement_extensions
#: view:confirm.statement.line:0
msgid "Confirmed lines cannot be changed anymore."
msgstr "Нельзя изменить подтвержденные позиции."
#. module: account_bank_statement_extensions
#: view:cancel.statement.line:0
msgid "Are you sure you want to cancel the selected Bank Statement lines ?"
msgstr ""
"Вы уверены, что хотите отменить выбранные позиции банковской выписки ?"
#. module: account_bank_statement_extensions
#: report:bank.statement.balance.report:0
msgid "Name"
msgstr "Название"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line.global,name:0
msgid "OBI"
msgstr "Назначение"
#. module: account_bank_statement_extensions
#: selection:account.bank.statement.line.global,type:0
msgid "ISO 20022"
msgstr "ISO 20022"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Notes"
msgstr "Примечания"
#. module: account_bank_statement_extensions
#: selection:account.bank.statement.line.global,type:0
msgid "Manual"
msgstr "Ручной"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Bank Transaction"
msgstr "Банковские операции"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Credit"
msgstr "Кредит"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line.global,amount:0
msgid "Amount"
msgstr "Сумма"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Fin.Account"
msgstr "Фин. Счет"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line,counterparty_currency:0
msgid "Counterparty Currency"
msgstr "Валюта контрагента"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line,counterparty_bic:0
msgid "Counterparty BIC"
msgstr "БИК контрагента"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line.global,child_ids:0
msgid "Child Codes"
msgstr "Субкоды"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Search Bank Transactions"
msgstr "Поиск банковских операций"
#. module: account_bank_statement_extensions
#: view:confirm.statement.line:0
msgid "Are you sure you want to confirm the selected Bank Statement lines ?"
msgstr ""
"Вы уверены, что хотите, подтвердить выбранные позиции банковской выписки ?"
#. module: account_bank_statement_extensions
#: help:account.bank.statement.line,globalisation_id:0
msgid ""
"Code to identify transactions belonging to the same globalisation level "
"within a batch payment"
msgstr ""
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Draft Statement Lines."
msgstr "Черновики позиций выписки."
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Glob. Am."
msgstr ""
#. module: account_bank_statement_extensions
#: model:ir.model,name:account_bank_statement_extensions.model_account_bank_statement_line
msgid "Bank Statement Line"
msgstr "Позиция банковской выписки"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line.global,code:0
msgid "Code"
msgstr "Код"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line,counterparty_name:0
msgid "Counterparty Name"
msgstr "Название контрагента"
#. module: account_bank_statement_extensions
#: model:ir.model,name:account_bank_statement_extensions.model_res_partner_bank
msgid "Bank Accounts"
msgstr "Банковские счета"
#. module: account_bank_statement_extensions
#: model:ir.model,name:account_bank_statement_extensions.model_account_bank_statement
msgid "Bank Statement"
msgstr "Банковская выписка"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Statement Line"
msgstr "Позиция выписки"
#. module: account_bank_statement_extensions
#: sql_constraint:account.bank.statement.line.global:0
msgid "The code must be unique !"
msgstr "Код должен быть уникальным !"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line.global,bank_statement_line_ids:0
#: model:ir.actions.act_window,name:account_bank_statement_extensions.action_bank_statement_line
#: model:ir.ui.menu,name:account_bank_statement_extensions.bank_statement_line
msgid "Bank Statement Lines"
msgstr "Позиции банковской выписки"
#. module: account_bank_statement_extensions
#: code:addons/account_bank_statement_extensions/account_bank_statement.py:129
#, python-format
msgid "Warning!"
msgstr "Внимание!"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line.global:0
msgid "Child Batch Payments"
msgstr ""
#. module: account_bank_statement_extensions
#: view:confirm.statement.line:0
msgid "Cancel"
msgstr "Отмена"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Statement Lines"
msgstr "Позиции выписки"
#. module: account_bank_statement_extensions
#: view:account.bank.statement.line:0
msgid "Total Amount"
msgstr "Итоговая сумма"
#. module: account_bank_statement_extensions
#: field:account.bank.statement.line,globalisation_id:0
msgid "Globalisation ID"
msgstr ""
+5 -5
View File
@@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-12-21 17:05+0000\n"
"PO-Revision-Date: 2012-06-13 17:16+0000\n"
"Last-Translator: Akira Hiyama <Unknown>\n"
"PO-Revision-Date: 2013-07-30 22:29+0000\n"
"Last-Translator: Masaki Yamaya <Unknown>\n"
"Language-Team: Japanese <ja@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-03-16 05:34+0000\n"
"X-Generator: Launchpad (build 16532)\n"
"X-Launchpad-Export-Date: 2013-07-31 05:16+0000\n"
"X-Generator: Launchpad (build 16718)\n"
#. module: account_budget
#: view:account.budget.analytic:0
@@ -418,7 +418,7 @@ msgstr "からの分析"
#. module: account_budget
#: view:crossovered.budget:0
msgid "Draft Budgets"
msgstr "ドラフト予算"
msgstr "予算案"
#, python-format
#~ msgid "The General Budget '%s' has no Accounts!"
+23
View File
@@ -0,0 +1,23 @@
# Ukrainian 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-08-01 11:11+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Ukrainian <uk@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-08-02 05:38+0000\n"
"X-Generator: Launchpad (build 16718)\n"
#. module: account_cancel
#: view:account.invoice:0
msgid "Cancel"
msgstr ""
+246
View File
@@ -0,0 +1,246 @@
# Bosnian 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-08-08 22:05+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Bosnian <bs@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-08-09 05:06+0000\n"
"X-Generator: Launchpad (build 16723)\n"
#. module: account_check_writing
#: selection:res.company,check_layout:0
msgid "Check on Top"
msgstr "Ček na vrhu"
#. module: account_check_writing
#: report:account.print.check.top:0
msgid "Open Balance"
msgstr "Otvoreno saldo"
#. module: account_check_writing
#: view:account.check.write:0
#: view:account.voucher:0
msgid "Print Check"
msgstr "Štampaj ček"
#. module: account_check_writing
#: selection:res.company,check_layout:0
msgid "Check in middle"
msgstr "Ćek u sredini"
#. module: account_check_writing
#: help:res.company,check_layout:0
msgid ""
"Check on top is compatible with Quicken, QuickBooks and Microsoft Money. "
"Check in middle is compatible with Peachtree, ACCPAC and DacEasy. Check on "
"bottom is compatible with Peachtree, ACCPAC and DacEasy only"
msgstr ""
"Ček na vrhu je kompatibilan sa Quicken, QuickBooks i Microsoft Money. Ček u "
"sredini je kompatibilan sa Peachtree, ACCPAC i DacEasy. Ček na dnu je "
"kompatibilan sa Peachtree, ACCPAC i DacEasy samo"
#. module: account_check_writing
#: selection:res.company,check_layout:0
msgid "Check on bottom"
msgstr "Ćek na dnu"
#. module: account_check_writing
#: model:ir.actions.act_window,name:account_check_writing.action_account_check_write
msgid "Print Check in Batch"
msgstr "Štampaj čekove grupno"
#. module: account_check_writing
#: code:addons/account_check_writing/wizard/account_check_batch_printing.py:59
#, python-format
msgid "One of the printed check already got a number."
msgstr "Jedan od odštampanih čekova je već dobio broj."
#. module: account_check_writing
#: help:account.journal,allow_check_writing:0
msgid "Check this if the journal is to be used for writing checks."
msgstr ""
"Označite ovo ako će se za pisanje čekova koristiti dnevnik knjiženja."
#. module: account_check_writing
#: field:account.journal,allow_check_writing:0
msgid "Allow Check writing"
msgstr "Dozvoli pisanje čekova"
#. module: account_check_writing
#: report:account.print.check.bottom:0
#: report:account.print.check.middle:0
#: report:account.print.check.top:0
msgid "Description"
msgstr "Opis"
#. module: account_check_writing
#: model:ir.model,name:account_check_writing.model_account_journal
msgid "Journal"
msgstr "Dnevnik knjiženja"
#. module: account_check_writing
#: model:ir.actions.act_window,name:account_check_writing.action_write_check
#: model:ir.ui.menu,name:account_check_writing.menu_action_write_check
msgid "Write Checks"
msgstr "Piši čekove"
#. module: account_check_writing
#: report:account.print.check.bottom:0
#: report:account.print.check.middle:0
#: report:account.print.check.top:0
msgid "Discount"
msgstr "Popust"
#. module: account_check_writing
#: report:account.print.check.bottom:0
#: report:account.print.check.middle:0
#: report:account.print.check.top:0
msgid "Original Amount"
msgstr "Originalni iznos"
#. module: account_check_writing
#: field:res.company,check_layout:0
msgid "Check Layout"
msgstr "Raspored čeka"
#. module: account_check_writing
#: field:account.voucher,allow_check:0
msgid "Allow Check Writing"
msgstr "Dozvoli pisanje čeka"
#. module: account_check_writing
#: report:account.print.check.bottom:0
#: report:account.print.check.middle:0
#: report:account.print.check.top:0
msgid "Payment"
msgstr "Plaćanje"
#. module: account_check_writing
#: field:account.journal,use_preprint_check:0
msgid "Use Preprinted Check"
msgstr "Koristi pred-odštampane čekove"
#. module: account_check_writing
#: model:ir.actions.report.xml,name:account_check_writing.account_print_check_bottom
msgid "Print Check (Bottom)"
msgstr "Štampaj ček (donji)"
#. module: account_check_writing
#: model:ir.actions.act_window,help:account_check_writing.action_write_check
msgid ""
"<p class=\"oe_view_nocontent_create\">\n"
" Click to create a new check. \n"
" </p><p>\n"
" The check payment form allows you to track the payment you "
"do\n"
" to your suppliers using checks. When you select a supplier, "
"the\n"
" payment method and an amount for the payment, OpenERP will\n"
" propose to reconcile your payment with the open supplier\n"
" invoices or bills.\n"
" </p>\n"
" "
msgstr ""
"<p class=\"oe_view_nocontent_create\">\n"
" Kliknite da kreirate nove čekove. \n"
" </p><p>\n"
" Forma plaćanje čekom omogućava vam da pratite plaćanja koja "
"izvršavate\n"
" vašim dobavljačima koristeći čekove. Kada odaberete "
"dobavljača,\n"
" metodu plaćanja i iznos za plaćanje, OpenERP će predložiti "
"da izravna\n"
" vašu uplatu sa otvorenom fakturom ili računom dobavljača.\n"
" </p>\n"
" "
#. module: account_check_writing
#: report:account.print.check.bottom:0
#: report:account.print.check.middle:0
#: report:account.print.check.top:0
msgid "Due Date"
msgstr "Datum dospijeća"
#. module: account_check_writing
#: model:ir.actions.report.xml,name:account_check_writing.account_print_check_middle
msgid "Print Check (Middle)"
msgstr "Štampaj ček (srednji)"
#. module: account_check_writing
#: model:ir.model,name:account_check_writing.model_res_company
msgid "Companies"
msgstr "Kompanije"
#. module: account_check_writing
#: code:addons/account_check_writing/wizard/account_check_batch_printing.py:59
#, python-format
msgid "Error!"
msgstr "Greška!"
#. module: account_check_writing
#: help:account.check.write,check_number:0
msgid "The number of the next check number to be printed."
msgstr "Broj sljedećeg čeka za štampanje."
#. module: account_check_writing
#: report:account.print.check.bottom:0
#: report:account.print.check.middle:0
msgid "Balance Due"
msgstr "Saldo valute"
#. module: account_check_writing
#: model:ir.actions.report.xml,name:account_check_writing.account_print_check_top
msgid "Print Check (Top)"
msgstr "Štampaj ček (na vrhu)"
#. module: account_check_writing
#: report:account.print.check.bottom:0
#: report:account.print.check.middle:0
#: report:account.print.check.top:0
msgid "Check Amount"
msgstr "Iznos čeka"
#. module: account_check_writing
#: model:ir.model,name:account_check_writing.model_account_voucher
msgid "Accounting Voucher"
msgstr "Računovodstveni vaučer"
#. module: account_check_writing
#: view:account.check.write:0
msgid "or"
msgstr "ili"
#. module: account_check_writing
#: field:account.voucher,amount_in_word:0
msgid "Amount in Word"
msgstr "Pisani iznos"
#. module: account_check_writing
#: model:ir.model,name:account_check_writing.model_account_check_write
msgid "Prin Check in Batch"
msgstr "Štampaj čekove grupno"
#. module: account_check_writing
#: view:account.check.write:0
msgid "Cancel"
msgstr "Otkaži"
#. module: account_check_writing
#: field:account.check.write,check_number:0
msgid "Next Check Number"
msgstr "Sljedeći broj čeka"
#. module: account_check_writing
#: view:account.check.write:0
msgid "Check"
msgstr "Ček"
-1
View File
@@ -20,6 +20,5 @@
##############################################################################
import base_state
import base_stage
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+2 -3
View File
@@ -24,11 +24,10 @@
'version': '1.0',
'category': 'Hidden',
'description': """
This module handles state and stage. It is derived from the crm_base and crm_case classes from crm.
===================================================================================================
This module handles state. It is derived from the crm_base and crm_case classes from crm.
==========================================================================================
* ``base_state``: state management
* ``base_stage``: stage management
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
-264
View File
@@ -1,264 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# 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 fields, osv
from openerp.tools.translate import _
class base_stage(object):
""" Base utility mixin class for objects willing to manage their stages.
Object that inherit from this class should inherit from mailgate.thread
to have access to the mail gateway, as well as Chatter. Objects
subclassing this class should define the following colums:
- ``date_open`` (datetime field)
- ``date_closed`` (datetime field)
- ``user_id`` (many2one to res.users)
- ``partner_id`` (many2one to res.partner)
- ``stage_id`` (many2one to a stage definition model)
- ``state`` (selection field, related to the stage_id.state)
"""
def _get_default_partner(self, cr, uid, context=None):
""" Gives id of partner for current user
:param context: if portal not in context returns False
"""
if context is None:
context = {}
if context.get('portal'):
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
return user.partner_id.id
return False
def _get_default_email(self, cr, uid, context=None):
""" Gives default email address for current user
:param context: if portal not in context returns False
"""
if context is None:
context = {}
if context.get('portal'):
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
return user.email
return False
def _get_default_user(self, cr, uid, context=None):
""" Gives current user id
:param context: if portal not in context returns False
"""
if context is None:
context = {}
if not context or context.get('portal'):
return False
return uid
def onchange_partner_address_id(self, cr, uid, ids, add, email=False, context=None):
""" This function returns value of partner email based on Partner Address
:param add: Id of Partner's address
:param email: Partner's email ID
"""
data = {'value': {'email_from': False, 'phone':False}}
if add:
address = self.pool.get('res.partner').browse(cr, uid, add)
data['value'] = {'partner_name': address and address.name or False,
'email_from': address and address.email or False,
'phone': address and address.phone or False,
'street': address and address.street or False,
'street2': address and address.street2 or False,
'city': address and address.city or False,
'state_id': address.state_id and address.state_id.id or False,
'zip': address and address.zip or False,
'country_id': address.country_id and address.country_id.id or False,
}
fields = self.fields_get(cr, uid, context=context or {})
for key in data['value'].keys():
if key not in fields:
del data['value'][key]
return data
def onchange_partner_id(self, cr, uid, ids, part, email=False):
""" This function returns value of partner address based on partner
:param part: Partner's id
:param email: Partner's email ID
"""
data={}
if part:
addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
return {'value': data}
def _get_default_section_id(self, cr, uid, context=None):
""" Gives default section """
return False
def _get_default_stage_id(self, cr, uid, context=None):
""" Gives default stage_id """
return self.stage_find(cr, uid, [], None, [('state', '=', 'draft')], context=context)
def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
""" Find stage, with a given (optional) domain on the search,
ordered by the order parameter. If several stages match the
search criterions, the first one will be returned, according
to the requested search order.
This method is meant to be overriden by subclasses. That way
specific behaviors can be achieved for every class inheriting
from base_stage.
:param cases: browse_record of cases
:param section_id: section limitating the search, given for
a generic search (for example default search).
A section models concepts such as Sales team
(for CRM), ou departments (for HR).
:param domain: a domain on the search of stages
:param order: order of the search
"""
return False
def stage_set_with_state_name(self, cr, uid, cases, state_name, context=None):
""" Set a new stage, with a state_name instead of a stage_id
:param cases: browse_record of cases
"""
if isinstance(cases, (int, long)):
cases = self.browse(cr, uid, cases, context=context)
for case in cases:
stage_id = self.stage_find(cr, uid, [case], None, [('state', '=', state_name)], context=context)
if stage_id:
self.stage_set(cr, uid, [case.id], stage_id, context=context)
return True
def stage_set(self, cr, uid, ids, stage_id, context=None):
""" Set the new stage. This methods is the right method to call
when changing states. It also checks whether an onchange is
defined, and execute it.
"""
value = {}
if hasattr(self, 'onchange_stage_id'):
value = self.onchange_stage_id(cr, uid, ids, stage_id, context=context)['value']
value['stage_id'] = stage_id
return self.write(cr, uid, ids, value, context=context)
def stage_change(self, cr, uid, ids, op, order, context=None):
""" Change the stage and take the next one, based on a condition
writen for the 'sequence' field and an operator. This methods
checks whether the case has a current stage, and takes its
sequence. Otherwise, a default 0 sequence is chosen and this
method will therefore choose the first available stage.
For example if op is '>' and current stage has a sequence of
10, this will call stage_find, with [('sequence', '>', '10')].
"""
for case in self.browse(cr, uid, ids, context=context):
seq = 0
if case.stage_id:
seq = case.stage_id.sequence or 0
section_id = None
next_stage_id = self.stage_find(cr, uid, [case], None, [('sequence', op, seq)],order, context=context)
if next_stage_id:
return self.stage_set(cr, uid, [case.id], next_stage_id, context=context)
return False
def stage_next(self, cr, uid, ids, context=None):
""" This function computes next stage for case from its current stage
using available stage for that case type
"""
return self.stage_change(cr, uid, ids, '>','sequence', context)
def stage_previous(self, cr, uid, ids, context=None):
""" This function computes previous stage for case from its current
stage using available stage for that case type
"""
return self.stage_change(cr, uid, ids, '<', 'sequence desc', context)
def copy(self, cr, uid, id, default=None, context=None):
""" Overrides orm copy method to avoid copying messages,
as well as date_closed and date_open columns if they
exist."""
if default is None:
default = {}
if hasattr(self, '_columns'):
if self._columns.get('date_closed'):
default.update({ 'date_closed': False, })
if self._columns.get('date_open'):
default.update({ 'date_open': False })
return super(base_stage, self).copy(cr, uid, id, default, context=context)
def case_escalate(self, cr, uid, ids, context=None):
""" Escalates case to parent level """
for case in self.browse(cr, uid, ids, context=context):
data = {'active': True}
if case.section_id.parent_id:
data['section_id'] = case.section_id.parent_id.id
if case.section_id.parent_id.change_responsible:
if case.section_id.parent_id.user_id:
data['user_id'] = case.section_id.parent_id.user_id.id
else:
raise osv.except_osv(_('Error!'), _("You are already at the top level of your sales-team category.\nTherefore you cannot escalate furthermore."))
self.write(cr, uid, [case.id], data, context=context)
return True
def case_open(self, cr, uid, ids, context=None):
""" Opens case """
cases = self.browse(cr, uid, ids, context=context)
for case in cases:
data = {'active': True}
if not case.user_id:
data['user_id'] = uid
self.case_set(cr, uid, [case.id], 'open', data, context=context)
return True
def case_close(self, cr, uid, ids, context=None):
""" Closes case """
return self.case_set(cr, uid, ids, 'done', {'active': True, 'date_closed': fields.datetime.now()}, context=context)
def case_cancel(self, cr, uid, ids, context=None):
""" Cancels case """
return self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
def case_pending(self, cr, uid, ids, context=None):
""" Set case as pending """
return self.case_set(cr, uid, ids, 'pending', {'active': True}, context=context)
def case_reset(self, cr, uid, ids, context=None):
""" Resets case as draft """
return self.case_set(cr, uid, ids, 'draft', {'active': True}, context=context)
def case_set(self, cr, uid, ids, new_state_name=None, values_to_update=None, new_stage_id=None, context=None):
""" Generic method for setting case. This methods wraps the update
of the record.
:params new_state_name: the new state of the record; this method
will call ``stage_set_with_state_name``
that will find the stage matching the
new state, using the ``stage_find`` method.
:params new_stage_id: alternatively, you may directly give the
new stage of the record
:params state_name: the new value of the state, such as
'draft' or 'close'.
:params update_values: values that will be added with the state
update when writing values to the record.
"""
cases = self.browse(cr, uid, ids, context=context)
# 1. update the stage
if new_state_name:
self.stage_set_with_state_name(cr, uid, cases, new_state_name, context=context)
elif not (new_stage_id is None):
self.stage_set(cr, uid, ids, new_stage_id, context=context)
# 2. update values
if values_to_update:
self.write(cr, uid, ids, values_to_update, context=context)
return True
+80
View File
@@ -0,0 +1,80 @@
# Bosnian 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-08-08 22:20+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Bosnian <bs@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-08-09 05:06+0000\n"
"X-Generator: Launchpad (build 16723)\n"
#. module: base_status
#: code:addons/base_status/base_state.py:107
#, python-format
msgid "Error !"
msgstr "Greška !"
#. module: base_status
#: code:addons/base_status/base_state.py:166
#, python-format
msgid "%s has been <b>opened</b>."
msgstr "%s je bio <b>otvoren</b>."
#. module: base_status
#: code:addons/base_status/base_state.py:199
#, python-format
msgid "%s has been <b>renewed</b>."
msgstr "%s je bio <b>obnovljen</b>."
#. module: base_status
#: code:addons/base_status/base_stage.py:210
#, python-format
msgid "Error!"
msgstr "Greška!"
#. module: base_status
#: code:addons/base_status/base_state.py:107
#, python-format
msgid ""
"You can not escalate, you are already at the top level regarding your sales-"
"team category."
msgstr ""
"Ne možete eskalirati, već ste na najvišem nivou u odnosu na kategoriju "
"prodajnog tima."
#. module: base_status
#: code:addons/base_status/base_state.py:193
#, python-format
msgid "%s is now <b>pending</b>."
msgstr "%s je sad <b>Na čekanju</b>."
#. module: base_status
#: code:addons/base_status/base_state.py:187
#, python-format
msgid "%s has been <b>canceled</b>."
msgstr "%s je bio <b>otkazan</b>."
#. module: base_status
#: code:addons/base_status/base_stage.py:210
#, python-format
msgid ""
"You are already at the top level of your sales-team category.\n"
"Therefore you cannot escalate furthermore."
msgstr ""
"Već ste na najvišem nivou vaše kategorije prodajnog tima.\n"
"Zato ne možete dalje eskalirati."
#. module: base_status
#: code:addons/base_status/base_state.py:181
#, python-format
msgid "%s has been <b>closed</b>."
msgstr "%s je bio <b>zatvoren</b>."
+1
View File
@@ -116,6 +116,7 @@ Dashboard for CRM will include:
'test/crm_lead_onchange.yml',
'test/crm_lead_copy.yml',
'test/crm_lead_unlink.yml',
'test/crm_lead_find_stage.yml',
],
'css': [
'static/src/css/crm.css'
+8 -3
View File
@@ -19,7 +19,11 @@
<field name="view_type">form</field>
<field name="view_mode">graph,tree,form</field>
<field name="view_id" ref="view_crm_opportunity_stage_graph"/>
<field name="domain">[('state', 'not in', ('done', 'cancel')), ('type', '=', 'opportunity')]</field>
<!-- avoid done / cancelled -->
<field name="domain">['|',
'!', '&amp;', ('probability', '=', 100), ('stage_id.on_change', '=', 1),
'!', '&amp;', ('probability', '=', 0), ('stage_id.sequence', '!=', 1),
('type', '=', 'opportunity')]</field>
<field name="context">{'search_default_Stage':1}</field>
</record>
@@ -43,8 +47,9 @@
<field name="view_type">form</field>
<field name="view_mode">graph,tree,form</field>
<field name="view_id" ref="view_crm_opportunity_user_stage_graph"/>
<field name="domain">[('state','!=','cancel'),('opening_date','&gt;',context_today().strftime("%Y-%m-%d"))]</field>
<field name="context">{'search_default_Stage':1}</field>
<!-- avoid cancelled -->
<field name="domain">['!', '&amp;', ('probability', '=', 0), ('stage_id.sequence', '!=', 1)]</field>
<field name="context">{'search_default_user': 1, 'search_default_Stage': 1}</field>
</record>
<record model="ir.ui.view" id="board_crm_statistical_form">
+3 -15
View File
@@ -26,15 +26,6 @@ from openerp import tools
from openerp.osv import fields
from openerp.osv import osv
MAX_LEVEL = 15
AVAILABLE_STATES = [
('draft', 'New'),
('cancel', 'Cancelled'),
('open', 'In Progress'),
('pending', 'Pending'),
('done', 'Closed')
]
AVAILABLE_PRIORITIES = [
('1', 'Highest'),
('2', 'High'),
@@ -72,16 +63,13 @@ class crm_case_stage(osv.osv):
'probability': fields.float('Probability (%)', required=True, help="This percentage depicts the default/average probability of the Case for this stage to be a success"),
'on_change': fields.boolean('Change Probability Automatically', help="Setting this stage will change the probability automatically on the opportunity."),
'requirements': fields.text('Requirements'),
'section_ids':fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', string='Sections',
'section_ids': fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', string='Sections',
help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
'state': fields.selection(AVAILABLE_STATES, 'Related Status', required=True,
help="The status of your document will automatically change regarding the selected stage. " \
"For example, if a stage is related to the status 'Close', when your document reaches this stage, it is automatically closed."),
'case_default': fields.boolean('Default to New Sales Team',
help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
'fold': fields.boolean('Fold by Default',
help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
'type': fields.selection([ ('lead','Lead'),
'type': fields.selection([('lead', 'Lead'),
('opportunity', 'Opportunity'),
('both', 'Both')],
string='Type', size=16, required=True,
@@ -91,7 +79,7 @@ class crm_case_stage(osv.osv):
_defaults = {
'sequence': lambda *args: 1,
'probability': lambda *args: 0.0,
'state': 'open',
'on_change': True,
'fold': False,
'type': 'both',
'case_default': True,
+2 -8
View File
@@ -5,7 +5,7 @@
<record id="filter_draft_lead" model="ir.filters">
<field name="name">Draft Leads</field>
<field name="model_id">crm.lead</field>
<field name="domain">[('state','=','draft')]</field>
<field name="domain">[('stage_id.sequence', '=', 1)]</field>
<field name="user_id" eval="False"/>
</record>
<record id="action_email_reminder_lead" model="ir.actions.server">
@@ -14,13 +14,7 @@
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="state">email</field>
<field name="email">object.user_id.email</field>
<field name="subject">Reminder on Lead: [[object.id ]] [[object.partner_id and 'of ' +object.partner_id.name or '']]</field>
<field name="message">Warning unprocessed incoming lead is more than 5 day old.
Name: [[object.name ]]
ID: [[object.id ]]
Description: [[object.description]]
</field>
<field name="template_id" ref="email_template_opportunity_reminder_mail"/>
</record>
<record id="rule_set_reminder_lead" model="base.action.rule">
<field name="name">Set Auto Reminder on leads which are not open since 5 days.</field>
+53 -117
View File
@@ -22,14 +22,13 @@
import crm
from datetime import datetime
from operator import itemgetter
from openerp.osv import fields, osv, orm
import time
from openerp import SUPERUSER_ID
from openerp import tools
from openerp.tools.translate import _
from openerp.tools import html2plaintext
from openerp.addons.base.res.res_partner import format_address
from openerp.osv import fields, osv, orm
from openerp.tools import html2plaintext
from openerp.tools.translate import _
CRM_LEAD_FIELDS_TO_MERGE = ['name',
'partner_id',
@@ -61,11 +60,7 @@ CRM_LEAD_FIELDS_TO_MERGE = ['name',
'email_from',
'email_cc',
'partner_name']
CRM_LEAD_PENDING_STATES = (
crm.AVAILABLE_STATES[2][0], # Cancelled
crm.AVAILABLE_STATES[3][0], # Done
crm.AVAILABLE_STATES[4][0], # Pending
)
class crm_lead(format_address, osv.osv):
""" CRM Lead Case """
@@ -75,13 +70,11 @@ class crm_lead(format_address, osv.osv):
_inherit = ['mail.thread', 'ir.needaction_mixin']
_track = {
'state': {
'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj.state in ['new', 'draft'],
'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj.state == 'done',
'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.state == 'cancel',
},
'stage_id': {
'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: obj.state not in ['new', 'draft', 'cancel', 'done'],
'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.sequence == 1,
'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: (obj.stage_id and obj.stage_id.sequence != 1) and obj.probability < 100,
'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj.probability == 100 and obj.stage_id and obj.stage_id.on_change,
'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.sequence != 1,
},
}
@@ -99,7 +92,7 @@ class crm_lead(format_address, osv.osv):
def _get_default_stage_id(self, cr, uid, context=None):
""" Gives default stage_id """
section_id = self._get_default_section_id(cr, uid, context=context)
return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft')], context=context)
return self.stage_find(cr, uid, [], section_id, [('sequence', '=', '1')], context=context)
def _resolve_section_id_from_context(self, cr, uid, context=None):
""" Returns ID of section based on the value of 'section_id'
@@ -150,7 +143,7 @@ class crm_lead(format_address, osv.osv):
stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
# restore order of the search
result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
fold = {}
for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
@@ -158,7 +151,7 @@ class crm_lead(format_address, osv.osv):
return result, fold
def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
res = super(crm_lead, self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
if view_type == 'form':
res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
return res
@@ -220,17 +213,6 @@ class crm_lead(format_address, osv.osv):
res[lead.id][field] = abs(int(duration))
return res
def _history_search(self, cr, uid, obj, name, args, context=None):
res = []
msg_obj = self.pool.get('mail.message')
message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
if lead_ids:
return [('id', 'in', lead_ids)]
else:
return [('id', '=', '0')]
_columns = {
'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', track_visibility='onchange',
select=True, help="Linked partner (optional). Usually created when converting the lead."),
@@ -243,10 +225,10 @@ class crm_lead(format_address, osv.osv):
'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
'section_id': fields.many2one('crm.case.section', 'Sales Team',
select=True, track_visibility='onchange', help='When sending mails, the default email address is taken from the sales team.'),
'create_date': fields.datetime('Creation Date' , readonly=True),
'email_cc': fields.text('Global CC', size=252 , help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
'create_date': fields.datetime('Creation Date', readonly=True),
'email_cc': fields.text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
'description': fields.text('Notes'),
'write_date': fields.datetime('Update Date' , readonly=True),
'write_date': fields.datetime('Update Date', readonly=True),
'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
@@ -264,17 +246,15 @@ class crm_lead(format_address, osv.osv):
domain="['&', ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
'user_id': fields.many2one('res.users', 'Salesperson', select=True, track_visibility='onchange'),
'referred': fields.char('Referred By', size=64),
'date_open': fields.datetime('Opened', readonly=True),
'date_open': fields.datetime('Assigned', readonly=True),
'day_open': fields.function(_compute_day, string='Days to Open', \
multi='day_open', type="float", store=True),
'day_close': fields.function(_compute_day, string='Days to Close', \
multi='day_close', type="float", store=True),
'state': fields.related('stage_id', 'state', type="selection", store=True,
selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
help='The Status is set to \'Draft\', when a case is created. If the case is in progress the Status is set to \'Open\'. When the case is over, the Status is set to \'Done\'. If the case needs to be reviewed then the Status is set to \'Pending\'.'),
'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
# Only used for type opportunity
'probability': fields.float('Success Rate (%)',group_operator="avg"),
'probability': fields.float('Success Rate (%)', group_operator="avg"),
'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
'ref': fields.reference('Reference', selection=crm._links_get, size=128),
'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
@@ -316,6 +296,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(),
}
_sql_constraints = [
@@ -325,7 +306,7 @@ class crm_lead(format_address, osv.osv):
def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
if not stage_id:
return {'value': {}}
stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context=context)
if not stage.on_change:
return {'value': {}}
return {'value': {'probability': stage.probability}}
@@ -358,22 +339,6 @@ class crm_lead(format_address, osv.osv):
section_id = section_ids[0]
return {'value': {'section_id': section_id}}
def _check(self, cr, uid, ids=False, context=None):
""" Override of the base.stage method.
Function called by the scheduler to process cases for date actions
Only works on not done and cancelled cases
"""
cr.execute('select * from crm_case \
where (date_action_last<%s or date_action_last is null) \
and (date_action_next<=%s or date_action_next is null) \
and state not in (\'cancel\',\'done\')',
(time.strftime("%Y-%m-%d %H:%M:%S"),
time.strftime('%Y-%m-%d %H:%M:%S')))
ids2 = map(lambda x: x[0], cr.fetchall() or [])
cases = self.browse(cr, uid, ids2, context=context)
return self._action(cr, uid, cases, False, context=context)
def stage_find(self, cr, uid, cases, section_id, domain=None, order='sequence', context=None):
""" Override of the base.stage method
Parameter of the stage search taken from the lead:
@@ -385,16 +350,16 @@ class crm_lead(format_address, osv.osv):
if isinstance(cases, (int, long)):
cases = self.browse(cr, uid, cases, context=context)
# collect all section_ids
section_ids = []
section_ids = set()
types = ['both']
if not cases:
type = context.get('default_type')
types += [type]
ctx_type = context.get('default_type')
types += [ctx_type]
if section_id:
section_ids.append(section_id)
section_ids.add(section_id)
for lead in cases:
if lead.section_id:
section_ids.append(lead.section_id.id)
section_ids.add(lead.section_id.id)
if lead.type not in types:
types.append(lead.type)
# OR all section_ids and OR with case_default
@@ -403,8 +368,7 @@ class crm_lead(format_address, osv.osv):
search_domain += [('|')] * len(section_ids)
for section_id in section_ids:
search_domain.append(('section_ids', '=', section_id))
else:
search_domain.append(('case_default', '=', True))
search_domain.append(('case_default', '=', True))
# AND with cases types
search_domain.append(('type', 'in', types))
# AND with the domain in parameter
@@ -415,27 +379,29 @@ class crm_lead(format_address, osv.osv):
return stage_ids[0]
return False
def stage_set(self, cr, uid, ids, stage_id, context=None):
""" Set the new stage. Now just writes the stage.
TDE TODO: remove me when removing state
"""
return self.write(cr, uid, ids, {'stage_id': stage_id}, context=context)
def case_mark_lost(self, cr, uid, ids, context=None):
""" Mark the case as lost: state=cancel and probability=0 """
for lead in self.browse(cr, uid, ids):
stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0),('on_change','=',True)], context=context)
stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0), ('on_change', '=', True), ('sequence', '>', 1)], context=context)
if stage_id:
self.stage_set(cr, uid, [lead.id], stage_id, context=context)
return True
return self.write(cr, uid, [lead.id], {'stage_id': stage_id}, context=context)
else:
raise osv.except_osv(_('Warning!'),
_('To relieve your sales pipe and group all Lost opportunities, configure one of your sales stage as follow:\n'
'probability = 0 %, select "Change Probability Automatically".\n'
'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
def case_mark_won(self, cr, uid, ids, context=None):
""" Mark the case as won: state=done and probability=100 """
for lead in self.browse(cr, uid, ids):
stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0),('on_change','=',True)], context=context)
stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0), ('on_change', '=', True), ('sequence', '>', 1)], context=context)
if stage_id:
self.stage_set(cr, uid, [lead.id], stage_id, context=context)
return True
return self.write(cr, uid, [lead.id], {'stage_id': stage_id}, context=context)
else:
raise osv.except_osv(_('Warning!'),
_('To relieve your sales pipe and group all Won opportunities, configure one of your sales stage as follow:\n'
'probability = 100 % and select "Change Probability Automatically".\n'
'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
def case_escalate(self, cr, uid, ids, context=None):
""" Escalates case to parent level """
@@ -451,20 +417,20 @@ class crm_lead(format_address, osv.osv):
self.write(cr, uid, [case.id], data, context=context)
return True
def set_priority(self, cr, uid, ids, priority):
def set_priority(self, cr, uid, ids, priority, context=None):
""" Set lead priority
"""
return self.write(cr, uid, ids, {'priority' : priority})
return self.write(cr, uid, ids, {'priority': priority}, context=context)
def set_high_priority(self, cr, uid, ids, context=None):
""" Set lead priority to high
"""
return self.set_priority(cr, uid, ids, '1')
return self.set_priority(cr, uid, ids, '1', context=context)
def set_normal_priority(self, cr, uid, ids, context=None):
""" Set lead priority to normal
"""
return self.set_priority(cr, uid, ids, '3')
return self.set_priority(cr, uid, ids, '3', context=context)
def _merge_get_result_type(self, cr, uid, opps, context=None):
"""
@@ -640,7 +606,8 @@ class crm_lead(format_address, osv.osv):
sequenced_opps = []
for opportunity in opportunities:
sequence = -1
if opportunity.stage_id and opportunity.stage_id.state != 'cancel':
# TDE: was "if opportunity.stage_id and opportunity.stage_id.state != 'cancel':"
if opportunity.probability == 0 and opportunity.stage_id and opportunity.stage_id.sequence != 1 and opportunity.stage_id.fold:
sequence = opportunity.stage_id.sequence
sequenced_opps.append(((int(sequence != -1 and opportunity.type == 'opportunity'), sequence, -opportunity.id), opportunity))
@@ -701,7 +668,7 @@ class crm_lead(format_address, osv.osv):
'phone': customer and customer.phone or lead.phone,
}
if not lead.stage_id or lead.stage_id.type=='lead':
val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('state', '=', 'draft'),('type', 'in', ('opportunity','both'))], context=context)
val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('sequence', '=', '1'), ('type', 'in', ('opportunity','both'))], context=context)
return val
def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
@@ -710,7 +677,8 @@ class crm_lead(format_address, osv.osv):
partner = self.pool.get('res.partner')
customer = partner.browse(cr, uid, partner_id, context=context)
for lead in self.browse(cr, uid, ids, context=context):
if lead.state in ('done', 'cancel'):
# TDE: was if lead.state in ('done', 'cancel'):
if (lead.probability == '100') or (lead.probability == '0' and lead.stage_id.sequence != '1'):
continue
vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
self.write(cr, uid, [lead.id], vals, context=context)
@@ -940,47 +908,15 @@ class crm_lead(format_address, osv.osv):
return super(crm_lead, self).create(cr, uid, vals, context=create_context)
def write(self, cr, uid, ids, vals, context=None):
# stage change: update date_last_stage_update
if 'stage_id' in vals:
vals['date_last_stage_update'] = fields.datetime.now()
# stage change with new stage: update probability
if vals.get('stage_id') and not vals.get('probability'):
onchange_stage_values = self.onchange_stage_id(cr, uid, ids, vals.get('stage_id'), context=context)['value']
vals.update(onchange_stage_values)
return super(crm_lead, self).write(cr, uid, ids, vals, context=context)
def new_mail_send(self, cr, uid, ids, context=None):
'''
This function opens a window to compose an email, with the edi sale template message loaded by default
'''
assert len(ids) == 1, 'This option should only be used for a single id at a time.'
ir_model_data = self.pool.get('ir.model.data')
try:
template_id = ir_model_data.get_object_reference(cr, uid, 'crm', 'email_template_opportunity_mail')[1]
except ValueError:
template_id = False
try:
compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
except ValueError:
compose_form_id = False
if context is None:
context = {}
ctx = context.copy()
ctx.update({
'default_model': 'crm.lead',
'default_res_id': ids[0],
'default_use_template': bool(template_id),
'default_template_id': template_id,
'default_composition_mode': 'comment',
})
return {
'name': _('Compose Email'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(compose_form_id, 'form')],
'view_id': compose_form_id,
'target': 'new',
'context': ctx,
}
# ----------------------------------------
# Mail Gateway
# ----------------------------------------
+44 -44
View File
@@ -5,71 +5,57 @@
<!-- Crm stages -->
<record model="crm.case.stage" id="stage_lead1">
<field name="name">New</field>
<field eval="1" name="case_default"/>
<field name="state">draft</field>
<field eval="0" name="probability"/>
<field eval="10" name="sequence"/>
<field name="case_default">1</field>
<field name="probability">0</field>
<field name="on_change">1</field>
<field name="sequence">1</field>
<field name="type">both</field>
</record>
<record model="crm.case.stage" id="stage_lead2">
<field name="name">Opportunity</field>
<field eval="1" name="case_default"/>
<field name="state">open</field>
<field eval="20" name="probability"/>
<field eval="20" name="sequence"/>
<field name="type">lead</field>
</record>
<record model="crm.case.stage" id="stage_lead7">
<field name="name">Dead</field>
<field eval="1" name="case_default"/>
<field eval="False" name="fold"/>
<field name="state">cancel</field>
<field eval="0" name="probability"/>
<field eval="30" name="sequence"/>
<field name="case_default">1</field>
<field name="probability">0</field>
<field name="on_change">1</field>
<field name="sequence">30</field>
<field name="type">lead</field>
</record>
<record model="crm.case.stage" id="stage_lead3">
<field name="name">Qualification</field>
<field eval="1" name="case_default"/>
<field name="state">open</field>
<field eval="20" name="probability"/>
<field eval="100" name="sequence"/>
<field name="case_default">1</field>
<field name="probability">20</field>
<field name="on_change">1</field>
<field name="sequence">40</field>
<field name="type">opportunity</field>
</record>
<record model="crm.case.stage" id="stage_lead4">
<field name="name">Proposition</field>
<field eval="1" name="case_default"/>
<field name="state">open</field>
<field eval="40" name="probability"/>
<field eval="110" name="sequence"/>
<field name="case_default">1</field>
<field name="probability">40</field>
<field name="sequence">50</field>
<field name="type">opportunity</field>
</record>
<record model="crm.case.stage" id="stage_lead5">
<field name="name">Negotiation</field>
<field eval="1" name="case_default"/>
<field name="state">open</field>
<field eval="60" name="probability"/>
<field eval="120" name="sequence"/>
<field name="case_default">1</field>
<field name="probability">60</field>
<field name="sequence">60</field>
<field name="type">opportunity</field>
</record>
<record model="crm.case.stage" id="stage_lead6">
<field name="name">Won</field>
<field eval="True" name="fold"/>
<field eval="1" name="case_default"/>
<field name="state">done</field>
<field eval="100" name="probability"/>
<field eval="130" name="sequence"/>
<field eval="1" name="on_change"/>
<field name="case_default">1</field>
<field name="probability">100</field>
<field name="on_change">1</field>
<field name="sequence">70</field>
<field name="type">opportunity</field>
</record>
<record model="crm.case.stage" id="stage_lead8">
<record model="crm.case.stage" id="stage_lead7">
<field name="name">Lost</field>
<field eval="1" name="case_default"/>
<field eval="True" name="fold"/>
<field eval="1" name="on_change"/>
<field name="state">cancel</field>
<field eval="0" name="probability"/>
<field eval="140" name="sequence"/>
<field name="case_default">1</field>
<field name="fold">1</field>
<field name="probability">0</field>
<field name="on_change">1</field>
<field name="sequence">80</field>
<field name="type">opportunity</field>
</record>
@@ -77,7 +63,7 @@
<field name="stage_ids" eval="[ (4, ref('stage_lead1')), (4, ref('stage_lead2')),
(4, ref('stage_lead3')), (4, ref('stage_lead4')),
(4, ref('stage_lead5')), (4, ref('stage_lead6')),
(4, ref('stage_lead7')), (4, ref('stage_lead8'))]"/>
(4, ref('stage_lead7'))]"/>
</record>
<!-- Crm campain -->
@@ -161,7 +147,7 @@
<field name="name">Lead Created</field>
<field name="res_model">crm.lead</field>
<field name="default" eval="False"/>
<field name="description">Opportunity created</field>
<field name="description">Lead created</field>
</record>
<record id="mt_lead_stage" model="mail.message.subtype">
<field name="name">Stage Changed</field>
@@ -220,6 +206,20 @@
<field name="email_to">${not object.partner_id and object.email_from}</field>
<field name="body_html"></field>
</record>
<record id="email_template_opportunity_reminder_mail" model="email.template">
<field name="name">Reminder to User</field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="auto_delete" eval="True"/>
<field name="email_from">admin@example.com</field>
<field name="email_to">${object.user_id != False and object.user_id.email}</field>
<field name="subject">Reminder on Lead: ${object.id} from ${object.partner_id != False and object.partner_id.name or object.contact_name}</field>
<field name="body_html"><![CDATA[<p>This opportunity did not have any activity since at least 5 days. Here are some details:</p>
<ul>
<li>Name: ${object.name}</li>
<li>ID: ${object.id}</li>
<li>Description: ${object.description}</field></li>
</ul>]]></field>
</record>
</data>
</openerp>
+85 -93
View File
@@ -6,9 +6,9 @@
<record id="crm_case_1" model="crm.lead">
<field name="type">lead</field>
<field name="name">Plan to Attend a Training</field>
<field name="contact_name">Jason Dunagan</field>
<field name="contact_name">Jacques Dunagan</field>
<field name="partner_name">Le Club SARL</field>
<field name="email_from">jason@leclub.fr</field>
<field name="email_from">jdunagan@leclub.fr</field>
<field name="partner_id" ref=""/>
<field name="function">Training Manager</field>
<field name="street">73, rue Léon Dierx</field>
@@ -20,14 +20,25 @@
<field name="categ_ids" eval="[(6, 0, [categ_oppor6])]"/>
<field name="channel_id" ref="crm_case_channel_email"/>
<field name="priority">1</field>
<field name="section_id" ref="crm_case_section_2"/>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="stage_lead1"/>
<field name="description">Hello,
I am Jason from Le Club SARL.
I am intertested to attend a training organized in your company.
Can you send me the details ?</field>
<field eval="1" name="active"/>
I am Jacques from Le Club SARL.
I am interested to attend a training organized in your company.
Could you please send me the details ?</field>
</record>
<record id="crm_case_1" model="crm.lead">
<field name="create_date" eval="(DateTime.today() - relativedelta(months=2)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<record id="msg_case1_1" model="mail.message">
<field name="subject">Inquiry</field>
<field name="model">crm.lead</field>
<field name="res_id" ref="crm_case_1"/>
<field name="body"><![CDATA[<p>Hello,<br />
I am Jacques from Le Club SARL. I am interested to attend a training organized in your company.<br />
Can you send me the details ?</p>]]></field>
<field name="type">email</field>
</record>
<record id="crm_case_2" model="crm.lead">
@@ -44,10 +55,20 @@ Can you send me the details ?</field>
<field name="categ_ids" eval="[(6, 0, [categ_oppor2])]"/>
<field name="channel_id" ref="crm_case_channel_website"/>
<field name="priority">4</field>
<field name="section_id" ref="crm_case_section_2"/>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="stage_lead1"/>
<field eval="1" name="active"/>
</record>
<record id="crm_case_2" model="crm.lead">
<field name="create_date" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<record id="msg_case2_1" model="mail.message">
<field name="subject">Need Details</field>
<field name="model">crm.lead</field>
<field name="author_id" ref="base.partner_demo"/>
<field name="res_id" ref="crm_case_2"/>
<field name="body">Want to know features and benefits to use the new software.</field>
<field name="type">comment</field>
</record>
<record id="crm_case_3" model="crm.lead">
@@ -65,8 +86,10 @@ Can you send me the details ?</field>
<field name="priority">2</field>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="stage_lead2"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="stage_lead1"/>
</record>
<record id="crm_case_2" model="crm.lead">
<field name="create_date" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<record id="crm_case_4" model="crm.lead">
@@ -82,10 +105,12 @@ Can you send me the details ?</field>
<field name="categ_ids" eval="[(6, 0, [categ_oppor5])]"/>
<field name="channel_id" ref=""/>
<field name="priority">3</field>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref=""/>
<field name="stage_id" ref="stage_lead7"/>
<field eval="1" name="active"/>
<field name="section_id" ref="crm_case_section_2"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="stage_lead6"/>
</record>
<record id="crm_case_2" model="crm.lead">
<field name="create_date" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<record id="crm_case_5" model="crm.lead">
@@ -105,8 +130,8 @@ Can you send me the details ?</field>
<field name="categ_ids" eval="[(6, 0, [categ_oppor1])]"/>
<field name="channel_id" ref="crm_case_channel_website"/>
<field name="priority">3</field>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_root"/>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref=""/>
<field name="stage_id" ref="stage_lead1"/>
<field name="description">Hi,
Can you send me a quotation for 20 computers with speakers?
@@ -116,7 +141,6 @@ Purchase Manager
Stonage IT,
Franklinville
Contact: +1 813 494 5005</field>
<field eval="1" name="active"/>
</record>
<record id="crm_case_6" model="crm.lead">
@@ -134,9 +158,8 @@ Contact: +1 813 494 5005</field>
<field name="channel_id" ref=""/>
<field name="priority">3</field>
<field name="section_id" ref="crm_case_section_2"/>
<field name="user_id" ref="base.user_root"/>
<field name="user_id" ref=""/>
<field name="stage_id" ref="stage_lead1"/>
<field eval="1" name="active"/>
</record>
<record id="crm_case_7" model="crm.lead">
@@ -153,9 +176,8 @@ Contact: +1 813 494 5005</field>
<field name="channel_id" ref=""/>
<field name="priority">5</field>
<field name="section_id" ref="crm_case_section_2"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="stage_lead7"/>
<field eval="1" name="active"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="stage_lead1"/>
</record>
<record id="crm_case_8" model="crm.lead">
@@ -173,9 +195,8 @@ Contact: +1 813 494 5005</field>
<field name="channel_id" ref=""/>
<field name="priority">4</field>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="stage_lead7"/>
<field eval="1" name="active"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="stage_lead1"/>
</record>
<record id="crm_case_9" model="crm.lead">
@@ -193,12 +214,8 @@ Contact: +1 813 494 5005</field>
<field name="channel_id" ref="crm_case_channel_phone"/>
<field name="priority">2</field>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_root"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="stage_lead1"/>
<field eval="1" name="active"/>
</record>
<record id="crm_case_9" model="crm.lead">
<field name="create_date" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<record id="crm_case_10" model="crm.lead">
@@ -215,14 +232,13 @@ Contact: +1 813 494 5005</field>
<field name="channel_id" ref="crm_case_channel_email"/>
<field name="priority">2</field>
<field name="section_id" ref="crm_case_section_2"/>
<field name="user_id" ref=""/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="stage_lead1"/>
<field name="description">Hi,
I would like to know more about specification and cost of laptops of your company.
Thanks,
Andrew</field>
<field eval="1" name="active"/>
</record>
<record id="crm_case_11" model="crm.lead">
@@ -239,9 +255,8 @@ Andrew</field>
<field name="channel_id" ref="crm_case_channel_direct"/>
<field name="priority">3</field>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="user_id" ref=""/>
<field name="stage_id" ref="stage_lead1"/>
<field eval="1" name="active"/>
</record>
<record id="crm_case_12" model="crm.lead">
@@ -258,15 +273,23 @@ Andrew</field>
<field name="channel_id" ref="crm_case_channel_website"/>
<field name="priority">2</field>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="stage_lead2"/>
<field eval="1" name="active"/>
<field name="user_id" ref=""/>
<field name="stage_id" ref="stage_lead1"/>
</record>
<!-- Call Function to Cancel the leads (set as Dead) -->
<function model="crm.lead" name="write"
eval="[ ref('crm_case_7'), ref('crm_case_8'),
ref('crm_case_9'), ref('crm_case_10'),
ref('crm_case_11'), ref('crm_case_12')],
{'stage_id': ref('stage_lead2')},
{'install_mode': True}"
/>
<!-- Call Function to set the leads as Unread -->
<function model="crm.lead" name="message_mark_as_unread"
eval="[ ref('crm_case_1'), ref('crm_case_2'),
ref('crm_case_5'), ref('crm_case_11')], {}"
ref('crm_case_5'), ref('crm_case_6')], {}"
/>
<!-- Demo Opportunities -->
@@ -289,8 +312,7 @@ Andrew</field>
<field name="title_action">Meeting for pricing information.</field>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead1"/>
</record>
<record id="crm_case_14" model="crm.lead">
@@ -316,7 +338,6 @@ Andrew</field>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field eval="1" name="active"/>
</record>
<record id="crm_case_15" model="crm.lead">
@@ -337,10 +358,12 @@ Andrew</field>
<field name="title_action">Call to ask system requirement</field>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="crm.stage_lead6"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="date_closed" eval="(DateTime.today() - relativedelta(months=2)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<record id="crm_case_15" model="crm.lead">
<field name="message_follower_ids" eval="[(3, ref('base.partner_root')), (4, ref('base.partner_demo'))]"/>
</record>
<record id="crm_case_16" model="crm.lead">
<field name="type">opportunity</field>
@@ -362,8 +385,7 @@ Andrew</field>
<field name="title_action">Convert to quote</field>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="crm.stage_lead6"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="date_closed" eval="(DateTime.today() - relativedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
@@ -388,10 +410,12 @@ Andrew</field>
<field name="title_action">Send price list regarding our interventions</field>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="crm.stage_lead6"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead4"/>
<field name="date_closed" eval="(DateTime.today() - relativedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<record id="crm_case_17" model="crm.lead">
<field name="message_follower_ids" eval="[(3, ref('base.partner_root')), (4, ref('base.partner_demo'))]"/>
</record>
<record id="crm_case_18" model="crm.lead">
<field name="type">opportunity</field>
@@ -413,9 +437,12 @@ Andrew</field>
<field name="title_action">Call to define real needs about training</field>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="crm.stage_lead3"/>
<field name="stage_id" ref="crm.stage_lead4"/>
<field eval="1" name="active"/>
</record>
<record id="crm_case_18" model="crm.lead">
<field name="message_follower_ids" eval="[(4, ref('base.partner_demo'))]"/>
</record>
<record id="crm_case_19" model="crm.lead">
<field name="type">opportunity</field>
@@ -437,8 +464,7 @@ Andrew</field>
<field name="title_action">Ask for the good receprion of the proposition</field>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="crm.stage_lead6"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead4"/>
<field name="date_closed" eval="(DateTime.today() - relativedelta(months=3)).strftime('%Y-%m-%d %H:%M')"/>
</record>
@@ -455,8 +481,7 @@ Andrew</field>
<field name="priority">2</field>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="crm.stage_lead1"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead4"/>
</record>
<record id="crm_case_21" model="crm.lead">
@@ -470,8 +495,7 @@ Andrew</field>
<field name="priority">3</field>
<field name="section_id" ref="crm_case_section_2"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="crm.stage_lead6"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead4"/>
<field name="date_closed" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
@@ -489,8 +513,7 @@ Andrew</field>
<field name="priority">3</field>
<field name="section_id" ref="crm_case_section_2"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="crm.stage_lead8"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead5"/>
</record>
<record id="crm_case_23" model="crm.lead">
@@ -505,8 +528,7 @@ Andrew</field>
<field name="priority">5</field>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="crm.stage_lead6"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead5"/>
<field name="date_closed" eval="(DateTime.today() - relativedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
@@ -525,8 +547,7 @@ Andrew</field>
<field eval="time.strftime('%Y-%m-6')" name="date_deadline"/>
<field name="section_id" ref="section_sales_department"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="crm.stage_lead6"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead5"/>
<field name="date_closed" eval="(DateTime.today() - relativedelta(month=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
@@ -549,8 +570,7 @@ Andrew</field>
<field name="title_action">Conf call with technical service</field>
<field name="section_id" ref="crm_case_section_1"/>
<field name="user_id" ref="base.user_root"/>
<field name="stage_id" ref="crm.stage_lead4"/>
<field eval="1" name="active"/>
<field name="stage_id" ref="crm.stage_lead5"/>
</record>
<record id="crm_case_26" model="crm.lead">
@@ -574,21 +594,10 @@ Andrew</field>
<field name="title_action">Send Catalogue by Email</field>
<field name="section_id" ref="crm_case_section_2"/>
<field name="user_id" ref="base.user_demo"/>
<field name="stage_id" ref="crm.stage_lead6"/>
<field eval="1" name="active"/>
<field name="date_closed" eval="(DateTime.today() - relativedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/>
<field name="stage_id" ref="crm.stage_lead5"/>
<!-- <field name="date_closed" eval="(DateTime.today() - relativedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/> -->
</record>
<!-- Unsubscribe Admin from case15, subscribe Demo -->
<record id="crm_case_15" model="crm.lead">
<field name="message_follower_ids" eval="[(3, ref('base.partner_root')), (4, ref('base.partner_demo'))]"/>
</record>
<record id="crm_case_17" model="crm.lead">
<field name="message_follower_ids" eval="[(3, ref('base.partner_root')), (4, ref('base.partner_demo'))]"/>
</record>
<record id="crm_case_18" model="crm.lead">
<field name="message_follower_ids" eval="[(4, ref('base.partner_demo'))]"/>
</record>
<!-- Some messages linked to the previous opportunities -->
<record id="msg_case15_attach1" model="ir.attachment">
<field name="datas">bWlncmF0aW9uIHRlc3Q=</field>
@@ -682,26 +691,9 @@ Andrew</field>
<field name="parent_id" ref="msg_case18_1"/>
<field name="author_id" ref="base.partner_demo"/>
</record>
<record id="msg_case1_1" model="mail.message">
<field name="subject">Inquiry</field>
<field name="model">crm.lead</field>
<field name="res_id" ref="crm_case_1"/>
<field name="body"><![CDATA[<p>Hello,<br />
I am Jason from Le Club SARL. I am interested to attend a training organized in your company.<br />
Can you send me the details ?</p>]]></field>
<field name="type">email</field>
</record>
<record id="msg_case2_1" model="mail.message">
<field name="subject">Need Details</field>
<field name="model">crm.lead</field>
<field name="res_id" ref="crm_case_2"/>
<field name="body">Want to know features and benefits to use the new software.</field>
<field name="type">comment</field>
</record>
<function model="mail.message" name="set_message_starred"
eval="[ ref('msg_case18_1'), ref('msg_case18_2')], True, {}"
/>
</data>
</openerp>
+57 -52
View File
@@ -12,9 +12,10 @@
<field name="model">crm.case.stage</field>
<field name="arch" type="xml">
<search string="Stage Search">
<field name="name" string="Stage Name"/>
<field name="state"/>
<field name="name"/>
<field name="type"/>
<field name="sequence"/>
<field name="probability"/>
</search>
</field>
</record>
@@ -94,7 +95,8 @@
<form string="Leads Form" version="7.0">
<header>
<button name="%(crm.action_crm_lead2opportunity_partner)d" string="Convert to Opportunity" type="action"
states="draft,open,pending" help="Convert to Opportunity" class="oe_highlight"/>
attrs="{'invisible': [('probability', '=', 100)]}"
help="Convert to Opportunity" class="oe_highlight"/>
<field name="stage_id" widget="statusbar" clickable="True"
domain="['&amp;', '|', ('case_default', '=', True), ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"
on_change="onchange_stage_id(stage_id)"/>
@@ -140,12 +142,6 @@
<field name="phone"/>
<field name="mobile"/>
<field name="fax"/>
<!--
This should be integrated in Open Chatter
<button string="Mail"
name="%(mail.action_email_compose_message_wizard)d"
icon="terp-mail-message-new" type="action" colspan="1"/>
-->
</group>
<group>
<field name="user_id" on_change="on_change_user(user_id, context)"
@@ -155,7 +151,7 @@
<field name="section_id"/>
<button name="case_escalate" string="Escalate"
type="object" class="oe_link"
attrs="{'invisible': ['|', ('section_id','=',False), ('state', 'not in', ['draft','open','pending'])]}"/>
attrs="{'invisible': ['|', ('section_id','=',False), ('probability', '=', 100)]}"/>
</div>
<field name="type" invisible="1"/>
</group>
@@ -173,20 +169,24 @@
<field name="description"/>
</page>
<page string="Extra Info">
<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" colspan="2"/>
<field name="state" groups="base.group_no_one"/>
</group>
<group string="Mailings">
<field name="opt_out"/>
</group>
<group string="Misc">
<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>
<field name="probability" groups="base.group_no_one"/>
<field name="active"/>
<field name="referred"/>
</group>
<group>
<field name="date_open" groups="base.group_no_one"/>
<field name="date_closed" groups="base.group_no_one"/>
</group>
</group>
</page>
</notebook>
@@ -216,7 +216,7 @@
<field name="name">Leads</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<tree string="Leads" fonts="bold:message_unread==True" colors="grey:state in ('cancel', 'done')">
<tree string="Leads" fonts="bold:message_unread==True" colors="grey:probability == 100">
<field name="date_deadline" invisible="1"/>
<field name="create_date"/>
<field name="name"/>
@@ -228,7 +228,7 @@
<field name="user_id" invisible="1"/>
<field name="partner_id" invisible="1"/>
<field name="section_id" invisible="context.get('invisible_section', True)" groups="base.group_multi_salesteams"/>
<field name="state" invisible="1"/>
<field name="probability" invisible="1"/>
<field name="type_id" invisible="1"/>
<field name="referred" invisible="1"/>
<field name="channel_id" invisible="1"/>
@@ -258,7 +258,6 @@
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<kanban default_group_by="stage_id">
<field name="state" groups="base.group_no_one"/>
<field name="stage_id"/>
<field name="color"/>
<field name="priority"/>
@@ -331,16 +330,17 @@
<field name="create_date"/>
<field name="country_id" context="{'invisible_country': False}"/>
<separator/>
<filter string="Open" name="open" domain="[('state','!=','cancel')]" help="Open Leads"/>
<filter string="Dead" name="dead" domain="[('state','=','cancel')]"/>
<filter string="Unassigned" name="unassigned" domain="[('user_id','=', False)]" help="No salesperson"/>
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]" help="Unread messages"/>
<filter string="Unassigned" name="unassigned"
domain="[('user_id','=', False)]"
help="No salesperson"/>
<filter string="Assigned to Me"
domain="[('user_id','=',uid)]" context="{'invisible_section': False}"
help="Leads that are assigned to me"/>
<filter string="Assigned to My Team(s)"
<filter string="Assigned to My Team(s)" groups="base.group_multi_salesteams"
domain="[('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}"
help="Leads that are assigned to any sales teams I am member of" groups="base.group_multi_salesteams"/>
help="Leads that are assigned to any sales teams I am member of"/>
<filter string="Dead" name="dead"
domain="[('probability', '=', '0'), ('stage_id.sequence', '!=', 1)]"/>
<separator />
<filter string="Available for mass mailing"
name='not_opt_out' domain="[('opt_out', '=', False)]"
@@ -378,20 +378,19 @@
<field name="arch" type="xml">
<form string="Opportunities" version="7.0">
<header>
<button name="case_mark_won" string="Mark Won" type="object"
states="draft,open,pending" class="oe_highlight"/>
<button name="case_mark_lost" string="Mark Lost" type="object"
states="draft,open" class="oe_highlight"/>
<field name="stage_id" widget="statusbar" clickable="True"/>
<button name="case_mark_won" string="Mark Won" type="object" class="oe_highlight"
attrs="{'invisible': [('probability', '=', 100)]}"/>
<button name="case_mark_lost" string="Mark Lost" type="object" class="oe_highlight"
attrs="{'invisible': ['|', ('probability', '=', 0), ('probability', '=', 100)]}"/>
<field name="stage_id" widget="statusbar" clickable="True"
domain="['&amp;', ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"/>
</header>
<sheet>
<div class="oe_right oe_button_box">
<button string="Schedule/Log Call"
name="%(opportunity2phonecall_act)d"
type="action"/>
<button string="Meeting"
<button string="Schedule/Log Call" type="action"
name="%(opportunity2phonecall_act)d"/>
<button string="Meeting" type="object"
name="action_makeMeeting"
type="object"
context="{'search_default_attendee_id': active_id, 'default_attendee_id' : active_id}"/>
</div>
<div class="oe_title">
@@ -408,7 +407,7 @@
<group>
<group>
<field name="partner_id"
on_change="onchange_partner_id(partner_id, email_from)"
on_change="on_change_partner_id(partner_id)"
string="Customer"
context="{'default_name': partner_name, 'default_email': email_from, 'default_phone': phone}"/>
<field name="email_from" string="Email"/>
@@ -430,7 +429,8 @@
<label for="section_id" groups="base.group_multi_salesteams"/>
<div groups="base.group_multi_salesteams">
<field name="section_id" widget="selection"/>
<button name="case_escalate" string="Escalate" type="object" class="oe_link" attrs="{'invisible': ['|', ('section_id','=',False), ('state', 'not in', ['draft','open','pending'])]}"/>
<button name="case_escalate" string="Escalate" type="object" class="oe_link"
attrs="{'invisible': ['|', ('section_id','=',False), ('probability', '=', 100)]}"/>
</div>
</group>
<group>
@@ -481,7 +481,6 @@
<field name="day_open" groups="base.group_no_one"/>
<field name="day_close" groups="base.group_no_one"/>
<field name="referred"/>
<field name="state" invisible="1"/>
<field name="type" invisible="1"/>
</group>
<group string="References">
@@ -511,7 +510,7 @@
<field name="name">Opportunities Tree</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<tree string="Opportunities" fonts="bold:message_unread==True" colors="gray:state in ('cancel', 'done');red:date_deadline and (date_deadline &lt; current_date)">
<tree string="Opportunities" fonts="bold:message_unread==True" colors="gray:probability == 100;red:date_deadline and (date_deadline &lt; current_date)">
<field name="date_deadline" invisible="1"/>
<field name="create_date"/>
<field name="name" string="Opportunity"/>
@@ -529,7 +528,8 @@
<field name="referred" invisible="1"/>
<field name="priority" invisible="1"/>
<field name="message_unread" invisible="1"/>
<field name="state" invisible="1"/>
<field name="probability" invisible="1"/>
<field name="write_date" invisible="1"/>
</tree>
</field>
</record>
@@ -546,15 +546,19 @@
<field name="section_id" context="{'invisible_section': False}" groups="base.group_multi_salesteams"/>
<field name="user_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="stage_id" domain="[]"/>
<field name="probability"/>
<separator/>
<filter string="New" name="new" domain="[('state','=','draft')]" help="New Opportunities"/>
<filter string="In Progress" name="open" domain="[('state','=','open')]" help="Open Opportunities"/>
<filter string="Won" name="won" domain="[('state','=','done')]"/>
<filter string="Lost" name="lost" domain="[('state','=','cancel')]"/>
<filter string="Unassigned" name="unassigned" domain="[('user_id','=', False)]" help="No salesperson"/>
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]" help="Unread messages"/>
<filter string="New" name="new"
domain="[('probability', '=', 0), ('stage_id.sequence', '=', 1)]"/>
<filter string="Won" name="won"
domain="[('probability', '=', 100), ('stage_id.on_change', '=', 1)]"/>
<filter string="Lost" name="lost"
domain="[('probability', '=', 0), ('stage_id.sequence', '!=', 1)]"/>
<filter string="Unassigned" name="unassigned"
domain="[('user_id','=', False)]" help="No salesperson"/>
<filter string="My Opportunities" name="assigned_to_me"
domain="[('user_id','=',uid)]" context="{'invisible_section': False}"
domain="[('user_id', '=', uid)]" context="{'invisible_section': False}"
help="Opportunities that are assigned to me"/>
<filter string="Assigned to My Team(s)"
domain="[('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}"
@@ -572,6 +576,7 @@
<filter string="Campaign" domain="[]" context="{'group_by':'type_id'}"/>
<filter string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter string="Creation" domain="[]" context="{'group_by':'create_date'}"/>
<filter string="Last Update Month" domain="[]" context="{'group_by':'write_date'}"/>
</group>
<group string="Display">
<filter string="Show Sales Team" context="{'invisible_section': False}" domain="[]" help="Show Sales Team" groups="base.group_multi_salesteams"/>
-2
View File
@@ -80,7 +80,6 @@
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="probability"/>
<field name="state"/>
<field name="type"/>
</tree>
</field>
@@ -96,7 +95,6 @@
<form string="Stage" version="7.0">
<group col="4">
<field name="name"/>
<field name="state"/>
<field name="probability"/>
<field name="type"/>
<field name="on_change"/>
+6
View File
@@ -6,6 +6,12 @@ Changelog
`trunk (saas-2)`
----------------
- Stage/state update
- ``crm.lead``: removed ``state`` field. Added ``date_last_stage_update`` field
holding last stage_id modification. Updated reports.
- ``crm.case.stage``: removed ``state`` field.
- ``crm``, ``crm_claim``: removed inheritance from ``base_stage`` class. Missing
methods have been added into ``crm`` and ``crm_claim``. Also removed inheritance
in ``crm_helpdesk`` because it uses states, not stages.
+5
View File
@@ -4,6 +4,11 @@ CRM module documentation
CRM documentation topics
'''''''''''''''''''''''''
.. toctree::
:maxdepth: 1
stage_status.rst
Changelog
'''''''''
+61
View File
@@ -0,0 +1,61 @@
.. _stage_status:
Stage and Status
================
.. versionchanged:: 8.0 saas-2 state/stage cleaning
Stage
+++++
This revision removed the concept of state on crm.lead objects. The ``state``
field has been totally removed and replaced by stages, using ``stage_id``. The
following models are impacted:
- ``crm.lead`` now use only stages. However conventions still exist about
'New', 'Won' and 'Lost' stages. Those conventions are:
- ``new``: ``stage_id and stage_id.sequence = 1``
- ``won``: ``stage_id and stage_id.probability = 100 and stage_id.on_change = True``
- ``lost``: ``stage_id and stage_id.probability = 0 and stage_id.on_change = True
and stage_id.sequence != 1``
- ``crm.case.stage`` do not have any ``state`` field anymore.
- ``crm.lead.report`` do not have any ``state`` field anymore.
By default a newly created lead is in a new stage. It means that it will
fetch the stage having ``sequence = 1``. Stage mangement is done using the
kanban view or the clikable statusbar. It is not done using buttons anymore.
Stage analysis
++++++++++++++
Stage analysis can be performed using the newly introduced ``date_last_stage_update``
datetime field. This field is updated everytime ``stage_id`` is updated.
``crm.lead.report`` model also uses the ``date_last_stage_update`` field.
This allows to group and analyse the time spend in the various stages.
Open / Assignation date
+++++++++++++++++++++++
The ``date_open`` field meaning has been updated. It is now set when the ``user_id``
(responsible) is set. It is therefore the assignation date.
Subtypes
++++++++
The following subtypes are triggered on ``crm.lead``:
- ``mt_lead_create``: new leads. Condition: ``obj.probability == 0 and obj.stage_id
and obj.stage_id.sequence == 1``
- ``mt_lead_stage``: stage changed. Condition: ``(obj.stage_id and obj.stage_id.sequence != 1)
and obj.probability < 100``
- ``mt_lead_won``: lead/oportunity is won. condition: `` obj.probability == 100
and obj.stage_id and obj.stage_id.on_change``
- ``mt_lead_lost``: lead/opportunity is lost. Condition: ``obj.probability == 0
and obj.stage_id and obj.stage_id.sequence != 1'``
Those subtypes are also available on the ``crm.case.section`` model and are used
for the auto subscription.
+126 -75
View File
@@ -1,25 +1,26 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * crm
# Spanish (Argentina) translation for openobject-addons
# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 5.0.0\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n"
"PO-Revision-Date: 2009-09-15 15:45+0000\n"
"Last-Translator: Silvana Herrera <sherrera@thymbra.com>\n"
"Language-Team: \n"
"PO-Revision-Date: 2013-08-13 02:17+0000\n"
"Last-Translator: Laureano Kloss <Unknown>\n"
"Language-Team: Spanish (Argentina) <es_AR@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-03-16 05:09+0000\n"
"X-Generator: Launchpad (build 16532)\n"
"X-Launchpad-Export-Date: 2013-08-13 05:00+0000\n"
"X-Generator: Launchpad (build 16723)\n"
#. module: crm
#: view:crm.lead.report:0
msgid "# Leads"
msgstr ""
msgstr "# Iniciativas"
#. module: crm
#: help:sale.config.settings,fetchmail_lead:0
@@ -27,9 +28,11 @@ msgid ""
"Allows you to configure your incoming mail server, and create leads from "
"incoming emails."
msgstr ""
"Le permite configurar su servidor de correo entrante y crear iniciativas "
"desde los correos entrantes."
#. module: crm
#: code:addons/crm/crm_lead.py:881
#: code:addons/crm/crm_lead.py:898
#: selection:crm.case.stage,type:0
#: view:crm.lead:0
#: selection:crm.lead,type:0
@@ -37,13 +40,13 @@ msgstr ""
#: selection:crm.lead.report,type:0
#, python-format
msgid "Lead"
msgstr ""
msgstr "Iniciativa"
#. module: crm
#: view:crm.lead:0
#: field:crm.lead,title:0
msgid "Title"
msgstr ""
msgstr "Denominación"
#. module: crm
#: model:ir.actions.server,message:crm.action_email_reminder_lead
@@ -54,40 +57,45 @@ msgid ""
"Description: [[object.description]]\n"
" "
msgstr ""
"Advertencia: Una iniciativa entrante está sin procesar más de 5 días.\n"
"Nombre: [[object.name ]]\n"
"ID: [[object.id ]]\n"
"Descripción: [[object.description]]\n"
" "
#. module: crm
#: field:crm.opportunity2phonecall,action:0
#: field:crm.phonecall2phonecall,action:0
msgid "Action"
msgstr ""
msgstr "Acción"
#. module: crm
#: model:ir.actions.server,name:crm.action_set_team_sales_department
msgid "Set team to Sales Department"
msgstr ""
msgstr "Establecer como equipo al Departamento de ventas"
#. module: crm
#: view:crm.lead2opportunity.partner.mass:0
msgid "Select Opportunities"
msgstr ""
msgstr "Seleccionar oportunidades"
#. module: crm
#: model:res.groups,name:crm.group_fund_raising
#: field:sale.config.settings,group_fund_raising:0
msgid "Manage Fund Raising"
msgstr ""
msgstr "Administrar recaudación de fondos"
#. module: crm
#: view:crm.lead.report:0
#: field:crm.phonecall.report,delay_close:0
msgid "Delay to close"
msgstr ""
msgstr "Demora para cerrar"
#. module: crm
#: view:crm.case.stage:0
#: field:crm.case.stage,name:0
msgid "Stage Name"
msgstr ""
msgstr "Nombre de etapa"
#. module: crm
#: view:crm.lead:0
@@ -95,52 +103,52 @@ msgstr ""
#: view:crm.lead.report:0
#: view:crm.phonecall.report:0
msgid "Salesperson"
msgstr ""
msgstr "Comercial"
#. module: crm
#: model:ir.model,name:crm.model_crm_lead_report
msgid "CRM Lead Analysis"
msgstr ""
msgstr "Análisis Iniciativas CRM"
#. module: crm
#: view:crm.lead.report:0
#: view:crm.phonecall.report:0
#: field:crm.phonecall.report,day:0
msgid "Day"
msgstr ""
msgstr "Día"
#. module: crm
#: view:crm.lead:0
msgid "Company Name"
msgstr ""
msgstr "Nombre de la compañía"
#. module: crm
#: model:crm.case.categ,name:crm.categ_oppor6
msgid "Training"
msgstr ""
msgstr "Formación"
#. module: crm
#: model:ir.actions.act_window,name:crm.crm_lead_categ_action
#: model:ir.ui.menu,name:crm.menu_crm_lead_categ
msgid "Sales Tags"
msgstr ""
msgstr "Etiquetas de ventas"
#. module: crm
#: view:crm.lead.report:0
msgid "Exp. Closing"
msgstr ""
msgstr "Cierre Esperado"
#. module: crm
#: help:crm.case.section,message_unread:0
#: help:crm.lead,message_unread:0
#: help:crm.phonecall,message_unread:0
msgid "If checked new messages require your attention."
msgstr ""
msgstr "Si está marcado, los nuevos mensajes requerirán su atención"
#. module: crm
#: help:crm.lead.report,creation_day:0
msgid "Creation day"
msgstr ""
msgstr "Fecha creación"
#. module: crm
#: field:crm.segmentation.line,name:0
@@ -151,7 +159,7 @@ msgstr "Nombre de regla"
#: code:addons/crm/crm_phonecall.py:280
#, python-format
msgid "It's only possible to convert one phonecall at a time."
msgstr ""
msgstr "Sólo es posible convertir una llamada telefónica cada vez."
#. module: crm
#: view:crm.case.resource.type:0
@@ -161,17 +169,17 @@ msgstr ""
#: field:crm.lead.report,type_id:0
#: model:ir.model,name:crm.model_crm_case_resource_type
msgid "Campaign"
msgstr ""
msgstr "Campaña"
#. module: crm
#: view:crm.lead:0
msgid "Search Opportunities"
msgstr ""
msgstr "Busqueda de oportunidades"
#. module: crm
#: help:crm.lead.report,deadline_month:0
msgid "Expected closing month"
msgstr ""
msgstr "Mes esperado de cierre"
#. module: crm
#: help:crm.case.section,message_summary:0
@@ -181,14 +189,17 @@ msgid ""
"Holds the Chatter summary (number of messages, ...). This summary is "
"directly in html format in order to be inserted in kanban views."
msgstr ""
"Contiene el resumen del chatter (nº de mensajes, ...). Este resumen está "
"generado directamente en formato HTML para poder ser insertado en las vistas "
"kanban."
#. module: crm
#: code:addons/crm/crm_lead.py:624
#: code:addons/crm/crm_lead.py:744
#: code:addons/crm/crm_lead.py:640
#: code:addons/crm/crm_lead.py:761
#: code:addons/crm/crm_phonecall.py:280
#, python-format
msgid "Warning!"
msgstr ""
msgstr "Alerta!"
#. module: crm
#: view:crm.lead:0
@@ -203,46 +214,46 @@ msgstr ""
#: model:ir.model,name:crm.model_res_partner
#: model:process.node,name:crm.process_node_partner0
msgid "Partner"
msgstr "Partner"
msgstr "Empresa"
#. module: crm
#: view:crm.phonecall:0
#: model:ir.actions.act_window,name:crm.phonecall_to_phonecall_act
msgid "Schedule Other Call"
msgstr ""
msgstr "Planificar otra llamada"
#. module: crm
#: code:addons/crm/crm_phonecall.py:209
#: view:crm.phonecall:0
#, python-format
msgid "Phone Call"
msgstr ""
msgstr "Llamada de teléfono"
#. module: crm
#: field:crm.lead,opt_out:0
msgid "Opt-Out"
msgstr ""
msgstr "No acepta recibir mensajes"
#. module: crm
#: view:crm.lead:0
#: field:crm.lead,state_id:0
msgid "State"
msgstr ""
msgstr "Estado"
#. module: crm
#: field:res.partner,meeting_count:0
msgid "# Meetings"
msgstr ""
msgstr "Nº reuniones"
#. module: crm
#: model:ir.actions.server,name:crm.action_email_reminder_lead
msgid "Reminder to User"
msgstr ""
msgstr "Recordatorio al usuario"
#. module: crm
#: field:crm.segmentation,segmentation_line:0
msgid "Criteria"
msgstr "Criterios"
msgstr "Criterio"
#. module: crm
#: view:crm.segmentation:0
@@ -252,26 +263,28 @@ msgstr "Respuestas excluidas:"
#. module: crm
#: model:ir.model,name:crm.model_crm_merge_opportunity
msgid "Merge opportunities"
msgstr ""
msgstr "Fusionar oportunidades"
#. module: crm
#: view:crm.lead.report:0
#: model:ir.actions.act_window,name:crm.action_report_crm_lead
#: model:ir.ui.menu,name:crm.menu_report_crm_leads_tree
msgid "Leads Analysis"
msgstr ""
msgstr "Análisis de iniciativas"
#. module: crm
#: code:addons/crm/crm_lead.py:1010
#, python-format
msgid "<b>%s a call</b> for the <em>%s</em>."
msgstr ""
"Copy text \t\r\n"
"<b>%s una llamada</b> para el <em>%s</em>."
#. module: crm
#: model:ir.actions.act_window,name:crm.crm_case_resource_type_act
#: model:ir.ui.menu,name:crm.menu_crm_case_resource_type_act
msgid "Campaigns"
msgstr ""
msgstr "Campañas"
#. module: crm
#: view:crm.lead:0
@@ -286,6 +299,9 @@ msgid ""
"If opt-out is checked, this contact has refused to receive emails or "
"unsubscribed to a campaign."
msgstr ""
"Si se marca la casilla 'No acepta recibir mensajes', este contacto ha "
"rehusado recibir correos electrónicos o ha eliminado su suscripción a una "
"campaña."
#. module: crm
#: model:process.transition,name:crm.process_transition_leadpartner0
@@ -293,15 +309,15 @@ msgid "Prospect Partner"
msgstr ""
#. module: crm
#: code:addons/crm/crm_lead.py:982
#: code:addons/crm/crm_lead.py:1002
#, python-format
msgid "No Subject"
msgstr ""
msgstr "Sin título"
#. module: crm
#: field:crm.lead,contact_name:0
msgid "Contact Name"
msgstr ""
msgstr "Nombre del contacto"
#. module: crm
#: help:crm.segmentation,categ_id:0
@@ -309,7 +325,7 @@ msgid ""
"The partner category that will be added to partners that match the "
"segmentation criterions after computation."
msgstr ""
"La categoría del partner que será añadida a las empresas que cumplan los "
"La categoría del partner será añadida a las empresas que cumplan los "
"criterios de segmentación después del cálculo."
#. module: crm
@@ -325,40 +341,50 @@ msgid ""
" </p>\n"
" "
msgstr ""
"<p class=\"oe_view_nocontent_create\">\n"
"Pulse para definir una nueva segmentación de clientes.\n"
"</p><p>\n"
"Cree categorías específicas que puede asignar a sus contactos para "
"administrar mejor sus interacciones con ellos. La herramienta de "
"segmentación es capaz de asignar categorías a los contactos de acuerdo a los "
"criterios que establezca.\n"
"</p>\n"
" "
#. module: crm
#: field:crm.opportunity2phonecall,contact_name:0
#: field:crm.phonecall,partner_id:0
#: field:crm.phonecall2phonecall,contact_name:0
msgid "Contact"
msgstr ""
msgstr "Contacto"
#. module: crm
#: help:crm.case.section,change_responsible:0
msgid ""
"When escalating to this team override the salesman with the team leader."
msgstr ""
"Al escalar a este equipo sobreescribir al comercial con el jefe de equipo."
#. module: crm
#: model:process.transition,name:crm.process_transition_opportunitymeeting0
msgid "Opportunity Meeting"
msgstr ""
msgstr "Reunión de oportunidad"
#. module: crm
#: help:crm.lead.report,delay_close:0
#: help:crm.phonecall.report,delay_close:0
msgid "Number of Days to close the case"
msgstr ""
msgstr "Número de días para cerrar el caso"
#. module: crm
#: model:process.node,note:crm.process_node_opportunities0
msgid "When a real project/opportunity is detected"
msgstr ""
msgstr "Cuando un proyecto/oportunidad es detectado"
#. module: crm
#: field:res.partner,opportunity_ids:0
msgid "Leads and Opportunities"
msgstr ""
msgstr "Iniciativas y oportunidades"
#. module: crm
#: model:ir.actions.act_window,help:crm.relate_partner_opportunities
@@ -377,12 +403,23 @@ msgid ""
" </p>\n"
" "
msgstr ""
"<p class=\"oe_view_nocontent_create\">\n"
"Pulse para crear una oportunidad relacionada con este cliente.\n"
"</p><p>\n"
"Use las oportunidades para seguir la pista al flujo de sus ventas, seguir "
"una venta potencial y prever mejor sus futuros ingresos.\n"
"</p><p>\n"
"Podrá planificar reuniones y llamadas telefónicas desde las oportunidades, "
"convertirlas en ofertas, adjuntar documentos relacionados, rastrear todas "
"las discusiones, y mucho más.\n"
"</p>\n"
" "
#. module: crm
#: model:crm.case.stage,name:crm.stage_lead7
#: view:crm.lead:0
msgid "Dead"
msgstr ""
msgstr "Muerta"
#. module: crm
#: field:crm.case.section,message_unread:0
@@ -390,7 +427,7 @@ msgstr ""
#: field:crm.lead,message_unread:0
#: field:crm.phonecall,message_unread:0
msgid "Unread Messages"
msgstr ""
msgstr "Mensajes no leídos"
#. module: crm
#: view:crm.segmentation:0
@@ -404,17 +441,17 @@ msgstr "Segmentación"
#: selection:crm.lead2opportunity.partner.mass,action:0
#: selection:crm.partner.binding,action:0
msgid "Link to an existing customer"
msgstr ""
msgstr "Enlace a cliente existente"
#. module: crm
#: field:crm.lead,write_date:0
msgid "Update Date"
msgstr ""
msgstr "Actualizar fecha"
#. module: crm
#: field:crm.case.section,user_id:0
msgid "Team Leader"
msgstr ""
msgstr "Jefe de equipo"
#. module: crm
#: help:crm.case.stage,probability:0
@@ -422,6 +459,8 @@ msgid ""
"This percentage depicts the default/average probability of the Case for this "
"stage to be a success"
msgstr ""
"Este porcentaje representa la probabilidad por defecto / media para que los "
"casos de esta etapa tengan éxito."
#. module: crm
#: view:crm.lead:0
@@ -436,24 +475,27 @@ msgstr "Categoría"
#. module: crm
#: view:crm.lead.report:0
msgid "#Opportunities"
msgstr ""
msgstr "# Oportunidades"
#. module: crm
#: code:addons/crm/crm_lead.py:624
#: code:addons/crm/crm_lead.py:640
#, python-format
msgid ""
"Please select more than one element (lead or opportunity) from the list view."
msgstr ""
"Seleccione más de un elemento (iniciativa u oportunidad) desde la vista "
"lista."
#. module: crm
#: view:crm.lead:0
msgid "Leads that are assigned to one of the sale teams I manage, or to me"
msgstr ""
"Iniciativas asignadas a uno de los equipos que gestiono, o a mí mismo"
#. module: crm
#: field:crm.lead,partner_address_email:0
msgid "Partner Contact Email"
msgstr ""
msgstr "Dirección de correo del contacto de la empresa"
#. module: crm
#: model:ir.actions.act_window,help:crm.crm_case_section_act
@@ -467,11 +509,19 @@ msgid ""
" </p>\n"
" "
msgstr ""
"<p class=\"oe_view_nocontent_create\">\n"
"Pulse para definir un nuevo equipo de ventas.\n"
"</p><p>\n"
"Use los equipos de venta para organizar a los diferentes comerciales o "
"departamentos en equipos separados. Cada equipo trabajará en su propia lista "
"de oportunidades.\n"
"</p>\n"
" "
#. module: crm
#: model:process.transition,note:crm.process_transition_opportunitymeeting0
msgid "Normal or phone meeting for opportunity"
msgstr ""
msgstr "Reunión o conferencia telefónica para oportunidad"
#. module: crm
#: field:crm.lead,state:0
@@ -480,12 +530,12 @@ msgstr ""
#: view:crm.phonecall.report:0
#: field:crm.phonecall.report,state:0
msgid "Status"
msgstr ""
msgstr "Estado"
#. module: crm
#: view:crm.lead2opportunity.partner:0
msgid "Create Opportunity"
msgstr ""
msgstr "Crear oportunidad"
#. module: crm
#: view:sale.config.settings:0
@@ -706,7 +756,7 @@ msgid "Statistics Dashboard"
msgstr ""
#. module: crm
#: code:addons/crm/crm_lead.py:861
#: code:addons/crm/crm_lead.py:878
#: model:crm.case.stage,name:crm.stage_lead2
#: selection:crm.case.stage,type:0
#: view:crm.lead:0
@@ -766,7 +816,7 @@ msgid "Exclusive"
msgstr "Exclusivo"
#. module: crm
#: code:addons/crm/crm_lead.py:584
#: code:addons/crm/crm_lead.py:600
#, python-format
msgid "From %s : %s"
msgstr ""
@@ -934,7 +984,7 @@ msgid "Next Action"
msgstr "Próxima acción"
#. module: crm
#: code:addons/crm/crm_lead.py:763
#: code:addons/crm/crm_lead.py:780
#, python-format
msgid "<b>Partner</b> set to <em>%s</em>."
msgstr ""
@@ -1024,7 +1074,7 @@ msgid "Creation Date"
msgstr "Fecha de creación"
#. module: crm
#: code:addons/crm/crm_lead.py:698
#: code:addons/crm/crm_lead.py:715
#, python-format
msgid "Lead <b>converted into an Opportunity</b>"
msgstr ""
@@ -1213,7 +1263,9 @@ msgid "Days to Close"
msgstr ""
#. module: crm
#: code:addons/crm/crm_lead.py:1057
#: field:crm.case.section,complete_name:0
#, python-format
msgid "unknown"
msgstr ""
@@ -1296,7 +1348,7 @@ msgid "Lead Description"
msgstr ""
#. module: crm
#: code:addons/crm/crm_lead.py:565
#: code:addons/crm/crm_lead.py:581
#, python-format
msgid "Merged opportunities"
msgstr ""
@@ -1856,7 +1908,7 @@ msgid "Leads"
msgstr ""
#. module: crm
#: code:addons/crm/crm_lead.py:563
#: code:addons/crm/crm_lead.py:579
#, python-format
msgid "Merged leads"
msgstr ""
@@ -1950,7 +2002,6 @@ msgid "Global CC"
msgstr ""
#. module: crm
#: view:crm.lead:0
#: view:crm.phonecall:0
#: model:ir.actions.act_window,name:crm.crm_case_categ_phone0
#: model:ir.ui.menu,name:crm.menu_crm_case_phone
@@ -2878,7 +2929,7 @@ msgid "Working Hours"
msgstr ""
#. module: crm
#: code:addons/crm/crm_lead.py:968
#: code:addons/crm/crm_lead.py:986
#: view:crm.lead:0
#: field:crm.lead2opportunity.partner,partner_id:0
#: field:crm.lead2opportunity.partner.mass,partner_id:0
+4 -4
View File
@@ -7,14 +7,14 @@ msgstr ""
"Project-Id-Version: OpenERP Server 6.0dev\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n"
"PO-Revision-Date: 2013-07-09 07:36+0000\n"
"PO-Revision-Date: 2013-07-25 13:52+0000\n"
"Last-Translator: krnkris <Unknown>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-07-10 05:25+0000\n"
"X-Generator: Launchpad (build 16696)\n"
"X-Launchpad-Export-Date: 2013-07-26 05:14+0000\n"
"X-Generator: Launchpad (build 16700)\n"
#. module: crm
#: model:crm.case.stage,name:crm.stage_lead3
@@ -241,7 +241,7 @@ msgstr "Kilép"
#: view:crm.lead:0
#: field:crm.lead,state_id:0
msgid "State"
msgstr "Állapot"
msgstr "Állam/Megye"
#. module: crm
#: field:res.partner,meeting_count:0
+321 -309
View File
File diff suppressed because it is too large Load Diff
+9 -8
View File
@@ -7,14 +7,14 @@ msgstr ""
"Project-Id-Version: OpenERP Server 6.0dev\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n"
"PO-Revision-Date: 2013-07-23 03:21+0000\n"
"PO-Revision-Date: 2013-08-06 09:41+0000\n"
"Last-Translator: fanvil <fanvil@hotmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-07-24 05:31+0000\n"
"X-Generator: Launchpad (build 16700)\n"
"X-Launchpad-Export-Date: 2013-08-07 04:46+0000\n"
"X-Generator: Launchpad (build 16721)\n"
#. module: crm
#: view:crm.lead.report:0
@@ -185,7 +185,7 @@ msgstr "预计结束月份"
msgid ""
"Holds the Chatter summary (number of messages, ...). This summary is "
"directly in html format in order to be inserted in kanban views."
msgstr "保留复杂的摘要(消息数量,……等)。为了插入到看板视图,这一摘要直接是是HTML格式。"
msgstr "保留复杂的摘要(消息数量,……等)。这一摘要直接是是HTML格式,以便插入到看板视图。"
#. module: crm
#: code:addons/crm/crm_lead.py:640
@@ -583,6 +583,7 @@ msgid ""
" If the call needs to be done then the status is set "
"to 'Not Held'."
msgstr ""
"当一个案子新建时,状态被设为‘待处理’,当案子进行中,状态被设为‘打开’,当电话结束,状态被设为‘挂起’。如果需要电话,状态设为‘未挂起’"
#. module: crm
#: field:crm.case.section,message_summary:0
@@ -843,7 +844,7 @@ msgstr "参考2"
msgid ""
"Link between stages and sales teams. When set, this limitate the current "
"stage to the selected sales teams."
msgstr ""
msgstr "在阶段和销售团队之间建立链接。设置后将限制此阶段只能用于被选中的销售团队。"
#. module: crm
#: view:crm.case.stage:0
@@ -979,7 +980,7 @@ msgstr "下一动作"
#: code:addons/crm/crm_lead.py:780
#, python-format
msgid "<b>Partner</b> set to <em>%s</em>."
msgstr ""
msgstr "<b>业务伙伴</b> 设给 <em>%s</em>."
#. module: crm
#: selection:crm.lead.report,state:0
@@ -1338,7 +1339,7 @@ msgstr "发送邮件的时候,默认的邮件地址来自销售团队。"
msgid ""
"Phone Calls Assigned to the current user or with a team having the current "
"user as team leader"
msgstr ""
msgstr "被分配给当前用户或者当前用户领导的销售团队的电话沟通任务。"
#. module: crm
#: view:crm.segmentation:0
@@ -2884,7 +2885,7 @@ msgstr "发现日期"
#. module: crm
#: view:crm.lead:0
msgid "at"
msgstr ""
msgstr "在"
#. module: crm
#: model:crm.case.stage,name:crm.stage_lead1
+8 -19
View File
@@ -19,17 +19,9 @@
#
##############################################################################
from openerp.osv import fields,osv
from openerp.osv import fields, osv
from openerp import tools
from .. import crm
AVAILABLE_STATES = [
('draft','Draft'),
('open','Open'),
('cancel', 'Cancelled'),
('done', 'Closed'),
('pending','Pending')
]
from openerp.addons.crm import crm
MONTHS = [
('01', 'January'),
@@ -66,11 +58,12 @@ class crm_lead_report(osv.osv):
# other date fields
'create_date': fields.datetime('Create Date', readonly=True),
'opening_date': fields.date('Opening Date', readonly=True),
'opening_date': fields.date('Assignation Date', readonly=True),
'date_closed': fields.date('Close Date', readonly=True),
'date_last_stage_update': fields.datetime('Last Stage Update', readonly=True),
# durations
'delay_open': fields.float('Delay to Open',digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to open the case"),
'delay_open': fields.float('Delay to Assign',digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to open the case"),
'delay_close': fields.float('Delay to Close',digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to close the case"),
'delay_expected': fields.float('Overpassed Deadline',digits=(16,2),readonly=True, group_operator="avg"),
@@ -79,7 +72,6 @@ class crm_lead_report(osv.osv):
'section_id':fields.many2one('crm.case.section', 'Sales Team', readonly=True),
'channel_id':fields.many2one('crm.case.channel', 'Channel', readonly=True),
'type_id':fields.many2one('crm.case.resource.type', 'Campaign', readonly=True),
'state': fields.selection(AVAILABLE_STATES, 'Status', size=16, readonly=True),
'company_id': fields.many2one('res.company', 'Company', readonly=True),
'probability': fields.float('Probability',digits=(16,2),readonly=True, group_operator="avg"),
'planned_revenue': fields.float('Planned Revenue',digits=(16,2),readonly=True),
@@ -94,10 +86,7 @@ class crm_lead_report(osv.osv):
('opportunity','Opportunity'),
],'Type', help="Type is used to separate Leads and Opportunities"),
}
def init(self, cr):
"""
@@ -109,7 +98,6 @@ class crm_lead_report(osv.osv):
CREATE OR REPLACE VIEW crm_lead_report AS (
SELECT
id,
to_char(c.date_deadline, 'YYYY') as deadline_year,
to_char(c.date_deadline, 'MM') as deadline_month,
to_char(c.date_deadline, 'YYYY-MM-DD') as deadline_day,
@@ -121,7 +109,8 @@ class crm_lead_report(osv.osv):
to_char(c.date_open, 'YYYY-MM-DD') as opening_date,
to_char(c.date_closed, 'YYYY-mm-dd') as date_closed,
c.state,
date_trunc('day',c.date_last_stage_update) as date_last_stage_update,
c.user_id,
c.probability,
c.stage_id,
+11 -8
View File
@@ -13,7 +13,7 @@
<field name="creation_month" invisible="1"/>
<field name="creation_day" invisible="1"/>
<field name="deadline_month" invisible="1"/>
<field name="state" invisible="1"/>
<field name="date_last_stage_update" invisible="1"/>
<field name="stage_id" invisible="1"/>
<field name="type_id" invisible="1"/>
<field name="channel_id" invisible="1"/>
@@ -69,10 +69,12 @@
<filter icon="terp-personal" name="lead" string="Lead" domain="[('type','=', 'lead')]" help="Show only lead"/>
<filter icon="terp-personal+" string="Opportunity" name="opportunity" domain="[('type','=','opportunity')]" help="Show only opportunity"/>
<separator/>
<filter icon="terp-check" string="New" domain="[('state','=','draft')]" help="Leads/Opportunities which are in New state"/>
<filter icon="terp-camera_test" string="Open" domain="[('state','=','open')]" help="Leads/Opportunities which are in open state"/>
<filter icon="gtk-media-pause" string="Pending" domain="[('state','=','pending')]" help="Leads/Opportunities which are in pending state"/>
<filter icon="terp-dialog-close" string="Closed" domain="[('state','=','done')]" help="Leads/Opportunities which are in done state"/>
<filter string="New" name="new"
domain="[('probability', '=', 0), ('stage_id.sequence', '=', 1)]"/>
<filter string="Won" name="won"
domain="[('probability', '=', 100), ('stage_id.on_change', '=', 1)]"/>
<filter string="Lost" name="lost"
domain="[('probability', '=', 0), ('stage_id.sequence', '!=', 1)]"/>
<separator/>
<filter string="My Sales Team(s)" icon="terp-personal+" context="{'invisible_section': False}" domain="[('section_id.user_id','=',uid)]"
help="Leads/Opportunities that are assigned to one of the sale teams I manage" groups="base.group_multi_salesteams"/>
@@ -120,6 +122,7 @@
<separator orientation="vertical" />
<filter string="Exp. Closing" icon="terp-go-month"
domain="[]" context="{'group_by':'deadline_month'}"/>
<filter string="Last Stage Update" context="{'group_by':'date_last_stage_update'}" />
</group>
</search>
</field>
@@ -131,7 +134,7 @@
<field name="name">crm.lead.report.tree</field>
<field name="model">crm.lead.report</field>
<field name="arch" type="xml">
<tree colors="blue:state == 'draft';black:state in ('open','pending','done');gray:state == 'cancel' " create="false" string="Opportunities Analysis">
<tree create="false" string="Opportunities Analysis">
<field name="creation_year" invisible="1"/>
<field name="creation_month" invisible="1"/>
<field name="creation_day" invisible="1"/>
@@ -141,7 +144,6 @@
<field name="user_id" invisible="1"/>
<field name="partner_id" invisible="1"/>
<field name="country_id" invisible="1"/>
<field name="state" invisible="1"/>
<field name="stage_id" invisible="1"/>
<field name="priority" invisible="1"/>
<field name="type_id" invisible="1"/>
@@ -150,8 +152,9 @@
<field name="company_id" invisible="1" groups="base.group_multi_company"/>
<field name="nbr" string="#Opportunities" sum="#Opportunities"/>
<field name="planned_revenue" sum="Planned Revenues"/>
<field name="delay_open" sum='Delay to open'/>
<field name="delay_open" sum='Delay to Assign'/>
<field name="delay_close" sum='Delay to close'/>
<field name="date_last_stage_update"/>
<field name="delay_expected"/>
<field name="probability" widget="progressbar"/>
<field name="probable_revenue"/>
+5 -5
View File
@@ -19,16 +19,16 @@
#
##############################################################################
from openerp.osv import fields,osv
from openerp import tools
from .. import crm
from openerp.addons.crm import crm
from openerp.osv import fields, osv
AVAILABLE_STATES = [
('draft','Draft'),
('open','Todo'),
('draft', 'Draft'),
('open', 'Todo'),
('cancel', 'Cancelled'),
('done', 'Held'),
('pending','Pending')
('pending', 'Pending')
]
@@ -23,7 +23,7 @@
<field name="nbr" string="#Phone calls" sum="#Phone calls"/>
<field name="duration" avg="Duration"/>
<field name="delay_close" avg="Avg Closing Delay"/>
<field name="delay_open" sum='Delay to open'/>
<field name="delay_open" sum='Delay to Assign'/>
</tree>
</field>
</record>
+1 -2
View File
@@ -41,7 +41,7 @@ class res_partner(osv.osv):
_columns = {
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
'opportunity_ids': fields.one2many('crm.lead', 'partner_id',\
'Leads and Opportunities', domain=[('state','in', ('draft','open','pending'))]),
'Leads and Opportunities', domain=[('probability' 'not in', ['0', '100'])]),
'meeting_ids': fields.many2many('crm.meeting', 'crm_meeting_partner_rel','partner_id', 'meeting_id',
'Meetings'),
'phonecall_ids': fields.one2many('crm.phonecall', 'partner_id',\
@@ -87,7 +87,6 @@ class res_partner(osv.osv):
'probability' : probability,
'partner_id' : partner_id,
'categ_ids' : categ_ids and categ_ids[0:1] or [],
'state' :'draft',
'type': 'opportunity'
}, context=context)
opportunity_ids[partner_id] = opportunity_id
+6 -1
View File
@@ -1,9 +1,14 @@
-
I set a new sale team (with Marketing at parent) and I cancel unqualified lead .
I set a new sale team (with Marketing at parent) .
-
!python {model: crm.lead}: |
section_id = self.pool.get('crm.case.section').create(cr, uid, {'name': "Phone Marketing", 'parent_id': ref("crm.crm_case_section_2")})
self.write(cr, uid, [ref("crm_case_1")], {'section_id': section_id})
-
I check unqualified lead .
-
!assert {model: crm.lead, id: crm.crm_case_1, string: Lead is in new stage}:
- stage_id.sequence == 1
-
I escalate the lead to parent team.
-
+43
View File
@@ -0,0 +1,43 @@
-
I create a new lead.
-
!record {model: crm.lead, id: test_crm_lead_new}:
type: 'lead'
name: 'Test lead new'
partner_id: base.res_partner_1
description: This is the description of the test new lead.
section_id: crm.section_sales_department
-
I check default stage of lead.
-
!python {model: crm.lead}: |
stage = self.pool.get('crm.case.stage').search_read(cr,uid,[('sequence','=',1)],['id'],context)[0]
lead = self.browse(cr, uid, ref('test_crm_lead_new'))
stage_id = self.stage_find(cr, uid , [lead], lead.section_id.id or False,[], context)
assert stage_id == stage['id'], "Default stage of lead is incorrect!"
-
I change type from lead to opportunity.
-
!python {model: crm.lead}: |
self.convert_opportunity(cr, uid ,[ref("test_crm_lead_new")], ref("base.res_partner_2"))
-
Now I check default stage after change type.
-
!python {model: crm.lead}: |
stage = self.pool.get('crm.case.stage').search_read(cr,uid,[('sequence','=',1)],['id'],context)[0]
opp = self.browse(cr, uid, ref('test_crm_lead_new'))
stage_id = self.stage_find(cr, uid , [opp], opp.section_id.id or False,[], context)
assert stage_id == stage['id'], "Default stage of opportunity is incorrect!"
-
Now I change the stage of opportunity to won.
-
!python {model: crm.lead}: |
self.case_mark_won(cr, uid, [ref("test_crm_lead_new")])
-
I check statge of opp should won, after change stage.
-
!python {model: crm.lead}: |
opp = self.browse(cr, uid, ref('test_crm_lead_new'))
stage_id = self.stage_find(cr, uid , [opp], opp.section_id.id or False,[('probability','=',100.0)], context)
assert stage_id == opp.stage_id.id, "Stage of opportunity is incorrect!"
+22 -21
View File
@@ -2,29 +2,29 @@
-
During a mixed merge (involving leads and opps), data should be handled a certain way following their type (m2o, m2m, text, ...) Start by creating two leads and an opp.
-
!record {model: crm.lead, id: test_crm_lead_01}:
type: 'lead'
name: 'Test lead 1'
partner_id: base.res_partner_1
stage_id: stage_lead1
description: This is the description of the test lead 1.
-
!record {model: crm.lead, id: test_crm_lead_02}:
type: 'lead'
name: 'Test lead 2'
partner_id: base.res_partner_3
stage_id: stage_lead1
description: This is the description of the test lead 2.
-
!record {model: crm.lead, id: test_crm_opp_01}:
!record {model: crm.lead, id: test_crm_opp_1}:
type: 'opportunity'
name: 'Test opportunity 1'
partner_id: base.res_partner_5
stage_id: stage_lead1
description: This is the description of the test opp 1.
-
!record {model: crm.lead, id: test_crm_lead_first}:
type: 'lead'
name: 'Test lead first'
partner_id: base.res_partner_1
stage_id: stage_lead1
description: This is the description of the test lead first.
-
!record {model: crm.lead, id: test_crm_lead_second}:
type: 'lead'
name: 'Test lead second'
partner_id: base.res_partner_1
stage_id: stage_lead1
description: This is the description of the test lead second.
-
!python {model: crm.lead}: |
lead_ids = [ref('test_crm_lead_01'), ref('test_crm_lead_02'), ref('test_crm_opp_01')]
lead_ids = [ref('test_crm_opp_1'), ref('test_crm_lead_first'), ref('test_crm_lead_second')]
context.update({'active_model': 'crm.lead', 'active_ids': lead_ids, 'active_id': lead_ids[0]})
-
I create a merge wizard and merge the leads and opp together in the first item of the list.
@@ -38,18 +38,19 @@
-
!python {model: crm.lead}: |
merge_id = self.search(cr, uid, [('name', '=', 'Test opportunity 1'), ('partner_id','=', ref("base.res_partner_5"))])
assert merge_id, 'Fail to create merge opportunity wizard'
merge_result = self.browse(cr, uid, merge_id)[0]
assert merge_result.description == 'This is the description of the test opp 1.\n\nThis is the description of the test lead 1.\n\nThis is the description of the test lead 2.', 'Description mismatch: when merging leads/opps with different text values, these values should get concatenated and separated with line returns'
assert merge_result.description == 'This is the description of the test opp 1.\n\nThis is the description of the test lead first.\n\nThis is the description of the test lead second.', 'Description mismatch: when merging leads/opps with different text values, these values should get concatenated and separated with line returns'
assert merge_result.type == 'opportunity', 'Type mismatch: when at least one opp in involved in the merge, the result should be a new opp (instead of %s)' % merge_result.type
-
The other (tailing) leads/opps shouldn't exist anymore.
-
!python {model: crm.lead}: |
tailing_lead = self.search(cr, uid, [('id', '=', ref('test_crm_lead_01'))])
assert not tailing_lead, 'This tailing lead (id %s) should not exist anymore' % ref('test_crm_lead_02')
tailing_lead = self.search(cr, uid, [('id', '=', ref('test_crm_lead_first'))])
assert not tailing_lead, 'This tailing lead (id %s) should not exist anymore' % ref('test_crm_lead_second')
tailing_opp = self.search(cr, uid, [('id', '=', ref('test_crm_lead_02'))])
tailing_opp = self.search(cr, uid, [('id', '=', ref('test_crm_lead_second'))])
assert not tailing_opp, 'This tailing opp (id %s) should not exist anymore' % ref('test_crm_opp_01')
-
I want to test leads merge. Start by creating two leads (with the same partner).
@@ -122,4 +123,4 @@
assert merge_result.partner_id.id == ref("base.res_partner_5"), 'Partner mismatch'
assert merge_result.type == 'opportunity', 'Type mismatch: when opps get merged together, the result should be a new opp (instead of %s)' % merge_result.type
tailing_opp = self.search(cr, uid, [('id', '=', ref('test_crm_opp_03'))])
assert not tailing_opp, 'This tailing opp (id %s) should not exist anymore' % ref('test_crm_opp_03')
assert not tailing_opp, 'This tailing opp (id %s) should not exist anymore' % ref('test_crm_opp_03')
+1 -2
View File
@@ -6,14 +6,13 @@
partner_id: base.res_partner_2
type: opportunity
stage_id: crm.stage_lead1
state: draft
-
I create a lead record to call a mailing opt-out onchange method.
-
!record {model: crm.lead, id: crm_case_18}:
name: 'Need 20 Days of Consultancy'
type: opportunity
state: draft
stage_id: crm.stage_lead1
opt_out: True
-
I create a phonecall record to call a partner onchange method.
+26 -15
View File
@@ -1,15 +1,25 @@
-
In order to test the conversion of a lead into a opportunity,
-
I set lead to open stage.
-
!python {model: crm.lead}: |
self.write(cr, uid, [ref("crm_case_3")],{'stage_id':ref("stage_lead1")})
-
I check if the lead stage is "Open".
-
!assert {model: crm.lead, id: crm.crm_case_3, string: Lead stage is Open}:
- stage_id.sequence == 1
-
I convert lead into opportunity for exiting customer.
-
!python {model: crm.lead}: |
self.convert_opportunity(cr, uid ,[ref("crm_case_1")], ref("base.res_partner_2"))
self.convert_opportunity(cr, uid ,[ref("crm_case_3")], ref("base.res_partner_2"))
-
I check details of converted opportunity.
-
!python {model: crm.lead}: |
lead = self.browse(cr, uid, ref('crm_case_1'))
lead = self.browse(cr, uid, ref('crm_case_3'))
assert lead.type == 'opportunity', 'Lead is not converted to opportunity!'
assert lead.partner_id.id == ref("base.res_partner_2"), 'Partner mismatch!'
assert lead.stage_id.id == ref("stage_lead1"), 'Stage of opportunity is incorrect!'
@@ -18,7 +28,7 @@
-
!python {model: crm.opportunity2phonecall}: |
import time
context.update({'active_model': 'crm.lead', 'active_ids': [ref('crm_case_1')]})
context.update({'active_model': 'crm.lead', 'active_ids': [ref('crm_case_3')]})
call_id = self.create(cr, uid, {'date': time.strftime('%Y-%m-%d %H:%M:%S'),
'name': "Bonjour M. Jean, Comment allez-vous? J'ai bien reçu votre demande, pourrions-nous en parler quelques minutes?"}, context=context)
self.action_schedule(cr, uid, [call_id], context=context)
@@ -26,46 +36,46 @@
I check that phonecall is scheduled for that opportunity.
-
!python {model: crm.phonecall}: |
ids = self.search(cr, uid, [('opportunity_id', '=', ref('crm_case_1'))])
ids = self.search(cr, uid, [('opportunity_id', '=', ref('crm_case_3'))])
assert len(ids), 'Phonecall is not scheduled'
-
Now I schedule meeting with customer.
-
!python {model: crm.lead}: |
self.action_makeMeeting(cr, uid, [ref('crm_case_1')])
self.action_makeMeeting(cr, uid, [ref('crm_case_3')])
-
After communicated with customer, I put some notes with contract details.
-
!python {model: crm.lead}: |
self.message_post(cr, uid, [ref('crm_case_1')], subject='Test note', body='Détails envoyés par le client sur ​​le FAX pour la qualité')
self.message_post(cr, uid, [ref('crm_case_3')], subject='Test note', body='Détails envoyés par le client sur ​​le FAX pour la qualité')
-
I win this opportunity
-
!python {model: crm.lead}: |
self.case_mark_won(cr, uid, [ref("crm_case_1")])
self.case_mark_won(cr, uid, [ref("crm_case_3")])
-
I check details of the opportunity after having won the opportunity.
-
!python {model: crm.lead}: |
lead = self.browse(cr, uid, ref('crm_case_1'))
lead = self.browse(cr, uid, ref('crm_case_3'))
assert lead.stage_id.id == ref('crm.stage_lead6'), "Opportunity stage should be 'Won'."
assert lead.state == 'done', "Opportunity is not in 'done' state!"
assert lead.stage_id.probability == 100.0, "Opportunity is not 'done'"
assert lead.probability == 100.0, "Revenue probability should be 100.0!"
-
I convert mass lead into opportunity customer.
-
!python {model: crm.lead2opportunity.partner.mass}: |
context.update({'active_model': 'crm.lead', 'active_ids': [ref("crm_case_11"), ref("crm_case_2")], 'active_id': ref("crm_case_11")})
context.update({'active_model': 'crm.lead', 'active_ids': [ref("crm_case_13"), ref("crm_case_2")], 'active_id': ref("crm_case_13")})
id = self.create(cr, uid, {'user_ids': [(6, 0, [ref('base.user_root')])], 'section_id': ref('crm.section_sales_department')}, context=context)
self.mass_convert(cr, uid, [id], context=context)
-
Now I check first lead converted on opportunity.
-
!python {model: crm.lead}: |
opp = self.browse(cr, uid, ref('crm_case_11'))
assert opp.name == "Need estimated cost for new project", "Opportunity name not correct"
opp = self.browse(cr, uid, ref('crm_case_13'))
assert opp.name == "Plan to buy 60 keyboards and mouses", "Opportunity name not correct"
assert opp.type == 'opportunity', 'Lead is not converted to opportunity!'
expected_partner = "Thomas Passot"
expected_partner = "Will McEncroe"
assert opp.partner_id.name == expected_partner, 'Partner mismatch! %s vs %s' % (opp.partner_id.name, expected_partner)
assert opp.stage_id.id == ref("stage_lead1"), 'Stage of probability is incorrect!'
-
@@ -86,14 +96,15 @@
-
!python {model: crm.lead}: |
lead = self.browse(cr, uid, ref('crm_case_2'))
assert lead.stage_id.id == ref('crm.stage_lead8'), "Opportunity stage should be 'Lost'."
assert lead.state == 'cancel', "Lost opportunity is not in 'cancel' state!"
assert lead.stage_id.id == ref('crm.stage_lead7'), "Opportunity stage should be 'Lost'."
assert lead.stage_id.sequence != 1 and lead.stage_id.probability == 0.0, "Lost opportunity is not in 'cancel' state!"
assert lead.probability == 0.0, "Revenue probability should be 0.0!"
-
I confirm review needs meeting.
-
!python {model: crm.meeting}: |
context.update({'active_model': 'crm.meeting'})
self.case_open(cr, uid, [ref('base_calendar.crm_meeting_4')])
-
I invite a user for meeting.
-
+4 -5
View File
@@ -21,7 +21,6 @@
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp import tools
import re
class crm_lead2opportunity_partner(osv.osv_memory):
@@ -60,11 +59,11 @@ class crm_lead2opportunity_partner(osv.osv_memory):
if partner_id:
# Search for opportunities that have the same partner and that arent done or cancelled
ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id), ('state', '!=', 'done')])
ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id), ('probability', '<', '100')])
for id in ids:
tomerge.add(id)
if email:
ids = lead_obj.search(cr, uid, [('email_from', 'ilike', email[0]), ('state', '!=', 'done')])
ids = lead_obj.search(cr, uid, [('email_from', 'ilike', email[0]), ('probability', '<', '100')])
for id in ids:
tomerge.add(id)
@@ -105,8 +104,8 @@ class crm_lead2opportunity_partner(osv.osv_memory):
context = {}
lead_obj = self.pool.get('crm.lead')
for lead in lead_obj.browse(cr, uid, context.get('active_ids', []), context=context):
if lead.state in ['done', 'cancel']:
raise osv.except_osv(_("Warning!"), _("Closed/Cancelled leads cannot be converted into opportunities."))
if lead.probability == 100:
raise osv.except_osv(_("Warning!"), _("Closed/Dead leads cannot be converted into opportunities."))
return False
def _convert_opportunity(self, cr, uid, ids, vals, context=None):
+2 -3
View File
@@ -62,8 +62,7 @@ class crm_merge_opportunity(osv.osv_memory):
def default_get(self, cr, uid, fields, context=None):
"""
Use active_ids from the context to fetch the leads/opps to merge.
In order to get merged, these leads/opps can't be in 'Done' or
'Cancel' state.
In order to get merged, these leads/opps can't be in 'Dead' or 'Closed'
"""
if context is None:
context = {}
@@ -74,7 +73,7 @@ class crm_merge_opportunity(osv.osv_memory):
opp_ids = []
opps = self.pool.get('crm.lead').browse(cr, uid, record_ids, context=context)
for opp in opps:
if opp.state not in ('done', 'cancel'):
if opp.probability < 100:
opp_ids.append(opp.id)
if 'opportunity_ids' in fields:
res.update({'opportunity_ids': opp_ids})
+1 -10
View File
@@ -42,7 +42,6 @@ class crm_claim_stage(osv.osv):
'sequence': fields.integer('Sequence', help="Used to order stages. Lower is better."),
'section_ids':fields.many2many('crm.case.section', 'section_claim_stage_rel', 'stage_id', 'section_id', string='Sections',
help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
'state': fields.selection(crm.AVAILABLE_STATES, 'Status', required=True, help="The related status for the stage. The status of your document will automatically change regarding the selected stage. For example, if a stage is related to the status 'Close', when your document reaches this stage, it will be automatically have the 'closed' status."),
'case_default': fields.boolean('Common to All Teams',
help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
'fold': fields.boolean('Hide in Views when Empty',
@@ -51,7 +50,6 @@ class crm_claim_stage(osv.osv):
_defaults = {
'sequence': lambda *args: 1,
'state': 'draft',
'fold': False,
}
@@ -70,7 +68,7 @@ class crm_claim(osv.osv):
def _get_default_stage_id(self, cr, uid, context=None):
""" Gives default stage_id """
section_id = self._get_default_section_id(cr, uid, context=context)
return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft')], context=context)
return self.stage_find(cr, uid, [], section_id, [('sequence', '=', '1')], context=context)
_columns = {
'id': fields.integer('ID', readonly=True),
@@ -105,13 +103,6 @@ class crm_claim(osv.osv):
'stage_id': fields.many2one ('crm.claim.stage', 'Stage', track_visibility='onchange',
domain="['|', ('section_ids', '=', section_id), ('case_default', '=', True)]"),
'cause': fields.text('Root Cause'),
'state': fields.related('stage_id', 'state', type="selection", store=True,
selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
help='The status is set to \'Draft\', when a case is created.\
If the case is in progress the status is set to \'Open\'.\
When the case is over, the status is set to \'Done\'.\
If the case needs to be reviewed then the status is \
set to \'Pending\'.'),
}
_defaults = {
+1 -6
View File
@@ -44,28 +44,23 @@
<record model="crm.claim.stage" id="stage_claim1">
<field name="name">New</field>
<field name="state">draft</field>
<field name="sequence">26</field>
<field name="sequence">1</field>
<field name="case_default" eval="True"/>
</record>
<record model="crm.claim.stage" id="stage_claim5">
<field name="name">In Progress</field>
<field name="state">open</field>
<field name="sequence">27</field>
<field name="case_default" eval="True"/>
</record>
<record model="crm.claim.stage" id="stage_claim2">
<field name="name">Settled</field>
<field name="state">done</field>
<field name="sequence">28</field>
<field name="case_default" eval="True"/>
</record>
<record model="crm.claim.stage" id="stage_claim3">
<field name="name">Rejected</field>
<field name="state">cancel</field>
<field name="sequence">29</field>
<field name="case_default" eval="True"/>
<field name="case_refused" eval="True"/>
<field name="fold" eval="True"/>
</record>
+1 -9
View File
@@ -38,7 +38,6 @@
<tree string="Claim Stages">
<field name="sequence"/>
<field name="name"/>
<field name="state"/>
</tree>
</field>
</record>
@@ -51,7 +50,6 @@
<field name="name"/>
<field name="case_default"/>
<field name="sequence"/>
<field name="state"/>
<field name="fold"/>
</form>
</field>
@@ -79,7 +77,7 @@
<field name="name">CRM - Claims Tree</field>
<field name="model">crm.claim</field>
<field name="arch" type="xml">
<tree string="Claims" colors="blue:state=='pending' and not(date_deadline and (date_deadline &lt; current_date));gray:state in ('close', 'cancel');red:date_deadline and (date_deadline &lt; current_date)">
<tree string="Claims">
<field name="name"/>
<field name="partner_id"/>
<field name="user_id"/>
@@ -90,7 +88,6 @@
<field name="categ_id" string="Type"/>
<field name="date_deadline" invisible="1"/>
<field name="date_closed" invisible="1"/>
<field name="state" groups="base.group_no_one"/>
</tree>
</field>
</record>
@@ -113,7 +110,6 @@
<field name="priority"/>
<field name="section_id" groups="base.group_multi_salesteams"/>
<field name="date_deadline"/>
<field name="state" groups="base.group_no_one"/>
</group>
<group colspan="4" col="4">
<notebook>
@@ -191,9 +187,6 @@
<field name="arch" type="xml">
<search string="Search Claims">
<field name="name" string="Claims"/>
<filter icon="terp-check" string="New" name="current" domain="[('state','=','draft')]" help="New Claims"/>
<filter icon="terp-camera_test" string="In Progress" domain="[('state','=','open')]" help="In Progress Claims"/>
<filter icon="terp-gtk-media-pause" string="Pending" domain="[('state','=','pending')]"/>
<separator/>
<filter string="Unassigned Claims" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Claims" />
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
@@ -203,7 +196,6 @@
<filter string="Responsible" icon="terp-personal" domain="[]" help="Responsible User" context="{'group_by':'user_id'}"/>
<filter string="Stage" icon="terp-stage" domain="[]" context="{'group_by':'stage_id'}"/>
<filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'categ_id'}"/>
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}" groups="base.group_no_one"/>
<filter string="Claim Date" icon="terp-go-month" domain="[]" help="Claim Date" context="{'group_by':'date'}"/>
<filter string="Deadline" icon="terp-go-month" domain="[]" context="{'group_by':'date_deadline'}"/>
<filter string="Closure" icon="terp-go-month" domain="[]" help="Date Closed" context="{'group_by':'date_closed'}" groups="base.group_no_one"/>
+1 -11
View File
@@ -22,14 +22,6 @@
from openerp.osv import fields,osv
from openerp import tools
AVAILABLE_STATES = [
('draft','Draft'),
('open','Open'),
('cancel', 'Cancelled'),
('done', 'Closed'),
('pending','Pending')
]
AVAILABLE_PRIORITIES = [
('5', 'Lowest'),
('4', 'Low'),
@@ -51,7 +43,6 @@ class crm_claim_report(osv.osv):
'user_id':fields.many2one('res.users', 'User', readonly=True),
'section_id':fields.many2one('crm.case.section', 'Section', readonly=True),
'nbr': fields.integer('# of Cases', readonly=True),
'state': fields.selection(AVAILABLE_STATES, 'Status', size=16, readonly=True),
'month':fields.selection([('01', 'January'), ('02', 'February'), \
('03', 'March'), ('04', 'April'),\
('05', 'May'), ('06', 'June'), \
@@ -92,7 +83,6 @@ class crm_claim_report(osv.osv):
to_char(c.date, 'YYYY-MM-DD') as day,
to_char(c.date_closed, 'YYYY-MM-DD') as date_closed,
to_char(c.date_deadline, 'YYYY-MM-DD') as date_deadline,
c.state,
c.user_id,
c.stage_id,
c.section_id,
@@ -109,7 +99,7 @@ class crm_claim_report(osv.osv):
from
crm_claim c
group by to_char(c.date, 'YYYY'), to_char(c.date, 'MM'),to_char(c.date, 'YYYY-MM-DD'),\
c.state, c.user_id,c.section_id, c.stage_id,\
c.user_id,c.section_id, c.stage_id,\
c.categ_id,c.partner_id,c.company_id,c.create_date,
c.priority,c.type_action,c.date_deadline,c.date_closed,c.id
)""")
@@ -21,7 +21,6 @@
<field name="email" sum="# Mails"/>
<field name="delay_close" avg="Avg Closing Delay"/>
<field name="delay_expected"/>
<field name="state" invisible="1"/>
<field name="stage_id" invisible="1"/>
<field name="categ_id" invisible="1"/>
<field name="priority" invisible="1"/>
@@ -37,7 +36,6 @@
<field name="model">crm.claim.report</field>
<field name="arch" type="xml">
<graph orientation="horizontal" string="Claims" type="bar">
<field name="state"/>
<field name="nbr" operator="+"/>
<field group="True" name="user_id"/>
</graph>
@@ -51,10 +49,6 @@
<field name="model">crm.claim.report</field>
<field name="arch" type="xml">
<search string="Search">
<filter icon="terp-document-new" string="New" domain="[('state','=','draft')]"/>
<filter icon="terp-camera_test" string="Open" domain="[('state','=','open')]"/>
<filter icon="terp-gtk-media-pause" string="Pending" domain="[('state','=','pending')]"/>
<separator/>
<filter string="My Sales Team(s)" icon="terp-personal+" context="{'invisible_section': False}" domain="[('section_id.user_id','=',uid)]" help="My Sales Team(s)" groups="base.group_multi_salesteams"/>
<separator/>
<filter string="My Company" icon="terp-go-home" context="{'invisible_section': False}" domain="[('section_id.user_id.company_id','=',uid)]" help="My company"/>
@@ -73,8 +67,6 @@
<field name="create_date" />
<field name="date_closed" />
<field name="date_deadline" />
<filter icon="terp-dialog-close" string="Done" domain="[('state','=','done')]"/>
<filter icon="gtk-cancel" string="Cancel" domain="[('state','=','cancel')]"/>
</group>
<group expand="1" string="Group By...">
<filter string="Salesperson" name="Salesperson" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}" />
@@ -84,7 +76,6 @@
<filter string="Priority" icon="terp-rating-rated" domain="[]" context="{'group_by':'priority'}" />
<filter string="Category" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'categ_id'}" />
<filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'type_action'}" help="Action Type"/>
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}" />
<filter string="Company" icon="terp-go-home" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
<filter string="Day" icon="terp-go-today" domain="[]" context="{'group_by':'day'}" help="Date of claim"/>
<filter string="Month" icon="terp-go-month" domain="[]" context="{'group_by':'month'}" help="Month of claim"/>
+9 -1
View File
@@ -26,6 +26,14 @@ from openerp import tools
from openerp.tools.translate import _
from openerp.tools import html2plaintext
AVAILABLE_STATES = [
('draft', 'New'),
('cancel', 'Cancelled'),
('open', 'In Progress'),
('pending', 'Pending'),
('done', 'Closed')
]
class crm_helpdesk(base_state, osv.osv):
""" Helpdesk Cases """
@@ -65,7 +73,7 @@ class crm_helpdesk(base_state, osv.osv):
domain="['|',('section_id','=',False),('section_id','=',section_id),\
('object_id.model', '=', 'crm.helpdesk')]"),
'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
'state': fields.selection(crm.AVAILABLE_STATES, 'Status', size=16, readonly=True,
'state': fields.selection(AVAILABLE_STATES, 'Status', size=16, readonly=True,
help='The status is set to \'Draft\', when a case is created.\
\nIf the case is in progress the status is set to \'Open\'.\
\nWhen the case is over, the status is set to \'Done\'.\
+5 -1
View File
@@ -49,7 +49,8 @@
<field name="arch" type="xml">
<field name="partner_id" position="after">
<field name="partner_assigned_id"/>
</field>
<field name="date_assign" invisible="1"/>
</field>
</field>
</record>
@@ -60,6 +61,9 @@
<field name="arch" type="xml">
<filter string="Team" position="after">
<filter string="Assigned Partner" icon="terp-personal" domain="[]" context="{'group_by':'partner_assigned_id'}"/>
<filter string="Assigned Month" icon="terp-go-month"
domain="[]" context="{'group_by':'date_assign'}"/>
</filter>
<field name="partner_id" position="after">
<field name="partner_assigned_id"/>
@@ -6,7 +6,7 @@
<field name="model">crm.lead</field>
<field name="priority" eval="32"/>
<field name="arch" type="xml">
<tree string="Leads" colors="blue:state=='pending';grey:state in ('cancel', 'done');red:stage_id[1]=='Disinterested';black:stage_id[1]=='Interested'">
<tree string="Leads" colors="red:stage_id[1]=='Disinterested';black:stage_id[1]=='Interested'">
<field name="date_deadline" invisible="1"/>
<field name="create_date"/>
<field name="name" string="Subject"/>
@@ -18,7 +18,6 @@
<field name="type_id" invisible="1"/>
<field name="referred" invisible="1"/>
<field name="channel_id" invisible="1"/>
<field name="state" invisible="1"/>
<field name="section_id" invisible="context.get('invisible_section', True)" />
<button string="I'm interested" name="case_interested" icon="gtk-index" type="object"/>
@@ -69,7 +68,7 @@
<field name="model">crm.lead</field>
<field name="priority" eval="32"/>
<field name="arch" type="xml">
<tree string="Leads" colors="blue:state=='pending';grey:state in ('cancel', 'done')">
<tree string="Leads">
<field name="date_deadline" invisible="1"/>
<field name="create_date" groups="base.group_no_one"/>
<field name="name" string="Opportunity"/>
@@ -83,7 +82,6 @@
<field name="probability" widget="progressbar" avg="Avg. of Probability"/>
<field name="section_id" invisible="context.get('invisible_section', True)" />
<field name="priority" invisible="1"/>
<field name="state" invisible="1"/>
</tree>
</field>
</record>
@@ -23,13 +23,6 @@ from openerp.osv import fields,osv
from openerp import tools
from openerp.addons.crm import crm
AVAILABLE_STATES = [
('draft','Draft'),
('open','Open'),
('cancel', 'Cancelled'),
('done', 'Closed'),
('pending','Pending')
]
class crm_lead_report_assign(osv.osv):
""" CRM Lead Report """
@@ -43,7 +36,6 @@ class crm_lead_report_assign(osv.osv):
'user_id':fields.many2one('res.users', 'User', readonly=True),
'country_id':fields.many2one('res.country', 'Country', readonly=True),
'section_id':fields.many2one('crm.case.section', 'Sales Team', readonly=True),
'state': fields.selection(AVAILABLE_STATES, 'Status', size=16, readonly=True),
'month':fields.selection([('01', 'January'), ('02', 'February'), \
('03', 'March'), ('04', 'April'),\
('05', 'May'), ('06', 'June'), \
@@ -54,7 +46,7 @@ class crm_lead_report_assign(osv.osv):
'date_assign': fields.date('Partner Date', readonly=True),
'create_date': fields.datetime('Create Date', readonly=True),
'day': fields.char('Day', size=128, readonly=True),
'delay_open': fields.float('Delay to Open',digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to open the case"),
'delay_open': fields.float('Delay to Assign',digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to open the case"),
'delay_close': fields.float('Delay to Close',digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to close the case"),
'delay_expected': fields.float('Overpassed Deadline',digits=(16,2),readonly=True, group_operator="avg"),
'probability': fields.float('Avg Probability',digits=(16,2),readonly=True, group_operator="avg"),
@@ -91,7 +83,6 @@ class crm_lead_report_assign(osv.osv):
to_char(c.create_date, 'YYYY-MM-DD') as creation_date,
to_char(c.date_open, 'YYYY-MM-DD') as opening_date,
to_char(c.date_closed, 'YYYY-mm-dd') as date_closed,
c.state,
c.date_assign,
c.user_id,
c.probability,
@@ -110,7 +101,7 @@ class crm_lead_report_assign(osv.osv):
c.planned_revenue*(c.probability/100) as probable_revenue,
1 as nbr,
date_trunc('day',c.create_date) as create_date,
extract('epoch' from (c.date_closed-c.create_date))/(3600*24) as delay_close,
extract('epoch' from (c.write_date-c.create_date))/(3600*24) as delay_close,
extract('epoch' from (c.date_deadline - c.date_closed))/(3600*24) as delay_expected,
extract('epoch' from (c.date_open-c.create_date))/(3600*24) as delay_open
FROM
@@ -8,8 +8,6 @@
<field name="model">crm.lead.report.assign</field>
<field name="arch" type="xml">
<search string="Leads Analysis">
<filter icon="terp-check" string="Current" domain="[('state','in',('draft','open'))]"/>
<filter icon="terp-dialog-close" string="Closed" domain="[('state','=','done')]"/>
<field name="section_id" context="{'invisible_section': False}" groups="base.group_multi_salesteams"/>
<field name="grade_id"/>
<field name="user_id"/>
@@ -36,7 +34,6 @@
domain="[]" context="{'group_by':'grade_id'}" />
<filter string="Stage" icon="terp-stage" domain="[]" context="{'group_by':'stage_id'}"/>
<filter string="Priority" icon="terp-rating-rated" domain="[]" context="{'group_by':'priority'}" />
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}" />
<filter string="Company" icon="terp-go-home" domain="[]" context="{'group_by':'company_id'}" />
<filter string="Assign Date" icon="terp-go-today" domain="[]" name="group_partner_date" context="{'group_by':'date_assign'}"/>
<filter string="Day" icon="terp-go-today" domain="[]" context="{'group_by':'day'}"/>
@@ -54,7 +51,6 @@
<field name="model">crm.lead.report.assign</field>
<field name="arch" type="xml">
<graph orientation="horizontal" string="Lead Assign" type="bar">
<field name="state"/>
<field name="nbr" operator="+"/>
<field group="True" name="user_id"/>
</graph>
@@ -76,14 +72,13 @@
<field name="partner_id" invisible="1"/>
<field name="country_id" invisible="1"/>
<field name="day" invisible="1"/>
<field name="state" invisible="1"/>
<field name="stage_id" invisible="1"/>
<field name="priority" invisible="1"/>
<field name="type" invisible="1"/>
<field name="company_id" invisible="1" groups="base.group_multi_company"/>
<field name="nbr" string="#Opportunities" sum="#Opportunities"/>
<field name="planned_revenue" sum="Planned Revenues"/>
<field name="delay_open" sum='Delay to open'/>
<field name="delay_open" sum='Delay to Assign'/>
<field name="delay_close" sum='Delay to close'/>
<field name="delay_expected"/>
<field name="probability" widget="progressbar"/>
+115 -119
View File
@@ -33,134 +33,130 @@
<field name="view_mode">tree,form</field>
</record>
<menuitem id="res_partner_activation_config_mi" parent="base.menu_config_address_book" action="res_partner_activation_act" groups="base.group_no_one" />
<menuitem id="res_partner_activation_config_mi" parent="base.menu_config_address_book" action="res_partner_activation_act" groups="base.group_no_one"/>
<!--Partner Grade -->
<!--Partner Grade -->
<record id="view_partner_grade_tree" model="ir.ui.view">
<field name="name">res.partner.grade.tree</field>
<field name="model">res.partner.grade</field>
<field name="arch" type="xml">
<tree string="Partner Grade">
<field name="sequence" invisible="1" />
<field name="name" />
</tree>
<record id="view_partner_grade_tree" model="ir.ui.view">
<field name="name">res.partner.grade.tree</field>
<field name="model">res.partner.grade</field>
<field name="arch" type="xml">
<tree string="Partner Grade">
<field name="sequence" invisible="1"/>
<field name="name"/>
</tree>
</field>
</record>
<record id="view_partner_grade_form" model="ir.ui.view">
<field name="name">res.partner.grade.form</field>
<field name="model">res.partner.grade</field>
<field name="arch" type="xml">
<form string="Partner Grade" version="7.0">
<group col="4">
<field name="name"/>
<field name="sequence"/>
<field name="active"/>
</group>
</form>
</field>
</record>
<record id="res_partner_grade_action" model="ir.actions.act_window">
<field name="name">Partner Grade</field>
<field name="res_model">res.partner.grade</field>
<field name="view_type">form</field>
</record>
<menuitem action="res_partner_grade_action" id="menu_res_partner_grade_action"
groups="base.group_no_one"
parent="base.menu_crm_config_lead" />
<!-- Partner form -->
<record id="view_res_partner_filter_assign_tree" model="ir.ui.view">
<field name="name">res.partner.geo.inherit.tree</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_tree"/>
<field name="arch" type="xml">
<field name="user_id" position="after">
<field name="date_review_next"/>
<field name="grade_id"/>
<field name="activation"/>
</field>
</record>
<record id="view_partner_grade_form" model="ir.ui.view">
<field name="name">res.partner.grade.form</field>
<field name="model">res.partner.grade</field>
<field name="arch" type="xml">
<form string="Partner Grade" version="7.0">
<group col="4">
</field>
</record>
<record id="view_res_partner_filter_assign" model="ir.ui.view">
<field name="name">res.partner.geo.inherit.search</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/>
<field name="arch" type="xml">
<filter string="Salesperson" position="after">
<filter string="Activation" context="{'group_by' : 'activation'}" domain="[]" icon="terp-personal" />
</filter>
<field name="category_id" position="after">
<field name="grade_id"/>
</field>
</field>
</record>
<record id="view_crm_partner_geo_form" model="ir.ui.view">
<field name="name">res.partner.geo.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook[last()]" position="inside">
<page string="Geo Localization">
<group>
<group>
<field name="name" />
<field name="partner_weight" />
<div>
<p class="oe_grey">
Define a weight to this grade. The weight will be used as default in the partner form to compute the chance for this partner to get leads. For instance, for business purpose, you can define a target revenue for each grade. To give the same chance to each partner to get leads, keep 1 in this field.
</p>
</div>
<separator string="Partner Activation" colspan="2"/>
<field name="grade_id" widget="selection"/>
<field name="activation" widget="selection"/>
<field name="partner_weight"/>
</group>
<group>
<field name="sequence" />
<field name="active" />
<separator string="Partner Review" colspan="2"/>
<field name="date_review"/>
<field name="date_review_next"/>
<field name="date_partnership"/>
</group>
</group>
</form>
</field>
</record>
<record id="res_partner_grade_action" model="ir.actions.act_window">
<field name="name">Partner Grade</field>
<field name="res_model">res.partner.grade</field>
<field name="view_type">form</field>
</record>
<menuitem action="res_partner_grade_action" id="menu_res_partner_grade_action" groups="base.group_no_one" parent="base.menu_crm_config_lead" />
<group colspan="2" col="2">
<separator string="Geo Localization" colspan="2"/>
<button
string="Geo Localize"
name="geo_localize"
colspan="2"
icon="gtk-apply"
type="object"/>
<field name="partner_latitude"/>
<field name="partner_longitude"/>
<field name="date_localization"/>
</group>
<newline/>
<!-- Partner form -->
<record id="view_res_partner_filter_assign_tree" model="ir.ui.view">
<field name="name">res.partner.geo.inherit.tree</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_tree" />
<field name="arch" type="xml">
<field name="user_id" position="after">
<field name="date_review_next" />
<field name="grade_id" />
<field name="activation" />
</field>
</field>
</record>
<record id="view_res_partner_filter_assign" model="ir.ui.view">
<field name="name">res.partner.geo.inherit.search</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter" />
<field name="arch" type="xml">
<filter string="Salesperson" position="after">
<filter string="Activation" context="{'group_by' : 'activation'}" domain="[]" icon="terp-personal" />
</filter>
<field name="category_id" position="after">
<field name="grade_id" />
</field>
</field>
</record>
<record id="view_crm_partner_geo_form" model="ir.ui.view">
<field name="name">res.partner.geo.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<xpath expr="//notebook[last()]" position="inside">
<page string="Forwarded Leads">
<group>
<group string="Partner Activation">
<label for="partner_latitude" string="Geolocalisation" />
<div class="oe_title oe_inline">
<h3 class="oe_inline">
<span class="oe_grey">( </span>
<field name="partner_latitude" nolabel="1" readonly="1" class="oe_inline" />
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&lt;=',0)]}">N </span>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&gt;=',0)]}">S </span>
<field name="partner_longitude" class="oe_inline" readonly="1" nolabel="1" />
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&lt;=',0)]}">E </span>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&gt;=',0)]}">W </span>
<span class="oe_grey">) </span>
</h3>
<button string="Geolocalize" name="geo_localize" class="oe_inline" type="object" />
</div>
<field name="grade_id" widget="selection" on_change="onchange_grade_id(grade_id)" />
<field name="partner_weight" class="oe_inline" />
<div colspan="2">
<p class="oe_grey">
Higher is the value, higher is the probability for this partner to get more leads.
</p>
</div>
</group>
<group>
<separator string="Partner Review" colspan="2" />
<field name="date_review" />
<field name="date_review_next" />
<field name="date_partnership" />
</group>
<group>
</group>
</group>
<newline />
<separator string="Forwarded Leads" colspan="2" />
<field name="opportunity_assigned_ids" colspan="4" nolabel="1">
<tree string="Assigned Opportunities" colors="blue:state=='pending';gray:state=='cancel'">
<field name="name" />
<field name="contact_name" />
<field name="email_from" />
<field name="phone" />
<field name="stage_id" />
<field name="state" invisible="1" />
</tree>
</field>
</page>
</xpath>
</field>
</record>
<field name="opportunity_assigned_ids" colspan="4" nolabel="1">
<tree string="Assigned Opportunities">
<field name="create_date"/>
<field name="name"/>
<field name="type"/>
<field name="stage_id"/>
<field name="section_id"
invisible="context.get('invisible_section', True)"
groups="base.group_multi_salesteams"/>
<field name="user_id" />
<button string="Convert to Opportunity"
name="convert_opportunity"
type="object"
attrs="{'invisible':[('type','=','opportunity'),('probability', '=', 100)]}" />
<button name="case_escalate" string="Escalate"
type="object"
icon="gtk-go-up"
attrs="{'invisible':[('probability', '=', 100)]}" />
</tree>
</field>
</page>
</xpath>
</field>
</record>
</data>
</openerp>
+1
View File
@@ -22,5 +22,6 @@
import email_template
import wizard
import res_partner
import ir_actions
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+1
View File
@@ -59,6 +59,7 @@ campaigns on any OpenERP document.
'wizard/email_template_preview_view.xml',
'email_template_view.xml',
'res_partner_view.xml',
'ir_actions_view.xml',
'wizard/mail_compose_message_view.xml',
'security/ir.model.access.csv'
],
+12
View File
@@ -0,0 +1,12 @@
.. _changelog:
Changelog
=========
`trunk (saas-2)`
----------------
- ``mail.compose.message``: added support of ``mail_server_id`` from template
- Server action update
- added `email` server action type, now entirely based on email templates.
+13
View File
@@ -0,0 +1,13 @@
Email Template module documentation
===================================
Email Template documentation topics
'''''''''''''''''''''''''''''''''''
Changelog
'''''''''
.. toctree::
:maxdepth: 1
changelog.rst
+86
View File
@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2013 OpenERP S.A. <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 fields, osv
class actions_server(osv.Model):
""" Add email option in server actions. """
_name = 'ir.actions.server'
_inherit = ['ir.actions.server']
def _get_states(self, cr, uid, context=None):
res = super(actions_server, self)._get_states(cr, uid, context=context)
res.insert(0, ('email', 'Send Email'))
return res
_columns = {
'email_from': fields.char('From',
help="Sender address; define the template to see its value. If not set, the default "
"value will be the author's email alias if configured, or email address."),
'email_to': fields.char('To (Emails)',
help="Comma-separated recipient addresses; define the template to see its value"),
'partner_to': fields.char('To (Partners)',
help="Comma-separated ids of recipient partners; define the template to see its value"),
'subject': fields.char('Subject',
help="Email subject; define the template to see its value"),
'body_html': fields.text('Body',
help="Rich-text/HTML version of the message; define the template to see its value"),
'template_id': fields.many2one('email.template', 'Email Template', ondelete='set null',
help="Define the email template to use for the email to send.")
}
def on_change_template_id(self, cr, uid, ids, template_id, context=None):
""" Render the raw template in the server action fields. """
if template_id:
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
if not values.get('email_from'):
return {'warning': {'title': 'Incomplete template', 'message': 'Your template should define email_from'}, 'value': values}
else:
values = self.default_get(cr, uid, ['subject', 'body_html', 'email_from', 'email_to', 'partner_to'], context=context)
return {'value': values}
def create(self, cr, uid, values, context=None):
if values.get('template_id'):
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
template_values = self.pool.get('email.template').read(cr, uid, values.get('template_id'), fields, context)
values.update(dict((field, template_values[field]) for field in fields if template_values.get(field)))
return super(actions_server, self).create(cr, uid, values, context=context)
def write(self, cr, uid, ids, values, context=None):
if values.get('template_id'):
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
template_values = self.pool.get('email.template').read(cr, uid, values.get('template_id'), fields, context)
values.update(dict((field, template_values[field]) for field in fields if template_values.get(field)))
return super(actions_server, self).write(cr, uid, ids, values, context=context)
def run_action_email(self, cr, uid, action, eval_context=None, context=None):
if not action.template_id or not context.get('active_id'):
return False
self.pool['email.template'].send_mail(cr, uid, action.template_id.id, context.get('active_id'),
force_send=False, raise_exception=False, context=context)
return False
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_server_action_form_template">
<field name="name">ir.actions.server.form</field>
<field name="model">ir.actions.server</field>
<field name="inherit_id" ref="base.view_server_action_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='code']" position="after">
<page string="Email" autofocus="autofocus"
attrs="{'invisible': [('state', '!=', 'email')]}">
<p attrs="{'invisible': [('model_id', '!=', False)]}">
Please set the Base Model before setting the action details.
</p>
<group attrs="{'invisible': [('model_id', '=', False)]}">
<field name="template_id"
on_change='on_change_template_id(template_id)'
domain="[('model_id', '=', model_id)]"
attrs="{'required': [('state', '=', 'email')]}"/>
<p colspan="2" attrs="{'invisible': [('template_id', '!=', False)]}">
Choose a template to display its values.
</p>
<p colspan="2" attrs="{'invisible': [('template_id', '=', False)]}">
The values displayed hereunder are informative. When sending the email, the values
will be taken from the email template.
</p>
</group>
<group attrs="{'invisible': ['|', ('model_id', '=', False), ('template_id', '=', False)]}">
<label for="email_from"/>
<div>
<field name="email_from" nolabel="1'" readonly="1"
attrs="{'required': [('state', '=', 'email')]}"/>
<p attrs="{'invisible': [('email_from', '!=', False)]}">
Your template does not defined any email_from. Please update your template.
</p>
</div>
<field name="email_to" readonly="1"/>
<field name="partner_to" readonly="1"/>
<field name="subject" readonly="1" attrs="{'required': [('state', '=', 'email')]}"/>
<field name="body_html" readonly="1" attrs="{'required': [('state', '=', 'email')]}"/>
</group>
</page>
</xpath>
</field>
</record>
</data>
</openerp>
+2 -1
View File
@@ -18,10 +18,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import test_mail
from . import test_mail, test_ir_actions
checks = [
test_mail,
test_ir_actions,
]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2013-TODAY OpenERP S.A. <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.base.tests.test_ir_actions import TestServerActionsBase
class TestServerActionsEmail(TestServerActionsBase):
def test_00_state_email(self):
""" Test ir.actions.server email type """
cr, uid = self.cr, self.uid
# create email_template
template_id = self.registry('email.template').create(cr, uid, {
'name': 'TestTemplate',
'email_from': 'myself@example.com',
'email_to': 'brigitte@example.com',
'partner_to': '[%s]' % self.test_partner_id,
'model_id': self.res_partner_model_id,
'subject': 'About ${object.name}',
'body_html': '<p>Dear ${object.name}, your parent is ${object.parent_id and object.parent_id.name or "False"}</p>',
})
self.ir_actions_server.write(cr, uid, self.act_id, {
'state': 'email',
'template_id': template_id,
})
run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
self.assertFalse(run_res, 'ir_actions_server: email server action correctly finished should return False')
# check an email is waiting for sending
mail_ids = self.registry('mail.mail').search(cr, uid, [('subject', '=', 'About TestingPartner')])
self.assertEqual(len(mail_ids), 1, 'ir_actions_server: TODO')
# check email content
mail = self.registry('mail.mail').browse(cr, uid, mail_ids[0])
self.assertEqual(mail.body, '<p>Dear TestingPartner, your parent is False</p>',
'ir_actions_server: TODO')
@@ -22,6 +22,7 @@
from openerp import tools
from openerp.osv import osv, fields
def _reopen(self, res_id, model):
return {'type': 'ir.actions.act_window',
'view_mode': 'form',
@@ -34,7 +35,8 @@ def _reopen(self, res_id, model):
'context': {
'default_model': model,
},
}
}
class mail_compose_message(osv.TransientModel):
_inherit = 'mail.compose.message'
@@ -58,7 +60,7 @@ class mail_compose_message(osv.TransientModel):
context = {}
wizard_context = dict(context)
for wizard in self.browse(cr, uid, ids, context=context):
if wizard.template_id and not wizard.template_id.user_signature:
if wizard.template_id:
wizard_context['mail_notify_user_signature'] = False # template user_signature is added when generating body_html
if not wizard.attachment_ids or wizard.composition_mode == 'mass_mail' or not wizard.template_id:
continue
@@ -75,7 +77,7 @@ class mail_compose_message(osv.TransientModel):
""" - mass_mailing: we cannot render, so return the template values
- normal mode: return rendered values """
if template_id and composition_mode == 'mass_mail':
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id']
template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
elif template_id:
@@ -95,7 +97,7 @@ class mail_compose_message(osv.TransientModel):
}
values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context))
else:
values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'email_to', 'email_cc', 'partner_to', 'reply_to', 'attachment_ids'], context=context)
values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'email_to', 'email_cc', 'partner_to', 'reply_to', 'attachment_ids', 'mail_server_id'], context=context)
if values.get('body_html'):
values['body'] = values.pop('body_html')
@@ -150,7 +152,7 @@ class mail_compose_message(osv.TransientModel):
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']
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', '')
@@ -28,7 +28,7 @@ import urllib2
import simplejson
class google_service(osv.osv):
class google_service(osv.osv_memory):
_name = 'google.service'
def generate_refresh_token(self, cr, uid, service, authorization_code, context=None):
@@ -51,10 +51,10 @@ class google_service(osv.osv):
content = simplejson.loads(content)
return content.get('refresh_token')
def _get_google_token_uri(self, cr, uid, service, context=None):
def _get_google_token_uri(self, cr, uid, service, scope, context=None):
ir_config = self.pool['ir.config_parameter']
params = {
'scope': 'https://www.googleapis.com/auth/drive',
'scope': scope,
'redirect_uri': ir_config.get_param(cr, SUPERUSER_ID, 'google_redirect_uri'),
'client_id': ir_config.get_param(cr, SUPERUSER_ID, 'google_%s_client_id' % service),
'response_type': 'code',
+52 -36
View File
@@ -54,58 +54,69 @@ class config(osv.osv):
attachment = attach_pool.browse(cr, uid, attach_ids[0], context)
url = attachment.url
else:
url = self.copy_doc(cr, uid, res_id, template_id, name_gdocs, model.model, context)
url = self.copy_doc(cr, uid, res_id, template_id, name_gdocs, model.model, context).get('url')
return url
def copy_doc(self, cr, uid, res_id, template_id, name_gdocs, res_model, context=None):
def get_access_token(self, cr, uid, scope=None, context=None):
ir_config = self.pool['ir.config_parameter']
google_drive_refresh_token = ir_config.get_param(cr, SUPERUSER_ID, 'google_drive_refresh_token')
group_config = self.pool['ir.model.data'].get_object_reference(cr, uid, 'base', 'group_erp_manager')[1]
user = self.pool['res.users'].read(cr, uid, uid, "groups_id")
if not google_drive_refresh_token:
raise self.pool.get('res.config.settings').get_config_warning(cr, _("You haven't configured 'Authorization Code' generated from google, Please generate and configure it in %(menu:base_setup.menu_general_configuration)s."), context=context)
if group_config in user['groups_id']:
raise self.pool.get('res.config.settings').get_config_warning(cr, _("You haven't configured 'Authorization Code' generated from google, Please generate and configure it in %(menu:base_setup.menu_general_configuration)s."), context=context)
else:
raise osv.except_osv(_('Error!'), _("Google Drive is not yet configured. Please contact your administrator."))
google_drive_client_id = ir_config.get_param(cr, SUPERUSER_ID, 'google_drive_client_id')
google_drive_client_secret = ir_config.get_param(cr, SUPERUSER_ID, 'google_drive_client_secret')
google_web_base_url = ir_config.get_param(cr, SUPERUSER_ID, 'web.base.url')
#For Getting New Access Token With help of old Refresh Token
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept-Encoding": "gzip, deflate"}
data = dict(client_id=google_drive_client_id,
refresh_token=google_drive_refresh_token,
client_secret=google_drive_client_secret,
grant_type="refresh_token")
data = urllib.urlencode(data)
data = urllib.urlencode(dict(client_id=google_drive_client_id,
refresh_token=google_drive_refresh_token,
client_secret=google_drive_client_secret,
grant_type="refresh_token",
scope=scope or 'https://www.googleapis.com/auth/drive'))
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept-Encoding": "gzip, deflate"}
try:
req = urllib2.Request('https://accounts.google.com/o/oauth2/token', data, headers)
content = urllib2.urlopen(req).read()
except urllib2.HTTPError:
raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during the token generation. Please request again an authorization code in %(menu:base_setup.menu_general_configuration)s."), context=context)
if group_config in user['groups_id']:
raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during the token generation. Please request again an authorization code in %(menu:base_setup.menu_general_configuration)s."), context=context)
else:
raise osv.except_osv(_('Error!'), _("Google Drive is not yet configured. Please contact your administrator."))
content = json.loads(content)
return content.get('access_token')
def copy_doc(self, cr, uid, res_id, template_id, name_gdocs, res_model, context=None):
ir_config = self.pool['ir.config_parameter']
google_web_base_url = ir_config.get_param(cr, SUPERUSER_ID, 'web.base.url')
access_token = self.get_access_token(cr, uid, context=context)
# Copy template in to drive with help of new access token
if 'access_token' in content:
request_url = "https://www.googleapis.com/drive/v2/files/%s?fields=parents/id&access_token=%s" % (template_id, content['access_token'])
try:
req = urllib2.Request(request_url, None, headers)
parents = urllib2.urlopen(req).read()
except urllib2.HTTPError:
raise self.pool.get('res.config.settings').get_config_warning(cr, _("The Google Template cannot be found. Maybe it has been deleted."), context=context)
parents_dict = json.loads(parents)
request_url = "https://www.googleapis.com/drive/v2/files/%s?fields=parents/id&access_token=%s" % (template_id, access_token)
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept-Encoding": "gzip, deflate"}
try:
req = urllib2.Request(request_url, None, headers)
parents = urllib2.urlopen(req).read()
except urllib2.HTTPError:
raise self.pool.get('res.config.settings').get_config_warning(cr, _("The Google Template cannot be found. Maybe it has been deleted."), context=context)
parents_dict = json.loads(parents)
record_url = "Click on link to open Record in OpenERP\n %s/?db=%s#id=%s&model=%s" % (google_web_base_url, cr.dbname, res_id, res_model)
data = {"title": name_gdocs, "description": record_url, "parents": parents_dict['parents']}
request_url = "https://www.googleapis.com/drive/v2/files/%s/copy?access_token=%s" % (template_id, content['access_token'])
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
data_json = json.dumps(data)
# resp, content = Http().request(request_url, "POST", data_json, headers)
req = urllib2.Request(request_url, data_json, headers)
content = urllib2.urlopen(req).read()
content = json.loads(content)
res = False
if 'alternateLink' in content.keys():
attach_pool = self.pool.get("ir.attachment")
attach_vals = {'res_model': res_model, 'name': name_gdocs, 'res_id': res_id, 'type': 'url', 'url': content['alternateLink']}
attach_pool.create(cr, uid, attach_vals)
res = content['alternateLink']
record_url = "Click on link to open Record in OpenERP\n %s/?db=%s#id=%s&model=%s" % (google_web_base_url, cr.dbname, res_id, res_model)
data = {"title": name_gdocs, "description": record_url, "parents": parents_dict['parents']}
request_url = "https://www.googleapis.com/drive/v2/files/%s/copy?access_token=%s" % (template_id, access_token)
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
data_json = json.dumps(data)
# resp, content = Http().request(request_url, "POST", data_json, headers)
req = urllib2.Request(request_url, data_json, headers)
content = urllib2.urlopen(req).read()
content = json.loads(content)
res = {}
if content.get('alternateLink'):
attach_pool = self.pool.get("ir.attachment")
attach_vals = {'res_model': res_model, 'name': name_gdocs, 'res_id': res_id, 'type': 'url', 'url': content['alternateLink']}
res['id'] = attach_pool.create(cr, uid, attach_vals)
res['url'] = content['alternateLink']
return res
def get_google_drive_config(self, cr, uid, res_model, res_id, context=None):
@@ -166,6 +177,7 @@ class config(osv.osv):
'google_drive_resource_id': fields.function(_resource_get, type="char", string='Resource Id'),
'google_drive_client_id': fields.function(_client_id_get, type="char", string='Google Client '),
'name_template': fields.char('Google Drive Name Pattern', size=64, help='Choose how the new google drive will be named, on google side. Eg. gdoc_%(field_name)s', required=True),
'active': fields.boolean('Active'),
}
def onchange_model_id(self, cr, uid, ids, model_id, context=None):
@@ -179,6 +191,7 @@ class config(osv.osv):
_defaults = {
'name_template': 'Document %(name)s',
'active': True,
}
def _check_model_id(self, cr, uid, ids, context=None):
@@ -191,6 +204,9 @@ class config(osv.osv):
(_check_model_id, 'Model of selected filter is not matching with model of current template.', ['model_id', 'filter_id']),
]
def get_google_scope(self):
return 'https://www.googleapis.com/auth/drive'
config()
@@ -202,7 +218,7 @@ class base_config_settings(osv.osv):
'google_drive_uri': fields.char('URI', readonly=True, help="The URL to generate the authorization code from Google"),
}
_defaults = {
'google_drive_uri': lambda s, cr, uid, c: s.pool['google.service']._get_google_token_uri(cr, uid, 'drive', context=c),
'google_drive_uri': lambda s, cr, uid, c: s.pool['google.service']._get_google_token_uri(cr, uid, 'drive', scope=s.pool['google.drive.config'].get_google_scope(), context=c),
}
def set_google_authorization_code(self, cr, uid, ids, context=None):
+2 -2
View File
@@ -4,12 +4,12 @@
<record id="config_google_drive_client_id" model="ir.config_parameter">
<field name="key">google_drive_client_id</field>
<field name="value">39623646228-eg3ggo3mk6o40m7rguobi3rkl9frh4tb.apps.googleusercontent.com</field>
<field name="value">598905559630.apps.googleusercontent.com</field>
</record>
<record id="config_google_drive_client_secret" model="ir.config_parameter">
<field name="key">google_drive_client_secret</field>
<field name="value">Ul-PtmnSWs3euWs20fdono0e</field>
<field name="value">vTmou73c-njP-1qCxm7qx7QE</field>
</record>
</data>
+70 -26
View File
@@ -8,19 +8,19 @@ msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2013-06-27 16:03+0000\n"
"PO-Revision-Date: 2012-12-12 12:51+0000\n"
"Last-Translator: Pedro Manuel Baeza <pedro.baeza@gmail.com>\n"
"PO-Revision-Date: 2013-08-07 02:00+0000\n"
"Last-Translator: Alejandro Santana <alejandrosantana@anubia.es>\n"
"Language-Team: Spanish <es@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-06-28 05:42+0000\n"
"X-Generator: Launchpad (build 16681)\n"
"X-Launchpad-Export-Date: 2013-08-08 04:39+0000\n"
"X-Generator: Launchpad (build 16723)\n"
#. module: google_drive
#: model:ir.ui.menu,name:google_drive.menu_google_drive_config
msgid "Google Drive configuration"
msgstr ""
msgstr "Configuración de Google Drive"
#. module: google_drive
#: code:addons/google_drive/google_drive.py:48
@@ -39,6 +39,11 @@ msgid ""
"in your Google Drive and in OpenERP attachment will be named\n"
" 'Agrolait_SO0001_Sales'."
msgstr ""
"El nombre del documento adjunto puede contener información fija o variable. "
"Para distinguir documentos en Google Drive, use palabras y campos fijos. "
"Así, en el ejemplo anterior, si se escribió 'Agrolait_%(name)s_Sales' en el "
"campo de nombre de GoogleDrive, el documento en su Google Drive y en los "
"adjuntos de OpenERP se llamarán 'Agrolait_SO0001_Sales'."
#. module: google_drive
#: view:google.drive.config:0
@@ -46,16 +51,18 @@ msgid ""
"- If filter is not specified, link of google document will appear in "
"\"More\" option for all users for all opportunities."
msgstr ""
"- Si no especifica un filtro, el enlace del documento de Google aparecerá en "
"la opción \"Más\" para todos los usuarios para todas las oportunidades."
#. module: google_drive
#: view:google.drive.config:0
msgid "To create a new filter:"
msgstr ""
msgstr "Para crear un nuevo filtro:"
#. module: google_drive
#: model:ir.model,name:google_drive.model_base_config_settings
msgid "base.config.settings"
msgstr ""
msgstr "base.config.settings"
#. module: google_drive
#: model:ir.actions.act_window,help:google_drive.action_google_drive_users_config
@@ -73,17 +80,29 @@ msgid ""
" </p>\n"
" "
msgstr ""
"<p class=\"oe_view_nocontent_create\">\n"
" Pulse para añadir una plantilla.\n"
" </p>\n"
" <p>\n"
" Enlace sus propias plantillas de Google Drive a "
"cualquier registro de OpenERP. Si tiene documentos realmente específicos que "
"quiere que su colaborador rellene (como usar una hoja de cálculo para "
"controlar la calidad de su producto o revisar los detalles de entrega de "
"cada pedido en un país extranjero,...) es muy sencillo gestionarlo. "
"Enlácelos en OpenERP y úselos para colaborar con sus empleados.\n"
" </p>\n"
" "
#. module: google_drive
#: code:addons/google_drive/google_drive.py:150
#, python-format
msgid "Incorrect URL!"
msgstr ""
msgstr "¡URL incorrecta!"
#. module: google_drive
#: view:base.config.settings:0
msgid "Configure your templates"
msgstr ""
msgstr "Configure sus plantillas"
#. module: google_drive
#: help:google.drive.config,name_template:0
@@ -91,6 +110,8 @@ msgid ""
"Choose how the new google drive will be named, on google side. Eg. "
"gdoc_%(field_name)s"
msgstr ""
"Elija el nombre de la nueva unidad de Google Drive, en el lado de Google. "
"P.ej.: gdoc_%(field_name)s"
#. module: google_drive
#: view:google.drive.config:0
@@ -98,6 +119,8 @@ msgid ""
"- Go to the OpenERP document you want to filter. For instance, go to "
"Opportunities and search on Sales Department."
msgstr ""
"- Vaya al documento de OpenERP que quiera filtrar. Por ejemplo, vaya a "
"Oportunidades y busque en el Departamento de ventas."
#. module: google_drive
#: view:google.drive.config:0
@@ -105,6 +128,8 @@ msgid ""
"- In this \"Search\" view, select the option \"Save Current Filter\", enter "
"the name (Ex: Sales Department)"
msgstr ""
"- En esta vista de \"Búsqueda\", seleccione la opción \"Guardar filtro "
"actual\" e introduzca el nombre (p.ej.: Departamento de ventas)"
#. module: google_drive
#: view:google.drive.config:0
@@ -113,6 +138,9 @@ msgid ""
"\"More\" options will appear for all users in opportunities of Sales "
"Department."
msgstr ""
"- Si selecciona \"Compartir con todos los usuarios\", el enlace al documento "
"de Google en la opción \"Más\" aparecerá para todos los usuarios en las "
"oportunidades del Departamento de ventas."
#. module: google_drive
#: view:google.drive.config:0
@@ -121,23 +149,28 @@ msgid ""
"\"More\" options will not appear for other users in opportunities of Sales "
"Department."
msgstr ""
"- Si no selecciona \"Compartir con todos los usuarios\", el enlace al "
"documento de Google en la opción \"Más\" no aparecerá para otros usuarios en "
"las oportunidades del Departamento de ventas."
#. module: google_drive
#: code:addons/google_drive/google_drive.py:48
#, python-format
msgid "At least one key cannot be found in your Google Drive name pattern"
msgstr ""
"No se ha podido encontrar al menos una clave de su patrón de nombres de "
"Google Drive"
#. module: google_drive
#: code:addons/google_drive/google_drive.py:150
#, python-format
msgid "Please enter a valid Google Document URL."
msgstr ""
msgstr "Por favor, introduzca una URL de documento de Google válida."
#. module: google_drive
#: field:google.drive.config,google_drive_client_id:0
msgid "Google Client "
msgstr ""
msgstr "Cliente de Google "
#. module: google_drive
#: view:google.drive.config:0
@@ -145,43 +178,45 @@ msgid ""
"https://docs.google.com/document/d/1vOtpJK9scIQz6taD9tJRIETWbEw3fSiaQHArsJYcu"
"a4/edit"
msgstr ""
"https://docs.google.com/document/d/1vOtpJK9scIQz6taD9tJRIETWbEw3fSiaQHArsJYcu"
"a4/edit"
#. module: google_drive
#: field:google.drive.config,filter_id:0
msgid "Filter"
msgstr ""
msgstr "Filtro"
#. module: google_drive
#: field:google.drive.config,name_template:0
msgid "Google Drive Name Pattern"
msgstr ""
msgstr "Patrón de nombres de Google Drive"
#. module: google_drive
#: help:base.config.settings,google_drive_uri:0
msgid "The URL to generate the authorization code from Google"
msgstr ""
msgstr "La URL para generar el código de autorización de Google"
#. module: google_drive
#: model:ir.filters,name:google_drive.filter_partner
msgid "Customer"
msgstr ""
msgstr "Cliente"
#. module: google_drive
#: field:google.drive.config,google_drive_resource_id:0
msgid "Resource Id"
msgstr ""
msgstr "ID del recurso"
#. module: google_drive
#: code:addons/google_drive/google_drive.py:91
#, python-format
msgid "The Google Template cannot be found. Maybe it has been deleted."
msgstr ""
msgstr "No se encuentra la plantilla de Google. Tal vez se haya eliminado."
#. module: google_drive
#: model:ir.actions.act_window,name:google_drive.action_google_drive_users_config
#: model:ir.ui.menu,name:google_drive.menu_google_drive_model_config
msgid "Google Drive Templates"
msgstr ""
msgstr "Plantillas de Google Drive"
#. module: google_drive
#: code:addons/google_drive/google_drive.py:81
@@ -190,23 +225,27 @@ msgid ""
"Something went wrong during the token generation. Please request again an "
"authorization code in %(menu:base_setup.menu_general_configuration)s."
msgstr ""
"Algo ha ido mal durante la creación del testigo. Por favor, solicite un "
"código de autorización otra vez en "
"%(menu:base_setup.menu_general_configuration)s."
#. module: google_drive
#: code:addons/google_drive/google_drive.py:124
#, python-format
msgid "Google Drive Error!"
msgstr ""
msgstr "¡Error de Google Drive!"
#. module: google_drive
#: field:base.config.settings,google_drive_uri:0
msgid "URI"
msgstr ""
msgstr "URI"
#. module: google_drive
#: code:addons/google_drive/google_drive.py:124
#, python-format
msgid "Creating google drive may only be done by one at a time."
msgstr ""
"Es posible que la creación de Google Drive sólo se pueda hacer de una en una."
#. module: google_drive
#: field:google.drive.config,model:0
@@ -217,38 +256,40 @@ msgstr "Modelo"
#. module: google_drive
#: view:google.drive.config:0
msgid "Google Drive Configuration"
msgstr ""
msgstr "Configuración de Google Drive"
#. module: google_drive
#: field:google.drive.config,name:0
msgid "Template Name"
msgstr ""
msgstr "Nombre de la plantilla"
#. module: google_drive
#: constraint:google.drive.config:0
msgid ""
"Model of selected filter is not matching with model of current template."
msgstr ""
"El modelo del filtro seleccionado no coincide con el modelo de la plantilla "
"actual."
#. module: google_drive
#: field:google.drive.config,google_drive_template_url:0
msgid "Template URL"
msgstr ""
msgstr "URL de la plantilla"
#. module: google_drive
#: view:base.config.settings:0
msgid "and paste it here"
msgstr ""
msgstr "y péguela aquí"
#. module: google_drive
#: field:base.config.settings,google_drive_authorization_code:0
msgid "Authorization Code"
msgstr ""
msgstr "Cídigo de autorización"
#. module: google_drive
#: model:ir.model,name:google_drive.model_google_drive_config
msgid "Google Drive templates config"
msgstr ""
msgstr "Configuración de plantillas de Google Drive"
#. module: google_drive
#: code:addons/google_drive/google_drive.py:64
@@ -257,3 +298,6 @@ msgid ""
"You haven't configured 'Authorization Code' generated from google, Please "
"generate and configure it in %(menu:base_setup.menu_general_configuration)s."
msgstr ""
"No ha configurado el 'código de autorización' generado por Google. Por "
"favor, genérelo y configúrelo en "
"%(menu:base_setup.menu_general_configuration)s."
+3 -2
View File
@@ -23,6 +23,7 @@
<field name="model" invisible="1" />
<group>
<field name="name" />
<field name="active" />
<field name="model_id" on_change="onchange_model_id(model_id)" />
<label for='filter_id' />
<div>
@@ -54,7 +55,7 @@
</record>
<record model='ir.actions.act_window' id='action_google_drive_users_config'>
<field name='name'>Google Drive Templates</field>
<field name='name'>Templates</field>
<field name='res_model'>google.drive.config</field>
<field name='type'>ir.actions.act_window</field>
<field name='view_type'>form</field>
@@ -87,7 +88,7 @@
</field>
</record>
<menuitem name='Google Drive configuration' id='menu_google_drive_config' parent='base.menu_administration' />
<menuitem name='Google Drive' id='menu_google_drive_config' parent='base.menu_administration' />
<menuitem id='menu_google_drive_model_config' parent='menu_google_drive_config' action='action_google_drive_users_config' />
</data>
</openerp>
+17 -12
View File
@@ -29,19 +29,24 @@ openerp.google_drive = function (instance, m) {
ds.call('get_google_drive_config', [view.dataset.model, res_id, context]).done(function (r) {
if (!_.isEmpty(r)) {
_.each(r, function (res) {
var g_item = _.indexOf(_.pluck(self.items.other, 'label'), res.name);
if (g_item !== -1) {
self.items.other.splice(g_item, 1);
var already_there = false;
for (var i = 0;i < self.items.other.length;i++){
if (self.items.other[i].classname === "oe_share_gdoc" && self.items.other[i].label.indexOf(res.name) > -1){
already_there = true;
break;
}
}
if (!already_there){
self.add_items('other', [{
label: res.name+ '<img style="position:absolute;right:5px;height:20px;width:20px;" title="Google Drive" src="google_drive/static/src/img/drive_icon.png"/>',
config_id: res.id,
res_id: res_id,
res_model: view.dataset.model,
callback: self.on_google_doc,
classname: 'oe_share_gdoc'
},
]);
}
self.add_items('other', [{
label: res.name+ '<img style="position:absolute;right:5px;height:20px;width:20px;" title="Google Drive" src="google_drive/static/src/img/drive_icon.png"/>',
config_id: res.id,
res_id: res_id,
res_model: view.dataset.model,
callback: self.on_google_doc,
classname: 'oe_share_gdoc'
},
]);
})
}
});
+1
View File
@@ -0,0 +1 @@
import google_spreadsheet
+43
View File
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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': 'Google Spreadsheet',
'version': '1.0',
'category': 'Tools',
'description': """
The module adds the possibility to display data from OpenERP in Google Spreadsheets in real time.
========================================
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'depends': ['board', 'google_drive'],
'js': [
'static/src/js/search.js',
],
'qweb': ['static/src/xml/*.xml'],
'data': ['google_spreadsheet_view.xml', 'google_spreadsheet_data.xml'],
'demo': [],
'installable': True,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@ -0,0 +1,119 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2012 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 simplejson
import logging
from lxml import etree
import re
import urllib
import urllib2
from openerp.osv import osv
from openerp import SUPERUSER_ID
_logger = logging.getLogger(__name__)
class config(osv.osv):
_inherit = 'google.drive.config'
def get_google_scope(self):
scope = super(config, self).get_google_scope()
return '%s https://spreadsheets.google.com/feeds' % scope
def write_config_formula(self, cr, uid, attachment_id, spreadsheet_key, model, domain, groupbys, view_id, context=None):
access_token = self.get_access_token(cr, uid, scope='https://spreadsheets.google.com/feeds', context=context)
fields = self.pool.get(model).fields_view_get(cr, uid, view_id=view_id, view_type='tree')
doc = etree.XML(fields.get('arch'))
display_fields = []
for node in doc.xpath("//field"):
if node.get('modifiers'):
modifiers = simplejson.loads(node.get('modifiers'))
if not modifiers.get('invisible') and not modifiers.get('tree_invisible'):
display_fields.append(node.get('name'))
fields = " ".join(display_fields)
domain = domain.replace("'", r"\'").replace('"', "'")
if groupbys:
fields = "%s %s" % (groupbys, fields)
formula = '=oe_read_group("%s";"%s";"%s";"%s")' % (model, fields, groupbys, domain)
else:
formula = '=oe_browse("%s";"%s";"%s")' % (model, fields, domain)
url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
dbname = cr.dbname
user = self.pool['res.users'].read(cr, uid, uid, ['login', 'password'], context=context)
username = user['login']
password = user['password']
if self.pool['ir.module.module'].search_count(cr, SUPERUSER_ID, ['&', ('name', '=', 'auth_crypt'), ('state', '=', 'installed')]) == 1:
config_formula = '=oe_settings("%s";"%s")' % (url, dbname)
else:
config_formula = '=oe_settings("%s";"%s";"%s";"%s")' % (url, dbname, username, password)
request = '''<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:batch="http://schemas.google.com/gdata/batch"
xmlns:gs="http://schemas.google.com/spreadsheets/2006">
<id>https://spreadsheets.google.com/feeds/cells/%s/od6/private/full</id>
<entry>
<batch:id>A1</batch:id>
<batch:operation type="update"/>
<id>https://spreadsheets.google.com/feeds/cells/%s/od6/private/full/R1C1</id>
<link rel="edit" type="application/atom+xml"
href="https://spreadsheets.google.com/feeds/cells/%s/od6/private/full/R1C1"/>
<gs:cell row="1" col="1" inputValue="%s"/>
</entry>
<entry>
<batch:id>A2</batch:id>
<batch:operation type="update"/>
<id>https://spreadsheets.google.com/feeds/cells/%s/od6/private/full/R60C15</id>
<link rel="edit" type="application/atom+xml"
href="https://spreadsheets.google.com/feeds/cells/%s/od6/private/full/R60C15"/>
<gs:cell row="60" col="15" inputValue="%s"/>
</entry>
</feed>''' % (spreadsheet_key, spreadsheet_key, spreadsheet_key, formula.replace('"', '&quot;'), spreadsheet_key, spreadsheet_key, config_formula.replace('"', '&quot;'))
try:
req = urllib2.Request(
'https://spreadsheets.google.com/feeds/cells/%s/od6/private/full/batch?%s' % (spreadsheet_key, urllib.urlencode({'v': 3, 'access_token': access_token})),
data=request,
headers={'content-type': 'application/atom+xml', 'If-Match': '*'})
urllib2.urlopen(req)
except (urllib2.HTTPError, urllib2.URLError):
_logger.warning("An error occured while writting the formula on the Google Spreadsheet.")
description = '''
formula: %s
''' % formula
if attachment_id:
self.pool['ir.attachment'].write(cr, uid, attachment_id, {'description': description}, context=context)
return True
def set_spreadsheet(self, cr, uid, model, domain, groupbys, view_id, context=None):
try:
config_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'google_spreadsheet', 'google_spreadsheet_template')[1]
except ValueError:
raise
config = self.browse(cr, uid, config_id, context=context)
title = 'Spreadsheet %s' % model
res = self.copy_doc(cr, uid, False, config.google_drive_resource_id, title, model, context=context)
mo = re.search("(key=|/d/)([A-Za-z0-9-_]+)", res['url'])
if mo:
key = mo.group(2)
self.write_config_formula(cr, uid, res.get('id'), key, model, domain, groupbys, view_id, context=context)
return res
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="google_spreadsheet_template" model="google.drive.config">
<field name="name">Base Spreadsheet Template</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="google_drive_template_url">https://docs.google.com/spreadsheet/ccc?key=0ApGVjjwUC-ygdDZ0TG5EQnRlLVFQNlFGdFN5b1ZrY1E</field>
<field name="name_template">Reporting %(name)s</field>
<field name="active" eval="0" />
</record>
</data>
</openerp>
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- add google drive config field in user form -->
<record model="ir.ui.view" id="view_ir_attachment_google_spreadsheet_tree">
<field name="name">ir.attachment.google.spreadsheet.tree</field>
<field name="model">ir.attachment</field>
<field name="arch" type="xml">
<tree string="Google Spreadsheets" version="7.0">
<field name="name" string="Name"/>
<field name="url" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_ir_attachment_google_spreadsheet_form">
<field name="name">ir.attachment.google.spreadsheet.form</field>
<field name="model">ir.attachment</field>
<field name="arch" type="xml">
<form string="Google Spreadsheets" version="7.0">
<sheet>
<group>
<group>
<field name="name" string="Name"/>
<field name="url" widget="url"/>
</group>
<group colspan="2">
<label for="description" colspan="2"/>
<field name="description" nolabel="1" colspan="2"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_ir_attachment_google_spreadsheet_tree" model="ir.actions.act_window">
<field name="name">Google Spreadsheets</field>
<field name="res_model">ir.attachment</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{}</field>
<field name="domain">[('url', 'like', '/spreadsheet/')]</field>
<field name="help">Google Spreadsheets</field>
</record>
<record id="action_ir_attachment_google_spreadsheet_tree_view" model="ir.actions.act_window.view">
<field eval="1" name="sequence"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_ir_attachment_google_spreadsheet_tree"/>
<field name="act_window_id" ref="action_ir_attachment_google_spreadsheet_tree"/>
</record>
<record id="action_ir_attachment_google_spreadsheet_form_view" model="ir.actions.act_window.view">
<field eval="2" name="sequence"/>
<field name="view_mode">form</field>
<field name="view_id" ref="view_ir_attachment_google_spreadsheet_form"/>
<field name="act_window_id" ref="action_ir_attachment_google_spreadsheet_tree"/>
</record>
<menuitem id="menu_reporting_dashboard_google_spreadsheets" parent="base.menu_reporting_dashboard" action="action_ir_attachment_google_spreadsheet_tree"/>
</data>
</openerp>
@@ -0,0 +1,42 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * google_spreadsheet
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 8.0alpha1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-25 12:39+0000\n"
"PO-Revision-Date: 2013-07-25 12:39+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: google_spreadsheet
#: model:ir.actions.act_window,help:google_spreadsheet.action_ir_attachment_google_spreadsheet_tree
#: model:ir.actions.act_window,name:google_spreadsheet.action_ir_attachment_google_spreadsheet_tree
#: view:ir.attachment:0
#: model:ir.ui.menu,name:google_spreadsheet.menu_reporting_dashboard_google_spreadsheets
msgid "Google Spreadsheets"
msgstr ""
#. module: google_spreadsheet
#. openerp-web
#: code:addons/google_spreadsheet/static/src/xml/addtospreadsheet.xml:3
#, python-format
msgid "Add to Google Spreadsheet"
msgstr ""
#. module: google_spreadsheet
#: view:ir.attachment:0
msgid "Name"
msgstr ""
#. module: google_spreadsheet
#: model:ir.model,name:google_spreadsheet.model_google_drive_config
msgid "Google Drive templates config"
msgstr ""
@@ -0,0 +1,54 @@
openerp.google_spreadsheet = function(instance) {
var _t = instance.web._t;
instance.web.FormView.include({
on_processed_onchange: function(result, processed) {
var self = this;
var fields = self.fields;
_(result.selection).each(function (selection, fieldname) {
var field = fields[fieldname];
if (!field) { return; }
field.field.selection = selection;
field.values = selection;
field.renderElement();
});
return this._super(result, processed);
},
});
instance.board.AddToGoogleSpreadsheet = instance.web.search.Input.extend({
template: 'SearchView.addtogooglespreadsheet',
_in_drawer: true,
start: function () {
var self = this;
this.$el.on('click', 'h4', function(){
var view = self.view;
var data = view.build_search_data();
var model = view.model;
var list_view = self.view.getParent().views['list'];
var view_id = list_view ? list_view.view_id : false;
var context = new instance.web.CompoundContext(view.dataset.get_context() || []);
var domain = new instance.web.CompoundDomain(view.dataset.get_domain() || []);
_.each(data.contexts, context.add, context);
_.each(data.domains, domain.add, domain);
domain = JSON.stringify(domain.eval());
var groupbys = instance.web.pyeval.eval('groupbys', data.groupbys).join(" ");
var view_id = view_id;
var ds = new instance.web.DataSet(self, 'google.drive.config');
ds.call('set_spreadsheet', [model, domain, groupbys, view_id]).done(function (res) {
if (res['url']){
window.open(res['url'], '_blank');
}
});
});
},
});
instance.web.SearchView.include({
add_common_inputs: function() {
this._super();
var vm = this.getParent().getParent();
if (vm.inner_action && vm.inner_action.views) {
(new instance.board.AddToGoogleSpreadsheet(this));
}
}
});
};
@@ -0,0 +1,5 @@
<template>
<div t-name="SearchView.addtogooglespreadsheet" class="oe_searchview_dashboard">
<h4>Add to Google Spreadsheet</h4>
</div>
</template>
+13
View File
@@ -0,0 +1,13 @@
.. _changelog:
Changelog
=========
`trunk (saas-2)`
----------------
- updated ``hr.holidays`` workflow. It now starts in ``confirm`` state. In
``confirm```and ``refuse`` a Reset to Draft has been added in view / workflow,
allowing to edit the request. Added a ``can_reset`` computed field to enable
this transition. A user can edit its own requests, or all requests if he is
an Hr Manager.
+13
View File
@@ -0,0 +1,13 @@
Hr Holidays module documentation
================================
Hr Holidays documentation topics
''''''''''''''''''''''''''''''''
Changelog
'''''''''
.. toctree::
:maxdepth: 1
changelog.rst
+59 -52
View File
@@ -22,11 +22,10 @@
##############################################################################
import datetime
import time
from itertools import groupby
from operator import attrgetter, itemgetter
import math
import time
from operator import attrgetter
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
@@ -36,40 +35,37 @@ class hr_holidays_status(osv.osv):
_name = "hr.holidays.status"
_description = "Leave Type"
def get_days(self, cr, uid, ids, employee_id, return_false, context=None):
cr.execute("""SELECT id, type, number_of_days, holiday_status_id FROM hr_holidays WHERE employee_id = %s AND state='validate' AND holiday_status_id in %s""",
[employee_id, tuple(ids)])
result = sorted(cr.dictfetchall(), key=lambda x: x['holiday_status_id'])
grouped_lines = dict((k, [v for v in itr]) for k, itr in groupby(result, itemgetter('holiday_status_id')))
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = {}
max_leaves = leaves_taken = 0
if not return_false:
if record.id in grouped_lines:
leaves_taken = -sum([item['number_of_days'] for item in grouped_lines[record.id] if item['type'] == 'remove'])
max_leaves = sum([item['number_of_days'] for item in grouped_lines[record.id] if item['type'] == 'add'])
res[record.id]['max_leaves'] = max_leaves
res[record.id]['leaves_taken'] = leaves_taken
res[record.id]['remaining_leaves'] = max_leaves - leaves_taken
return res
def get_days(self, cr, uid, ids, employee_id, context=None):
result = dict((id, dict(max_leaves=0, leaves_taken=0, remaining_leaves=0,
virtual_remaining_leaves=0)) for id in ids)
holiday_ids = self.pool['hr.holidays'].search(cr, uid, [('employee_id', '=', employee_id),
('state', 'in', ['confirm', 'validate1', 'validate']),
('holiday_status_id', 'in', ids)
], context=context)
for holiday in self.pool['hr.holidays'].browse(cr, uid, holiday_ids, context=context):
status_dict = result[holiday.holiday_status_id.id]
if holiday.type == 'add':
status_dict['virtual_remaining_leaves'] += holiday.number_of_days
if holiday.state == 'validate':
status_dict['max_leaves'] += holiday.number_of_days
status_dict['remaining_leaves'] += holiday.number_of_days
elif holiday.type == 'remove': # number of days is negative
status_dict['virtual_remaining_leaves'] += holiday.number_of_days
if holiday.state == 'validate':
status_dict['leaves_taken'] -= holiday.number_of_days
status_dict['remaining_leaves'] += holiday.number_of_days
return result
def _user_left_days(self, cr, uid, ids, name, args, context=None):
return_false = False
employee_id = False
res = {}
if context and context.has_key('employee_id'):
if not context['employee_id']:
return_false = True
if context and 'employee_id' in context:
employee_id = context['employee_id']
else:
employee_ids = self.pool.get('hr.employee').search(cr, uid, [('user_id','=',uid)], context=context)
employee_ids = self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
if employee_ids:
employee_id = employee_ids[0]
else:
return_false = True
if employee_id:
res = self.get_days(cr, uid, ids, employee_id, return_false, context=context)
res = self.get_days(cr, uid, ids, employee_id, context=context)
else:
res = dict.fromkeys(ids, {'leaves_taken': 0, 'remaining_leaves': 0, 'max_leaves': 0})
return res
@@ -84,6 +80,7 @@ class hr_holidays_status(osv.osv):
'max_leaves': fields.function(_user_left_days, string='Maximum Allowed', help='This value is given by the sum of all holidays requests with a positive value.', multi='user_left_days'),
'leaves_taken': fields.function(_user_left_days, string='Leaves Already Taken', help='This value is given by the sum of all holidays requests with a negative value.', multi='user_left_days'),
'remaining_leaves': fields.function(_user_left_days, string='Remaining Leaves', help='Maximum Leaves Allowed - Leaves Already Taken', multi='user_left_days'),
'virtual_remaining_leaves': fields.function(_user_left_days, string='Virtual Remaining Leaves', help='Maximum Leaves Allowed - Leaves Already Taken - Leaves Waiting Approval', multi='user_left_days'),
'double_validation': fields.boolean('Apply Double Validation', help="When selected, the Allocation/Leave Requests for this type require a second validation to be approved."),
}
_defaults = {
@@ -92,8 +89,6 @@ class hr_holidays_status(osv.osv):
}
def name_get(self, cr, uid, ids, context=None):
if not ids:
return []
res = []
for record in self.browse(cr, uid, ids, context=context):
name = record.name
@@ -134,6 +129,19 @@ class hr_holidays(osv.osv):
result[hol.id] = hol.number_of_days_temp
return result
def _get_can_reset(self, cr, uid, ids, name, arg, context=None):
"""User can reset a leave request if it is its own leave request or if
he is an Hr Manager. """
user = self.pool['res.users'].browse(cr, uid, uid, context=context)
group_hr_manager_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_hr_manager')[1]
if group_hr_manager_id in [g.id for g in user.groups_id]:
return dict.fromkeys(ids, True)
result = dict.fromkeys(ids, False)
for holiday in self.browse(cr, uid, ids, context=context):
if holiday.employee_id and holiday.employee_id.user_id and holiday.employee_id.user_id.id == uid:
result[holiday.id] = True
return result
def _check_date(self, cr, uid, ids):
for holiday in self.browse(cr, uid, ids):
holiday_ids = self.search(cr, uid, [('date_from', '<=', holiday.date_to), ('date_to', '>=', holiday.date_from), ('employee_id', '=', holiday.employee_id.id), ('id', '<>', holiday.id)])
@@ -167,10 +175,13 @@ class hr_holidays(osv.osv):
'holiday_type': fields.selection([('employee','By Employee'),('category','By Employee Tag')], 'Allocation Mode', readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}, help='By Employee: Allocation/Request for individual Employee, By Employee Tag: Allocation/Request for group of employees in category', required=True),
'manager_id2': fields.many2one('hr.employee', 'Second Approval', readonly=True, help='This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)'),
'double_validation': fields.related('holiday_status_id', 'double_validation', type='boolean', relation='hr.holidays.status', string='Apply Double Validation'),
'can_reset': fields.function(
_get_can_reset,
type='boolean'),
}
_defaults = {
'employee_id': _employee_get,
'state': 'draft',
'state': 'confirm',
'type': 'remove',
'user_id': lambda obj, cr, uid, context: uid,
'holiday_type': 'employee'
@@ -302,27 +313,20 @@ class hr_holidays(osv.osv):
if context is None:
context = {}
context = dict(context, mail_create_nolog=True)
return super(hr_holidays, self).create(cr, uid, values, context=context)
hol_id = super(hr_holidays, self).create(cr, uid, values, context=context)
self.check_holidays(cr, uid, [hol_id], context=context)
return hol_id
def write(self, cr, uid, ids, vals, context=None):
check_fnct = self.pool.get('hr.holidays.status').check_access_rights
for holiday in self.browse(cr, uid, ids, context=context):
if holiday.state in ('validate','validate1') and not check_fnct(cr, uid, 'write', raise_exception=False):
raise osv.except_osv(_('Warning!'),_('You cannot modify a leave request that has been approved. Contact a human resource manager.'))
return super(hr_holidays, self).write(cr, uid, ids, vals, context=context)
def set_to_draft(self, cr, uid, ids, context=None):
def holidays_reset(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {
'state': 'draft',
'manager_id': False,
'manager_id2': False,
})
self.delete_workflow(cr, uid, ids)
self.create_workflow(cr, uid, ids)
to_unlink = []
for record in self.browse(cr, uid, ids, context=context):
for record2 in record.linked_request_ids:
self.set_to_draft(cr, uid, [record2.id], context=context)
self.holidays_reset(cr, uid, [record2.id], context=context)
to_unlink.append(record2.id)
if to_unlink:
self.unlink(cr, uid, to_unlink, context=context)
@@ -421,13 +425,16 @@ class hr_holidays(osv.osv):
return True
def check_holidays(self, cr, uid, ids, context=None):
holi_status_obj = self.pool.get('hr.holidays.status')
for record in self.browse(cr, uid, ids):
if record.holiday_type == 'employee' and record.type == 'remove':
if record.employee_id and not record.holiday_status_id.limit:
leaves_rest = holi_status_obj.get_days( cr, uid, [record.holiday_status_id.id], record.employee_id.id, False)[record.holiday_status_id.id]['remaining_leaves']
if leaves_rest < record.number_of_days_temp:
raise osv.except_osv(_('Warning!'), _('There are not enough %s allocated for employee %s; please create an allocation request for this leave type.') % (record.holiday_status_id.name, record.employee_id.name))
for record in self.browse(cr, uid, ids, context=context):
if record.holiday_type != 'employee' or record.type != 'remove' or not record.employee_id or record.holiday_status_id.limit:
continue
leave_days = self.pool.get('hr.holidays.status').get_days(cr, uid, [record.holiday_status_id.id], record.employee_id.id, context=context)[record.holiday_status_id.id]
if leave_days['remaining_leaves'] < record.number_of_days_temp:
raise osv.except_osv(_('Warning!'),
_('There are not enough remaining days available in %s for employee %s.') % (record.holiday_status_id.name, record.employee_id.name))
if leave_days['virtual_remaining_leaves'] < record.number_of_days_temp:
raise osv.except_osv(_('Warning!'),
_('Other pending requests already book too much days in %s for employee %s.') % (record.holiday_status_id.name, record.employee_id.name))
return True
# -----------------------------
+1 -1
View File
@@ -49,7 +49,7 @@
<record id="mt_holidays_confirmed" model="mail.message.subtype">
<field name="name">To Approve</field>
<field name="res_model">hr.holidays</field>
<field name="description">Request created and waiting confirmation</field>
<field name="description">Request confirmed and waiting approval</field>
</record>
<record id="mt_holidays_approved" model="mail.message.subtype">
<field name="name">Approved</field>
+11 -5
View File
@@ -53,11 +53,14 @@
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="Leave Request" version="7.0">
<field name="can_reset" invisible="1"/>
<header>
<button string="Confirm" name="confirm" states="draft" type="workflow" class="oe_highlight"/>
<button string="Approve" name="validate" states="confirm" type="workflow" groups="base.group_hr_user" class="oe_highlight"/>
<button string="Validate" name="second_validate" states="validate1" type="workflow" groups="base.group_hr_user" class="oe_highlight"/>
<button string="Refuse" name="refuse" states="confirm,validate1,validate" type="workflow" groups="base.group_hr_user"/>
<button string="Reset to New" name="set_to_draft" states="refuse" type="object" groups="base.group_hr_user"/>
<button string="Reset to Draft" name="reset" type="workflow"
attrs="{'invisible': ['|', ('can_reset', '=', False), ('state', 'not in', ['confirm', 'refuse'])]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,validate" statusbar_colors='{"confirm":"blue","validate1":"blue","refuse":"red"}'/>
</header>
<sheet string="Leave Request">
@@ -98,11 +101,14 @@
<field name="model">hr.holidays</field>
<field name="arch" type="xml">
<form string="Allocation Request" version="7.0">
<field name="can_reset" invisible="1"/>
<header>
<button string="Confirm" name="confirm" states="draft" type="workflow" class="oe_highlight"/>
<button string="Approve" name="validate" states="confirm" type="workflow" groups="base.group_hr_user" class="oe_highlight"/>
<button string="Validate" name="second_validate" states="validate1" type="workflow" groups="base.group_hr_user" class="oe_highlight"/>
<button string="Refuse" name="refuse" states="confirm,validate,validate1" type="workflow" groups="base.group_hr_user"/>
<button string="Reset to New" name="set_to_draft" states="cancel,refuse" type="object" groups="base.group_hr_user"/>
<button string="Reset to Draft" name="reset" type="workflow"
attrs="{'invisible': ['|', ('can_reset', '=', False), ('state', 'not in', ['confirm', 'refuse'])]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,validate" statusbar_colors='{"confirm":"blue","validate1":"blue","refuse":"red"}'/>
</header>
<sheet>
@@ -163,7 +169,7 @@
<button string="Submit to Manager" name="confirm" states="draft" type="workflow" icon="gtk-yes"/>
<button string="Approve" name="validate" states="confirm" type="workflow" icon="gtk-apply"/>
<button string="Refuse" name="refuse" states="confirm,validate,draft" type="workflow" icon="gtk-no"/>
<button string="Reset to New" name="set_to_draft" states="cancel" type="object" icon="gtk-convert"/>
<button string="Reset to Draft" name="reset" states="confirm" type="workflow" groups="base.group_hr_manager"/>
<field name="state"/>
</header>
<group col="4">
@@ -351,8 +357,8 @@
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" eval="view_holiday_simple"/>
<field name="context">{'search_default_group_type': 1, 'search_default_validated': 1}</field>
<field name="domain">[('holiday_type','=','employee')]</field>
<field name="context">{'search_default_group_type': 1}</field>
<field name="domain">[('holiday_type','=','employee'), ('state', '!=', 'refuse')]</field>
<field name="search_view_id" ref="view_hr_holidays_filter"/>
</record>
+29 -11
View File
@@ -2,15 +2,16 @@
<openerp>
<data>
<!-- Workflow definition
1. draft->submitted (no signal)
<!-- Workflow definition
1. draft->submitted (confirm signal) if can_reset
2. submitted->draft (reset signal) if can_reset
2. submitted->accepted (validate signal) if not double_validation
2. submitted -> first_accepted (validate signal) if double_validation
2. submitted->first_accepted (validate signal) if double_validation
2. submitted->refused (refuse signal)
3. accepted->refused (refuse signal)
4. first_accepted -> accepted (second_validate signal)
4. first_accepted -> accepted (second_validate signal)
4. first_accepted -> refused (refuse signal)
5. refuse -> draft (reset signal) if can_reset
-->
<record model="workflow" id="wkf_holidays">
@@ -21,13 +22,15 @@
<record model="workflow.activity" id="act_draft"> <!-- draft -->
<field name="wkf_id" ref="wkf_holidays" />
<field name="flow_start">True</field>
<field name="name">draft</field>
<field name="kind">function</field>
<field name="action">holidays_reset()</field>
</record>
<record model="workflow.activity" id="act_confirm"> <!-- submitted -->
<field name="wkf_id" ref="wkf_holidays" />
<field name="name">confirm</field>
<field name="flow_start">True</field>
<field name="kind">function</field>
<field name="action">holidays_confirm()</field>
<field name="split_mode">OR</field>
@@ -48,11 +51,9 @@
<field name="split_mode">OR</field>
</record>
<record model="workflow.activity" id="act_refuse"> <!-- refused -->
<field name="wkf_id" ref="wkf_holidays" />
<field name="name">refuse</field>
<field name="flow_stop">True</field>
<field name="kind">function</field>
<field name="action">holidays_refuse()</field>
</record>
@@ -61,9 +62,20 @@
workflow transition
-->
<record model="workflow.transition" id="holiday_draft2confirm"> <!-- 1. draft->submitted (no signal) -->
<record model="workflow.transition" id="holiday_draft2confirm"> <!-- 1. draft->submitted (confirm signal) -->
<field name="act_from" ref="act_draft" />
<field name="act_to" ref="act_confirm" />
<field name="signal">confirm</field>
<field name="condition">can_reset</field>
<field name="group_id" ref="base.group_user"/>
</record>
<record model="workflow.transition" id="holiday_confirm2draft"> <!-- 2. submitted->draft (reset signal) -->
<field name="act_from" ref="act_confirm" />
<field name="act_to" ref="act_draft" />
<field name="signal">reset</field>
<field name="condition">can_reset</field>
<field name="group_id" ref="base.group_user"/>
</record>
<record model="workflow.transition" id="holiday_confirm2validate"> <!-- 2. submitted->accepted (validate signal) if not double_validation-->
@@ -82,7 +94,6 @@
<field name="group_id" ref="base.group_hr_user"/>
</record>
<record model="workflow.transition" id="holiday_confirm2refuse"> <!-- 2. submitted->refused (refuse signal) -->
<field name="act_from" ref="act_confirm" />
<field name="act_to" ref="act_refuse" />
@@ -107,7 +118,6 @@
<field name="group_id" ref="base.group_hr_user"/>
</record>
<record model="workflow.transition" id="holiday_validate1_validate"> <!-- 4. first_accepted -> accepted (second_validate signal) -->
<field name="act_from" ref="act_validate1" />
<field name="act_to" ref="act_validate" />
@@ -124,5 +134,13 @@
<field name="group_id" ref="base.group_hr_user"/>
</record>
<record model="workflow.transition" id="holiday_refuse2draft"> <!-- 5. refused->draft (reset signal) -->
<field name="act_from" ref="act_refuse" />
<field name="act_to" ref="act_draft" />
<field name="signal">reset</field>
<field name="condition">can_reset</field>
<field name="group_id" ref="base.group_hr_manager"/>
</record>
</data>
</openerp>
@@ -1,8 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_holydays_status_user,hr.holidays.status user,model_hr_holidays_status,base.group_hr_user,1,1,1,1
access_hr_holidays_user,hr.holidays.user,model_hr_holidays,base.group_hr_user,1,1,1,1
access_hr_holidays_employee,hr.holidays.employee,model_hr_holidays,base.group_user,1,1,1,1
access_hr_holydays_status_employee,hr.holidays.status employee,model_hr_holidays_status,base.group_user,1,0,0,0
access_hr_holydays_status_manager,hr.holidays.status manager,model_hr_holidays_status,base.group_hr_manager,1,1,1,1
access_hr_holidays_remain_user,hr.holidays.ramain.user,model_hr_holidays_remaining_leaves_user,base.group_hr_user,1,1,1,1
access_resource_calendar_leaves_user,resource_calendar_leaves_user,resource.model_resource_calendar_leaves,base.group_hr_user,1,1,1,1
access_crm_meeting_hr_user,crm.meeting.hr.user,base_calendar.model_crm_meeting,base.group_hr_user,1,1,1,1
access_crm_meeting_type_manager,crm.meeting.type.manager,base_calendar.model_crm_meeting_type,base.group_hr_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
access_hr_holydays_status_user hr.holidays.status user model_hr_holidays_status base.group_hr_user 1 1 1 1
2 access_hr_holidays_user hr.holidays.user model_hr_holidays base.group_hr_user 1 1 1 1
3 access_hr_holidays_employee hr.holidays.employee model_hr_holidays base.group_user 1 1 1 1
4 access_hr_holydays_status_employee hr.holidays.status employee model_hr_holidays_status base.group_user 1 0 0 0
5 access_hr_holydays_status_manager hr.holidays.status manager model_hr_holidays_status base.group_hr_manager 1 1 1 1
6 access_hr_holidays_remain_user hr.holidays.ramain.user model_hr_holidays_remaining_leaves_user base.group_hr_user 1 1 1 1
7 access_resource_calendar_leaves_user resource_calendar_leaves_user resource.model_resource_calendar_leaves base.group_hr_user 1 1 1 1
8 access_crm_meeting_hr_user crm.meeting.hr.user base_calendar.model_crm_meeting base.group_hr_user 1 1 1 1
9 access_crm_meeting_type_manager crm.meeting.type.manager base_calendar.model_crm_meeting_type base.group_hr_manager 1 1 1 1
+1 -1
View File
@@ -18,7 +18,7 @@
I again set to draft and then confirm.
-
!python {model: hr.holidays}: |
self.set_to_draft(cr, uid, [ref('hr_holidays_employee1_cl')])
self.holidays_reset(cr, uid, [ref('hr_holidays_employee1_cl')])
self.signal_confirm(cr, uid, [ref('hr_holidays_employee1_cl')])
-
I validate the holiday request by clicking on "To Approve" button.
+28
View File
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2013-TODAY OpenERP S.A. <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.hr_holidays.tests import test_holidays_flow
checks = [
test_holidays_flow,
]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+98
View File
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2013-TODAY OpenERP S.A. <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.tests import common
class TestHrHolidaysBase(common.TransactionCase):
def setUp(self):
super(TestHrHolidaysBase, self).setUp()
cr, uid = self.cr, self.uid
# Usefull models
self.hr_employee = self.registry('hr.employee')
self.hr_holidays = self.registry('hr.holidays')
self.hr_holidays_status = self.registry('hr.holidays.status')
self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner')
# Find Employee group
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
# Find Hr User group
group_hr_user_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_hr_user')
self.group_hr_user_ref_id = group_hr_user_ref and group_hr_user_ref[1] or False
# Find Hr Manager group
group_hr_manager_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_hr_manager')
self.group_hr_manager_ref_id = group_hr_manager_ref and group_hr_manager_ref[1] or False
# Test partners to use through the various tests
self.hr_partner_id = self.res_partner.create(cr, uid, {
'name': 'Gertrude AgrolaitPartner',
'email': 'gertrude.partner@agrolait.com',
})
self.email_partner_id = self.res_partner.create(cr, uid, {
'name': 'Patrick Ratatouille',
'email': 'patrick.ratatouille@agrolait.com',
})
# Test users to use through the various tests
self.user_hruser_id = self.res_users.create(cr, uid, {
'name': 'Armande HrUser',
'login': 'Armande',
'alias_name': 'armande',
'email': 'armande.hruser@example.com',
'groups_id': [(6, 0, [self.group_employee_id, self.group_hr_user_ref_id])]
}, {'no_reset_password': True})
self.user_hrmanager_id = self.res_users.create(cr, uid, {
'name': 'Bastien HrManager',
'login': 'bastien',
'alias_name': 'bastien',
'email': 'bastien.hrmanager@example.com',
'groups_id': [(6, 0, [self.group_employee_id, self.group_hr_manager_ref_id])]
}, {'no_reset_password': True})
self.user_none_id = self.res_users.create(cr, uid, {
'name': 'Charlie Avotbonkeur',
'login': 'charlie',
'alias_name': 'charlie',
'email': 'charlie.noone@example.com',
'groups_id': [(6, 0, [])]
}, {'no_reset_password': True})
self.user_employee_id = self.res_users.create(cr, uid, {
'name': 'David Employee',
'login': 'david',
'alias_name': 'david',
'email': 'david.employee@example.com',
'groups_id': [(6, 0, [self.group_employee_id])]
}, {'no_reset_password': True})
# Hr Data
self.employee_emp_id = self.hr_employee.create(cr, uid, {
'name': 'David Employee',
'user_id': self.user_employee_id,
})
self.employee_hruser_id = self.hr_employee.create(cr, uid, {
'name': 'Armande HrUser',
'user_id': self.user_hruser_id,
})
@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2013-TODAY OpenERP S.A. <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.relativedelta import relativedelta
from openerp.addons.hr_holidays.tests.common import TestHrHolidaysBase
from openerp.osv.orm import except_orm
from openerp.tools import mute_logger
class TestHolidaysFlow(TestHrHolidaysBase):
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
def test_00_leave_request_flow(self):
""" Testing leave request flow """
cr, uid = self.cr, self.uid
def _check_holidays_status(holiday_status, ml, lt, rl, vrl):
self.assertEqual(holiday_status.max_leaves, ml,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.leaves_taken, lt,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.remaining_leaves, rl,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.virtual_remaining_leaves, vrl,
'hr_holidays: wrong type days computation')
# HrUser creates some holiday statuses -> crash because only HrManagers should do this
with self.assertRaises(except_orm):
self.holidays_status_dummy = self.hr_holidays_status.create(cr, self.user_hruser_id, {
'name': 'UserCheats',
'limit': True,
})
# HrManager creates some holiday statuses
self.holidays_status_0 = self.hr_holidays_status.create(cr, self.user_hrmanager_id, {
'name': 'WithMeetingType',
'limit': True,
'categ_id': self.registry('crm.meeting.type').create(cr, self.user_hrmanager_id, {'name': 'NotLimitedMeetingType'}),
})
self.holidays_status_1 = self.hr_holidays_status.create(cr, self.user_hrmanager_id, {
'name': 'NotLimited',
'limit': True,
})
self.holidays_status_2 = self.hr_holidays_status.create(cr, self.user_hrmanager_id, {
'name': 'Limited',
'limit': False,
'double_validation': True,
})
# --------------------------------------------------
# Case1: unlimited type of leave request
# --------------------------------------------------
# Employee creates a leave request for another employee -> should crash
with self.assertRaises(except_orm):
self.hr_holidays.create(cr, self.user_employee_id, {
'name': 'Hol10',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.holidays_status_1,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days_temp': 1,
})
# Employee creates a leave request in a no-limit category
hol1_id = self.hr_holidays.create(cr, self.user_employee_id, {
'name': 'Hol11',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_status_1,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days_temp': 1,
})
hol1 = self.hr_holidays.browse(cr, self.user_hruser_id, hol1_id)
self.assertEqual(hol1.state, 'confirm', 'hr_holidays: newly created leave request should be in confirm state')
# Employee validates its leave request -> should not work
self.hr_holidays.signal_validate(cr, self.user_employee_id, [hol1_id])
hol1.refresh()
self.assertEqual(hol1.state, 'confirm', 'hr_holidays: employee should not be able to validate its own leave request')
# HrUser validates the employee leave request
self.hr_holidays.signal_validate(cr, self.user_hrmanager_id, [hol1_id])
hol1.refresh()
self.assertEqual(hol1.state, 'validate', 'hr_holidays: validates leave request should be in validate state')
# --------------------------------------------------
# Case2: limited type of leave request
# --------------------------------------------------
# Employee creates a new leave request at the same time -> crash, avoid interlapping
with self.assertRaises(except_orm):
self.hr_holidays.create(cr, self.user_employee_id, {
'name': 'Hol21',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_status_1,
'date_from': (datetime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M'),
'date_to': datetime.today(),
'number_of_days_temp': 1,
})
# Employee creates a leave request in a limited category -> crash, not enough days left
with self.assertRaises(except_orm):
self.hr_holidays.create(cr, self.user_employee_id, {
'name': 'Hol22',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_status_2,
'date_from': (datetime.today() + relativedelta(days=0)).strftime('%Y-%m-%d %H:%M'),
'date_to': (datetime.today() + relativedelta(days=1)),
'number_of_days_temp': 1,
})
# Clean transaction
self.hr_holidays.unlink(cr, uid, self.hr_holidays.search(cr, uid, [('name', 'in', ['Hol21', 'Hol22'])]))
# HrUser allocates some leaves to the employee
aloc1_id = self.hr_holidays.create(cr, self.user_hruser_id, {
'name': 'Days for limited category',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_status_2,
'type': 'add',
'number_of_days_temp': 2,
})
# HrUser validates the allocation request
self.hr_holidays.signal_validate(cr, self.user_hruser_id, [aloc1_id])
self.hr_holidays.signal_second_validate(cr, self.user_hruser_id, [aloc1_id])
# Checks Employee has effectively some days left
hol_status_2 = self.hr_holidays_status.browse(cr, self.user_employee_id, self.holidays_status_2)
_check_holidays_status(hol_status_2, 2.0, 0.0, 2.0, 2.0)
# Employee creates a leave request in the limited category, now that he has some days left
hol2_id = self.hr_holidays.create(cr, self.user_employee_id, {
'name': 'Hol22',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_status_2,
'date_from': (datetime.today() + relativedelta(days=2)).strftime('%Y-%m-%d %H:%M'),
'date_to': (datetime.today() + relativedelta(days=3)),
'number_of_days_temp': 1,
})
hol2 = self.hr_holidays.browse(cr, self.user_hruser_id, hol2_id)
# Check left days: - 1 virtual remaining day
hol_status_2.refresh()
_check_holidays_status(hol_status_2, 2.0, 0.0, 2.0, 1.0)
# HrUser validates the first step
self.hr_holidays.signal_validate(cr, self.user_hruser_id, [hol2_id])
hol2.refresh()
self.assertEqual(hol2.state, 'validate1',
'hr_holidays: first validation should lead to validate1 state')
# HrUser validates the second step
self.hr_holidays.signal_second_validate(cr, self.user_hruser_id, [hol2_id])
hol2.refresh()
self.assertEqual(hol2.state, 'validate',
'hr_holidays: second validation should lead to validate state')
# Check left days: - 1 day taken
hol_status_2.refresh()
_check_holidays_status(hol_status_2, 2.0, 1.0, 1.0, 1.0)
# HrManager finds an error: he refuses the leave request
self.hr_holidays.signal_refuse(cr, self.user_hrmanager_id, [hol2_id])
hol2.refresh()
self.assertEqual(hol2.state, 'refuse',
'hr_holidays: refuse should lead to refuse state')
# Check left days: 2 days left again
hol_status_2.refresh()
_check_holidays_status(hol_status_2, 2.0, 0.0, 2.0, 2.0)
# Annoyed, HrUser tries to fix its error and tries to reset the leave request -> does not work, only HrManager
self.hr_holidays.signal_reset(cr, self.user_hruser_id, [hol2_id])
self.assertEqual(hol2.state, 'refuse',
'hr_holidays: hr_user should not be able to reset a refused leave request')
# HrManager resets the request
self.hr_holidays.signal_reset(cr, self.user_hrmanager_id, [hol2_id])
hol2.refresh()
self.assertEqual(hol2.state, 'draft',
'hr_holidays: resetting should lead to draft state')
# HrManager changes the date and put too much days -> crash when confirming
self.hr_holidays.write(cr, self.user_hrmanager_id, [hol2_id], {
'date_from': (datetime.today() + relativedelta(days=4)).strftime('%Y-%m-%d %H:%M'),
'date_to': (datetime.today() + relativedelta(days=7)),
'number_of_days_temp': 4,
})
with self.assertRaises(except_orm):
self.hr_holidays.signal_confirm(cr, self.user_hrmanager_id, [hol2_id])
+169
View File
@@ -0,0 +1,169 @@
# Chinese (Simplified) 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-02-08 01:06+0000\n"
"PO-Revision-Date: 2013-07-24 12:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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-07-25 05:13+0000\n"
"X-Generator: Launchpad (build 16700)\n"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_fiscal_position_template
msgid "Template for Fiscal Position"
msgstr "财务结构模板"
#. module: l10n_multilang
#: sql_constraint:account.account:0
msgid "The code of the account must be unique per company !"
msgstr "该科目的代码,每家公司必须是唯一的!"
#. module: l10n_multilang
#: constraint:account.account.template:0
msgid ""
"Configuration Error!\n"
"You can not define children to an account with internal type different of "
"\"View\"! "
msgstr ""
"配置错误! \n"
"上级科目必须是“视图”类型的科目! "
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_analytic_journal
msgid "Analytic Journal"
msgstr "辅助核算类型"
#. module: l10n_multilang
#: constraint:account.account.template:0
msgid "Error ! You can not create recursive account templates."
msgstr "错误!您不能创建递归的科目模板。"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_journal
msgid "Journal"
msgstr "分录序时薄"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_chart_template
msgid "Templates for Account Chart"
msgstr "科目一览表模板"
#. module: l10n_multilang
#: sql_constraint:account.tax:0
msgid "The description must be unique per company!"
msgstr "此说明必须是每个公司唯一的"
#. module: l10n_multilang
#: constraint:account.tax.code.template:0
msgid "Error ! You can not create recursive Tax Codes."
msgstr "错误!您不能创建递归的税编码。"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_tax_template
msgid "account.tax.template"
msgstr "account.tax.template"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_tax
msgid "account.tax"
msgstr "account.tax"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_account
msgid "Account"
msgstr "科目"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_wizard_multi_charts_accounts
msgid "wizard.multi.charts.accounts"
msgstr "wizard.multi.charts.accounts"
#. module: l10n_multilang
#: constraint:account.journal:0
msgid ""
"Configuration error! The currency chosen should be shared by the default "
"accounts too."
msgstr "设置错误!所选币种应与默认科目共享。"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_account_template
msgid "Templates for Accounts"
msgstr "科目模板"
#. module: l10n_multilang
#: help:account.chart.template,spoken_languages:0
msgid ""
"State here the languages for which the translations of templates could be "
"loaded at the time of installation of this localization module and copied in "
"the final object when generating them from templates. You must provide the "
"language codes separated by ';'"
msgstr ""
#. module: l10n_multilang
#: constraint:account.account:0
msgid "Error ! You can not create recursive accounts."
msgstr "错误!您不能创建递归的科目"
#. module: l10n_multilang
#: constraint:account.account:0
msgid ""
"Configuration Error! \n"
"You can not select an account type with a deferral method different of "
"\"Unreconciled\" for accounts with internal type \"Payable/Receivable\"! "
msgstr ""
"配置错误!\n"
"对于内部类型是‘应收/应付’的会计科目,你需要选择结转方式是‘未核销’的科目类型。 "
#. module: l10n_multilang
#: sql_constraint:account.journal:0
msgid "The name of the journal must be unique per company !"
msgstr "每个公司的日记账名称必须唯一!"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_analytic_account
msgid "Analytic Account"
msgstr "辅助核算项目"
#. module: l10n_multilang
#: sql_constraint:account.journal:0
msgid "The code of the journal must be unique per company !"
msgstr "每个公司的日记账代码必须唯一!"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_fiscal_position
msgid "Fiscal Position"
msgstr "财务结构"
#. module: l10n_multilang
#: constraint:account.account:0
msgid ""
"Configuration Error! \n"
"You can not define children to an account with internal type different of "
"\"View\"! "
msgstr ""
"配置错误! \n"
"您不能给 非“视图”类型的科目定义一个子科目。 "
#. module: l10n_multilang
#: constraint:account.analytic.account:0
msgid "Error! You can not create recursive analytic accounts."
msgstr "错误! 你不能创建递归的辅助核算项目"
#. module: l10n_multilang
#: model:ir.model,name:l10n_multilang.model_account_tax_code_template
msgid "Tax Code Template"
msgstr "税编码模板"
#. module: l10n_multilang
#: field:account.chart.template,spoken_languages:0
msgid "Spoken Languages"
msgstr "交流语言"
-1
View File
@@ -83,7 +83,6 @@ Main Features
'static/src/css/mail_group.css',
],
'js': [
'static/lib/jquery.expander/jquery.expander.js',
'static/src/js/mail.js',
'static/src/js/mail_followers.js',
'static/src/js/many2many_tags_email.js',
+7 -1
View File
@@ -6,6 +6,12 @@ Changelog
`trunk (saas-2)`
----------------
- 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``.
- added ``mail_server_id`` to mail_message, removing it from mail_mail. This allows
to set the preferred mail server to use for notifications emails, when using
templates.
- added ``_mail_post_access`` attribute that specifies the access right that
should have the user in order to post a new message on a given model. Values
are ``read`` (portal documents), ``write`` (default value), ``unlink`` or ``create``.
are ``read`` (portal documents), ``write`` (default value), ``unlink`` or ``create``.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -151,7 +151,7 @@ class mail_notification(osv.Model):
return footer
def _notify(self, cr, uid, msg_id, partners_to_notify=None, context=None,
force_send=False, user_signature=True):
force_send=False, user_signature=True):
""" Send by email the notification depending on the user preferences
:param list partners_to_notify: optional list of partner ids restricting
+8 -3
View File
@@ -44,7 +44,6 @@ class mail_mail(osv.Model):
_columns = {
'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade'),
'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
'state': fields.selection([
('outgoing', 'Outgoing'),
('sent', 'Sent'),
@@ -140,10 +139,16 @@ class mail_mail(osv.Model):
# 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'):
values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
return super(mail_mail, self).create(cr, uid, values, context=context)
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
def unlink(self, cr, uid, ids, context=None):
# cascade-delete the parent message for all mails that are not created for a notification
+12 -5
View File
@@ -191,6 +191,7 @@ class mail_message(osv.Model):
'vote_user_ids': fields.many2many('res.users', 'mail_vote',
'message_id', 'user_id', string='Votes',
help='Users that voted for this message'),
'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
}
def _needaction_domain_get(self, cr, uid, context=None):
@@ -394,15 +395,21 @@ class mail_message(osv.Model):
has_voted = uid in [user.id for user in message.vote_user_ids]
try:
body_html = html_email_clean(message.body)
if parent_id:
max_length = 300
else:
max_length = 100
body_short = html_email_clean(message.body, remove=False, shorten=True, max_length=max_length)
except Exception:
body_html = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % message.id
body_short = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % message.id
_logger.exception(Exception)
return {'id': message.id,
'type': message.type,
'subtype': message.subtype_id.name if message.subtype_id else False,
'body': body_html,
'body': message.body,
'body_short': body_short,
'model': message.model,
'res_id': message.res_id,
'record_name': message.record_name,
@@ -780,8 +787,8 @@ class mail_message(osv.Model):
values['message_id'] = tools.generate_tracking_message_id('private')
newid = super(mail_message, self).create(cr, uid, values, context)
self._notify(cr, uid, newid, context=context,
force_send=context.get('mail_notify_force_send', True),
user_signature=context.get('mail_notify_user_signature', True))
force_send=context.get('mail_notify_force_send', True),
user_signature=context.get('mail_notify_user_signature', True))
# TDE FIXME: handle default_starred. Why not setting an inv on starred ?
# Because starred will call set_message_starred, that looks for notifications.
# When creating a new mail_message, it will create a notification to a message
+13 -7
View File
@@ -682,6 +682,12 @@ class mail_thread(osv.AbstractModel):
assert thread_id == 0, 'Routing: posting a message without model should be with a null res_id (private message).'
_warn('posting a message without model should be with a null res_id (private message), resetting thread_id')
thread_id = 0
# Private message: should have a parent_id (only answers)
if not model and not message_dict.get('parent_id'):
if assert_model:
assert message_dict.get('parent_id'), 'Routing: posting a message without model should be with a parent_id (private mesage).'
_warn('posting a message without model should be with a parent_id (private mesage), skipping')
return ()
# Existing Document: check if exists; if not, fallback on create if allowed
if thread_id and not model_pool.exists(cr, uid, thread_id):
@@ -695,7 +701,7 @@ class mail_thread(osv.AbstractModel):
return ()
# Existing Document: check model accepts the mailgateway
if thread_id and not hasattr(model_pool, 'message_update'):
if thread_id and model and not hasattr(model_pool, 'message_update'):
if create_fallback:
_warn('model %s does not accept document update, fall back on document creation' % model)
thread_id = None
@@ -706,7 +712,7 @@ class mail_thread(osv.AbstractModel):
return ()
# New Document: check model accepts the mailgateway
if not thread_id and not hasattr(model_pool, 'message_new'):
if not thread_id and model and not hasattr(model_pool, 'message_new'):
if assert_model:
assert hasattr(model_pool, 'message_new'), 'Model %s does not accept document creation, crashing' % model
_warn('model %s does not accept document creation, skipping' % model)
@@ -798,16 +804,16 @@ class mail_thread(osv.AbstractModel):
# 2. Reply to a private message
if in_reply_to:
message_ids = self.pool.get('mail.message').search(cr, uid, [
mail_message_ids = self.pool.get('mail.message').search(cr, uid, [
('message_id', '=', in_reply_to),
'!', ('message_id', 'ilike', 'reply_to')
], limit=1, context=context)
if message_ids:
message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
if mail_message_ids:
mail_message = self.pool.get('mail.message').browse(cr, uid, mail_message_ids[0], context=context)
_logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, message.id, custom_values, uid)
email_from, email_to, message_id, mail_message.id, custom_values, uid)
route = self.message_route_verify(cr, uid, message, message_dict,
(message.model, message.res_id, custom_values, uid, None),
(mail_message.model, mail_message.res_id, custom_values, uid, None),
update_author=True, assert_model=True, create_fallback=True, context=context)
return route and [route] or []
@@ -1,385 +0,0 @@
/*!
* jQuery Expander Plugin v1.4.2
*
* Date: Fri Mar 16 14:29:56 2012 EDT
* Requires: jQuery v1.3+
*
* Copyright 2011, Karl Swedberg
* Dual licensed under the MIT and GPL licenses (just like jQuery):
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*
*
*
*/
(function($) {
$.expander = {
version: '1.4.2',
defaults: {
// the number of characters at which the contents will be sliced into two parts.
slicePoint: 100,
// whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false)
preserveWords: true,
// a threshold of sorts for whether to initially hide/collapse part of the element's contents.
// If after slicing the contents in two there are fewer words in the second part than
// the value set by widow, we won't bother hiding/collapsing anything.
widow: 4,
// text displayed in a link instead of the hidden part of the element.
// clicking this will expand/show the hidden/collapsed text
expandText: 'read more',
expandPrefix: '&hellip; ',
expandAfterSummary: false,
// class names for summary element and detail element
summaryClass: 'summary',
detailClass: 'details',
// class names for <span> around "read-more" link and "read-less" link
moreClass: 'read-more',
lessClass: 'read-less',
// number of milliseconds after text has been expanded at which to collapse the text again.
// when 0, no auto-collapsing
collapseTimer: 0,
// effects for expanding and collapsing
expandEffect: 'fadeIn',
expandSpeed: 250,
collapseEffect: 'fadeOut',
collapseSpeed: 200,
// allow the user to re-collapse the expanded text.
userCollapse: true,
// text to use for the link to re-collapse the text
userCollapseText: 'read less',
userCollapsePrefix: ' ',
// all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called
onSlice: null, // function() {}
beforeExpand: null, // function() {},
afterExpand: null, // function() {},
onCollapse: null // function(byUser) {}
}
};
$.fn.expander = function(options) {
var meth = 'init';
if (typeof options == 'string') {
meth = options;
options = {};
}
var opts = $.extend({}, $.expander.defaults, options),
rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i,
rAmpWordEnd = opts.wordEnd || /(&(?:[^;]+;)?|[a-zA-Z\u00C0-\u0100]+)$/,
rOpenCloseTag = /<\/?(\w+)[^>]*>/g,
rOpenTag = /<(\w+)[^>]*>/g,
rCloseTag = /<\/(\w+)>/g,
rLastCloseTag = /(<\/[^>]+>)\s*$/,
rTagPlus = /^<[^>]+>.?/,
delayedCollapse;
var methods = {
init: function() {
this.each(function() {
var i, l, tmp, newChar, summTagless, summOpens, summCloses,
lastCloseTag, detailText, detailTagless,
$thisDetails, $readMore,
openTagsForDetails = [],
closeTagsForsummaryText = [],
defined = {},
thisEl = this,
$this = $(this),
$summEl = $([]),
o = $.meta ? $.extend({}, opts, $this.data()) : opts,
hasDetails = !!$this.find('.' + o.detailClass).length,
hasBlocks = !!$this.find('*').filter(function() {
var display = $(this).css('display');
return (/^block|table|list/).test(display);
}).length,
el = hasBlocks ? 'div' : 'span',
detailSelector = el + '.' + o.detailClass,
moreSelector = 'span.' + o.moreClass,
expandSpeed = o.expandSpeed || 0,
allHtml = $.trim( $this.html() ),
allText = $.trim( $this.text() ),
summaryText = allHtml.slice(0, o.slicePoint);
// bail out if we've already set up the expander on this element
if ( $.data(this, 'expander') ) {
return;
}
$.data(this, 'expander', true);
// determine which callback functions are defined
$.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) {
defined[val] = $.isFunction(o[val]);
});
// back up if we're in the middle of a tag or word
summaryText = backup(summaryText);
// summary text sans tags length
summTagless = summaryText.replace(rOpenCloseTag, '').length;
// add more characters to the summary, one for each character in the tags
while (summTagless < o.slicePoint) {
newChar = allHtml.charAt(summaryText.length);
if (newChar == '<') {
newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0];
}
summaryText += newChar;
summTagless++;
}
summaryText = backup(summaryText, o.preserveWords);
// separate open tags from close tags and clean up the lists
summOpens = summaryText.match(rOpenTag) || [];
summCloses = summaryText.match(rCloseTag) || [];
// filter out self-closing tags
tmp = [];
$.each(summOpens, function(index, val) {
if ( !rSelfClose.test(val) ) {
tmp.push(val);
}
});
summOpens = tmp;
// strip close tags to just the tag name
l = summCloses.length;
for (i = 0; i < l; i++) {
summCloses[i] = summCloses[i].replace(rCloseTag, '$1');
}
// tags that start in summary and end in detail need:
// a). close tag at end of summary
// b). open tag at beginning of detail
$.each(summOpens, function(index, val) {
var thisTagName = val.replace(rOpenTag, '$1');
var closePosition = $.inArray(thisTagName, summCloses);
if (closePosition === -1) {
openTagsForDetails.push(val);
closeTagsForsummaryText.push('</' + thisTagName + '>');
} else {
summCloses.splice(closePosition, 1);
}
});
// reverse the order of the close tags for the summary so they line up right
closeTagsForsummaryText.reverse();
// create necessary summary and detail elements if they don't already exist
if ( !hasDetails ) {
// end script if there is no detail text or if detail has fewer words than widow option
detailText = allHtml.slice(summaryText.length);
detailTagless = $.trim( detailText.replace(rOpenCloseTag, '') );
if ( detailTagless === '' || detailTagless.split(/\s+/).length < o.widow ) {
return;
}
// otherwise, continue...
lastCloseTag = closeTagsForsummaryText.pop() || '';
summaryText += closeTagsForsummaryText.join('');
detailText = openTagsForDetails.join('') + detailText;
} else {
// assume that even if there are details, we still need readMore/readLess/summary elements
// (we already bailed out earlier when readMore el was found)
// but we need to create els differently
// remove the detail from the rest of the content
detailText = $this.find(detailSelector).remove().html();
// The summary is what's left
summaryText = $this.html();
// allHtml is the summary and detail combined (this is needed when content has block-level elements)
allHtml = summaryText + detailText;
lastCloseTag = '';
}
o.moreLabel = $this.find(moreSelector).length ? '' : buildMoreLabel(o);
if (hasBlocks) {
detailText = allHtml;
}
summaryText += lastCloseTag;
// onSlice callback
o.summary = summaryText;
o.details = detailText;
o.lastCloseTag = lastCloseTag;
if (defined.onSlice) {
// user can choose to return a modified options object
// one last chance for user to change the options. sneaky, huh?
// but could be tricky so use at your own risk.
tmp = o.onSlice.call(thisEl, o);
// so, if the returned value from the onSlice function is an object with a details property, we'll use that!
o = tmp && tmp.details ? tmp : o;
}
// build the html with summary and detail and use it to replace old contents
var html = buildHTML(o, hasBlocks);
$this.html( html );
// set up details and summary for expanding/collapsing
$thisDetails = $this.find(detailSelector);
$readMore = $this.find(moreSelector);
$thisDetails.hide();
$readMore.find('a').unbind('click.expander').bind('click.expander', expand);
$summEl = $this.find('div.' + o.summaryClass);
if ( o.userCollapse && !$this.find('span.' + o.lessClass).length ) {
$this
.find(detailSelector)
.append('<span class="' + o.lessClass + '">' + o.userCollapsePrefix + '<a href="#">' + o.userCollapseText + '</a></span>');
}
$this
.find('span.' + o.lessClass + ' a')
.unbind('click.expander')
.bind('click.expander', function(event) {
event.preventDefault();
clearTimeout(delayedCollapse);
var $detailsCollapsed = $(this).closest(detailSelector);
reCollapse(o, $detailsCollapsed);
if (defined.onCollapse) {
o.onCollapse.call(thisEl, true);
}
});
function expand(event) {
event.preventDefault();
$readMore.hide();
$summEl.hide();
if (defined.beforeExpand) {
o.beforeExpand.call(thisEl);
}
$thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function() {
$thisDetails.css({zoom: ''});
if (defined.afterExpand) {o.afterExpand.call(thisEl);}
delayCollapse(o, $thisDetails, thisEl);
});
}
}); // this.each
},
destroy: function() {
if ( !this.data('expander') ) {
return;
}
this.removeData('expander');
this.each(function() {
var $this = $(this),
o = $.meta ? $.extend({}, opts, $this.data()) : opts,
details = $this.find('.' + o.detailClass).contents();
$this.find('.' + o.moreClass).remove();
$this.find('.' + o.summaryClass).remove();
$this.find('.' + o.detailClass).after(details).remove();
$this.find('.' + o.lessClass).remove();
});
}
};
// run the methods (almost always "init")
if ( methods[meth] ) {
methods[ meth ].call(this);
}
// utility functions
function buildHTML(o, blocks) {
var el = 'span',
summary = o.summary;
if ( blocks ) {
el = 'div';
// if summary ends with a close tag, tuck the moreLabel inside it
if ( rLastCloseTag.test(summary) && !o.expandAfterSummary) {
summary = summary.replace(rLastCloseTag, o.moreLabel + '$1');
} else {
// otherwise (e.g. if ends with self-closing tag) just add moreLabel after summary
// fixes #19
summary += o.moreLabel;
}
// and wrap it in a div
summary = '<div class="' + o.summaryClass + '">' + summary + '</div>';
} else {
summary += o.moreLabel;
}
return [
summary,
'<',
el + ' class="' + o.detailClass + '"',
'>',
o.details,
'</' + el + '>'
].join('');
}
function buildMoreLabel(o) {
var ret = '<span class="' + o.moreClass + '">' + o.expandPrefix;
ret += '<a href="#">' + o.expandText + '</a></span>';
return ret;
}
function backup(txt, preserveWords) {
if ( txt.lastIndexOf('<') > txt.lastIndexOf('>') ) {
txt = txt.slice( 0, txt.lastIndexOf('<') );
}
if (preserveWords) {
txt = txt.replace(rAmpWordEnd,'');
}
return $.trim(txt);
}
function reCollapse(o, el) {
el.stop(true, true)[o.collapseEffect](o.collapseSpeed, function() {
var prevMore = el.prev('span.' + o.moreClass).show();
if (!prevMore.length) {
el.parent().children('div.' + o.summaryClass).show()
.find('span.' + o.moreClass).show();
}
});
}
function delayCollapse(option, $collapseEl, thisEl) {
if (option.collapseTimer) {
delayedCollapse = setTimeout(function() {
reCollapse(option, $collapseEl);
if ( $.isFunction(option.onCollapse) ) {
option.onCollapse.call(thisEl, false);
}
}, option.collapseTimer);
}
}
return this;
};
// plugin defaults
$.fn.expander.defaults = $.expander.defaults;
})(jQuery);
+3
View File
@@ -118,6 +118,9 @@
text-overflow:ellipsis;
word-wrap: break-word;
}
.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body .oe_mail_cleaned {
display: none;
}
/* a) Indented Messages */
+9 -11
View File
@@ -2,7 +2,7 @@ openerp.mail = function (session) {
var _t = session.web._t,
_lt = session.web._lt;
var mail = session.mail = {};
var mail = session.mail;
openerp_mail_followers(session, mail); // import mail_followers.js
openerp_FieldMany2ManyTagsEmail(session); // import manyy2many_tags_email.js
@@ -225,6 +225,7 @@ openerp.mail = function (session) {
this.name = datasets.name || false,
this.record_name = datasets.record_name || false,
this.body = datasets.body || '',
this.body_short = datasets.body_short || '',
this.vote_nb = datasets.vote_nb || 0,
this.has_voted = datasets.has_voted || false,
this.is_favorite = datasets.is_favorite || false,
@@ -944,7 +945,6 @@ openerp.mail = function (session) {
start: function () {
this._super.apply(this, arguments);
this.expender();
this.bind_events();
if(this.thread_level < this.options.display_indented_thread) {
this.create_thread();
@@ -967,6 +967,8 @@ openerp.mail = function (session) {
this.$('.oe_reply').on('click', this.on_message_reply);
this.$('.oe_star').on('click', this.on_star);
this.$('.oe_msg_vote').on('click', this.on_vote);
this.$('.oe_mail_expand').on('click', this.on_expand);
this.$('.oe_mail_reduce').on('click', this.on_expand);
this.$('.oe_mail_action_model').on('click', this.on_record_clicked);
},
@@ -995,15 +997,11 @@ openerp.mail = function (session) {
return false;
},
expender: function () {
this.$('.oe_msg_body:first').expander({
slicePoint: this.options.truncate_limit,
expandText: _t('read more'),
userCollapseText: _t('read less'),
detailClass: 'oe_msg_tail',
moreClass: 'oe_mail_expand',
lessClass: 'oe_mail_reduce',
});
on_expand: function (event) {
event.stopPropagation();
this.$('.oe_msg_body_short:first').toggle();
this.$('.oe_msg_body_long:first').toggle();
return false;
},
/**
+7 -1
View File
@@ -266,7 +266,13 @@
<t t-if="widget.subject" t-raw="widget.subject"/>
</h1>
<div class="oe_msg_body">
<t t-raw="widget.body"/>
<t t-if="widget.body_short">
<div class="oe_msg_body_short"><t t-raw="widget.body_short"/></div>
<div class="oe_msg_body_long" style="display: none;"><t t-raw="widget.body"/><span class="oe_mail_reduce"><a href="#">read less</a></span></div>
</t>
<t t-if="! widget.body_short">
<t t-raw="widget.body"/>
</t>
</div>
</div>
<div class="oe_msg_footer">
+32 -1
View File
@@ -640,7 +640,7 @@ class test_mail(TestMailBase):
})
compose = mail_compose.browse(cr, uid, compose_id)
# D: Post the comment, get created message for each group
# Do: Post the comment, get created message for each group
mail_compose.send_mail(cr, user_raoul.id, [compose_id], context={
'default_res_id': -1,
'active_ids': [self.group_pigs_id, group_bird_id]
@@ -679,6 +679,37 @@ class test_mail(TestMailBase):
self.assertEqual(set(bird_pids), set(test_pids),
'compose wizard: mail_post_autofollow and mail_create_nosubscribe context keys not correctly taken into account')
# Do: Compose in mass_mail, coming from list_view, we have an active_domain that should be supported
compose_id = mail_compose.create(cr, user_raoul.id,
{
'subject': _subject,
'body': '${object.description}',
'partner_ids': [(4, p_c_id), (4, p_d_id)],
}, context={
'default_composition_mode': 'mass_mail',
'default_model': 'mail.group',
'default_res_id': False,
'active_ids': [self.group_pigs_id],
'active_domain': [('name', 'in', ['Pigs', 'Bird'])],
})
compose = mail_compose.browse(cr, uid, compose_id)
# Do: Post the comment, get created message for each group
mail_compose.send_mail(
cr, user_raoul.id, [compose_id], context={
'default_res_id': -1,
'active_ids': [self.group_pigs_id, group_bird_id]
})
group_pigs.refresh()
group_bird.refresh()
message1 = group_pigs.message_ids[0]
message2 = group_bird.message_ids[0]
# Test: Pigs and Bird did receive their message
test_msg_ids = self.mail_message.search(cr, uid, [], limit=2)
self.assertIn(message1.id, test_msg_ids, 'compose wizard: Pigs did not receive its mass mailing message')
self.assertIn(message2.id, test_msg_ids, 'compose wizard: Bird did not receive its mass mailing message')
def test_30_needaction(self):
""" Tests for mail.message needaction. """
cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs
+160 -125
View File
@@ -20,7 +20,7 @@
##############################################################################
from openerp.addons.mail.tests.test_mail_base import TestMailBase
from openerp.tools import mute_logger, email_split
from openerp.tools import mute_logger
MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com>
To: {to}
@@ -101,9 +101,9 @@ class TestMailgateway(TestMailBase):
# 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')
'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')
'mail_thread: message_partner_info_from_emails wrong partner found')
# Data: add some data about partners
# 2 - User BRaoul
@@ -112,7 +112,7 @@ class TestMailgateway(TestMailBase):
# 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')
'mail_thread: message_partner_info_from_emails wrong partner found')
# --------------------------------------------------
# CASE1: with object
@@ -122,7 +122,7 @@ class TestMailgateway(TestMailBase):
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')
'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, ... """
@@ -146,46 +146,46 @@ class TestMailgateway(TestMailBase):
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')
'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')
'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')
'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')
'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')
'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')
'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')
'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')
'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')
'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')
'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')
'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')
'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)
@@ -199,7 +199,7 @@ class TestMailgateway(TestMailBase):
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')
'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, {})
@@ -209,7 +209,7 @@ class TestMailgateway(TestMailBase):
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')
'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})
@@ -219,7 +219,7 @@ class TestMailgateway(TestMailBase):
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')
'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')
@@ -232,21 +232,21 @@ class TestMailgateway(TestMailBase):
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')
'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')
'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')
'mail_mail: reply_to should equal the rpely_to given to create')
@mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
def test_10_message_process(self):
@@ -254,9 +254,9 @@ class TestMailgateway(TestMailBase):
cr, uid, user_raoul = self.cr, self.uid, self.user_raoul
def format_and_process(template, to='groups@example.com, other@gmail.com', subject='Frogs',
extra='', email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>',
msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
model=None):
extra='', email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>',
msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
model=None):
self.assertEqual(self.mail_group.search(cr, uid, [('name', '=', subject)]), [])
mail = template.format(to=to, subject=subject, extra=extra, email_from=email_from, msg_id=msg_id)
self.mail_thread.message_process(cr, uid, model, mail)
@@ -289,29 +289,29 @@ class TestMailgateway(TestMailBase):
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
res = self.mail_group.perm_read(cr, uid, [frog_group.id], details=False)
self.assertEqual(res[0].get('create_uid'), uid,
'message_process: group should have been created by uid as alias_user__id is False on the alias')
'message_process: group should have been created by uid as alias_user__id is False on the alias')
# Test: one message that is the incoming email
self.assertEqual(len(frog_group.message_ids), 1,
'message_process: newly created group should have the incoming email in message_ids')
'message_process: newly created group should have the incoming email in message_ids')
msg = frog_group.message_ids[0]
self.assertEqual('Frogs', msg.subject,
'message_process: newly created group should have the incoming email as first message')
'message_process: newly created group should have the incoming email as first message')
self.assertIn('Please call me as soon as possible this afternoon!', msg.body,
'message_process: newly created group should have the incoming email as first message')
'message_process: newly created group should have the incoming email as first message')
self.assertEqual('email', msg.type,
'message_process: newly created group should have an email as first message')
'message_process: newly created group should have an email as first message')
self.assertEqual('Discussions', msg.subtype_id.name,
'message_process: newly created group should not have a log first message but an email')
'message_process: newly created group should not have a log first message but an email')
# Test: message: unknown email address -> message has email_from, not author_id
self.assertFalse(msg.author_id,
'message_process: message on created group should not have an author_id')
'message_process: message on created group should not have an author_id')
self.assertIn('test.sylvie.lelitre@agrolait.com', msg.email_from,
'message_process: message on created group should have an email_from')
'message_process: message on created group should have an email_from')
# Test: followers: nobody
self.assertEqual(len(frog_group.message_follower_ids), 0, 'message_process: newly create group should not have any follower')
# Test: sent emails: no-one
self.assertEqual(len(sent_emails), 0,
'message_process: should create emails without any follower added')
'message_process: should create emails without any follower added')
# Data: unlink group
frog_group.unlink()
@@ -324,11 +324,11 @@ class TestMailgateway(TestMailBase):
# Test: email bounced
sent_emails = self._build_email_kwargs_list
self.assertEqual(len(sent_emails), 1,
'message_process: incoming email on Partners alias should send a bounce email')
'message_process: incoming email on Partners alias should send a bounce email')
self.assertIn('Frogs', sent_emails[0].get('subject'),
'message_process: bounce email on Partners alias should contain the original subject')
'message_process: bounce email on Partners alias should contain the original subject')
self.assertIn('test.sylvie.lelitre@agrolait.com', sent_emails[0].get('email_to'),
'message_process: bounce email on Partners alias should have original email sender as recipient')
'message_process: bounce email on Partners alias should have original email sender as recipient')
# Do: incoming email from an unknown partner on a Followers only alias -> bounce
self._init_mock_build_email()
@@ -339,11 +339,11 @@ class TestMailgateway(TestMailBase):
# Test: email bounced
sent_emails = self._build_email_kwargs_list
self.assertEqual(len(sent_emails), 1,
'message_process: incoming email on Followers alias should send a bounce email')
'message_process: incoming email on Followers alias should send a bounce email')
self.assertIn('Frogs', sent_emails[0].get('subject'),
'message_process: bounce email on Followers alias should contain the original subject')
'message_process: bounce email on Followers alias should contain the original subject')
self.assertIn('test.sylvie.lelitre@agrolait.com', sent_emails[0].get('email_to'),
'message_process: bounce email on Followers alias should have original email sender as recipient')
'message_process: bounce email on Followers alias should have original email sender as recipient')
# Do: incoming email from a known partner on a Partners alias -> ok (+ test on alias.user_id)
self.mail_alias.write(cr, uid, [alias_id], {'alias_user_id': self.user_raoul_id, 'alias_contact': 'partners'})
@@ -357,24 +357,24 @@ class TestMailgateway(TestMailBase):
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
res = self.mail_group.perm_read(cr, uid, [frog_group.id], details=False)
self.assertEqual(res[0].get('create_uid'), self.user_raoul_id,
'message_process: group should have been created by alias_user_id')
'message_process: group should have been created by alias_user_id')
# Test: one message that is the incoming email
self.assertEqual(len(frog_group.message_ids), 1,
'message_process: newly created group should have the incoming email in message_ids')
'message_process: newly created group should have the incoming email in message_ids')
msg = frog_group.message_ids[0]
# Test: message: author found
self.assertEqual(p1id, msg.author_id.id,
'message_process: message on created group should have Sylvie as author_id')
'message_process: message on created group should have Sylvie as author_id')
self.assertIn('Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>', msg.email_from,
'message_process: message on created group should have have an email_from')
'message_process: message on created group should have have an email_from')
# Test: author (not recipient and not Raoul (as alias owner)) added as follower
frog_follower_ids = set([p.id for p in frog_group.message_follower_ids])
self.assertEqual(frog_follower_ids, set([p1id]),
'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
# Test: sent emails: no-one, no bounce effet
sent_emails = self._build_email_kwargs_list
self.assertEqual(len(sent_emails), 0,
'message_process: should not bounce incoming emails')
'message_process: should not bounce incoming emails')
# Data: unlink group
frog_group.unlink()
@@ -387,7 +387,7 @@ class TestMailgateway(TestMailBase):
# Test: email bounced
sent_emails = self._build_email_kwargs_list
self.assertEqual(len(sent_emails), 1,
'message_process: incoming email on Partners alias should send a bounce email')
'message_process: incoming email on Partners alias should send a bounce email')
# Do: incoming email from a parent document follower on a Followers only alias -> ok
self._init_mock_build_email()
@@ -398,15 +398,15 @@ class TestMailgateway(TestMailBase):
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: one message that is the incoming email
self.assertEqual(len(frog_group.message_ids), 1,
'message_process: newly created group should have the incoming email in message_ids')
'message_process: newly created group should have the incoming email in message_ids')
# Test: author (and not recipient) added as follower
frog_follower_ids = set([p.id for p in frog_group.message_follower_ids])
self.assertEqual(frog_follower_ids, set([p1id]),
'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
# Test: sent emails: no-one, no bounce effet
sent_emails = self._build_email_kwargs_list
self.assertEqual(len(sent_emails), 0,
'message_process: should not bounce incoming emails')
'message_process: should not bounce incoming emails')
# --------------------------------------------------
# Test2: update-like alias
@@ -416,43 +416,43 @@ class TestMailgateway(TestMailBase):
self._init_mock_build_email()
self.mail_group.write(cr, uid, [frog_group.id], {'alias_name': 'frogs', 'alias_contact': 'followers', 'alias_force_thread_id': frog_group.id})
frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
to='frogs@example.com>', subject='Re: news')
msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
to='frogs@example.com>', subject='Re: news')
# Test: no group 'Re: news' created, still only 1 Frogs group
self.assertEqual(len(frog_groups), 0,
'message_process: reply on Frogs should not have created a new group with new subject')
'message_process: reply on Frogs should not have created a new group with new subject')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
self.assertEqual(len(frog_groups), 1,
'message_process: reply on Frogs should not have created a duplicate group with old subject')
'message_process: reply on Frogs should not have created a duplicate group with old subject')
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: email bounced
sent_emails = self._build_email_kwargs_list
self.assertEqual(len(sent_emails), 1,
'message_process: incoming email on Followers alias should send a bounce email')
'message_process: incoming email on Followers alias should send a bounce email')
self.assertIn('Re: news', sent_emails[0].get('subject'),
'message_process: bounce email on Followers alias should contain the original subject')
'message_process: bounce email on Followers alias should contain the original subject')
# Do: Pigs alias is restricted, should accept Followers
self._init_mock_build_email()
self.mail_group.message_subscribe(cr, uid, [frog_group.id], [p2id])
frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
msg_id='<1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>',
to='frogs@example.com>', subject='Re: cats')
msg_id='<1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>',
to='frogs@example.com>', subject='Re: cats')
# Test: no group 'Re: news' created, still only 1 Frogs group
self.assertEqual(len(frog_groups), 0,
'message_process: reply on Frogs should not have created a new group with new subject')
'message_process: reply on Frogs should not have created a new group with new subject')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
self.assertEqual(len(frog_groups), 1,
'message_process: reply on Frogs should not have created a duplicate group with old subject')
'message_process: reply on Frogs should not have created a duplicate group with old subject')
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: one new message
self.assertEqual(len(frog_group.message_ids), 2, 'message_process: group should contain 2 messages after reply')
# Test: sent emails: 1 (Sylvie copy of the incoming email, but no bounce)
sent_emails = self._build_email_kwargs_list
self.assertEqual(len(sent_emails), 1,
'message_process: one email should have been generated')
'message_process: one email should have been generated')
self.assertIn('test.sylvie.lelitre@agrolait.com', sent_emails[0].get('email_to')[0],
'message_process: email should be sent to Sylvie')
'message_process: email should be sent to Sylvie')
self.mail_group.message_unsubscribe(cr, uid, [frog_group.id], [p2id])
# --------------------------------------------------
@@ -461,40 +461,40 @@ class TestMailgateway(TestMailBase):
# Do: even with a wrong destination, a reply should end up in the correct thread
frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
to='erroneous@example.com>', subject='Re: news',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
to='erroneous@example.com>', subject='Re: news',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
# Test: no group 'Re: news' created, still only 1 Frogs group
self.assertEqual(len(frog_groups), 0,
'message_process: reply on Frogs should not have created a new group with new subject')
'message_process: reply on Frogs should not have created a new group with new subject')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
self.assertEqual(len(frog_groups), 1,
'message_process: reply on Frogs should not have created a duplicate group with old subject')
'message_process: reply on Frogs should not have created a duplicate group with old subject')
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: one new message
self.assertEqual(len(frog_group.message_ids), 3, 'message_process: group should contain 2 messages after reply')
# Test: author (and not recipient) added as follower
frog_follower_ids = set([p.id for p in frog_group.message_follower_ids])
self.assertEqual(frog_follower_ids, set([p1id, p2id]),
'message_process: after reply, group should have 2 followers')
'message_process: after reply, group should have 2 followers')
# Do: due to some issue, same email goes back into the mailgateway
frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
subject='Re: news', extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
subject='Re: news', extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
# Test: no group 'Re: news' created, still only 1 Frogs group
self.assertEqual(len(frog_groups), 0,
'message_process: reply on Frogs should not have created a new group with new subject')
'message_process: reply on Frogs should not have created a new group with new subject')
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
self.assertEqual(len(frog_groups), 1,
'message_process: reply on Frogs should not have created a duplicate group with old subject')
'message_process: reply on Frogs should not have created a duplicate group with old subject')
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: no new message
self.assertEqual(len(frog_group.message_ids), 3, 'message_process: message with already existing message_id should not have been duplicated')
# Test: message_id is still unique
msg_ids = self.mail_message.search(cr, uid, [('message_id', 'ilike', '<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>')])
self.assertEqual(len(msg_ids), 1,
'message_process: message with already existing message_id should not have been duplicated')
'message_process: message with already existing message_id should not have been duplicated')
# --------------------------------------------------
# Test4: email_from and partner finding
@@ -505,28 +505,28 @@ class TestMailgateway(TestMailBase):
# Do: post a new message, with a known partner -> duplicate emails -> partner
format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik <test_raoul@email.com>',
to='erroneous@example.com>', subject='Re: news (2)',
msg_id='<1198923581.41972151344608186760.JavaMail.new1@agrolait.com>',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
to='erroneous@example.com>', subject='Re: news (2)',
msg_id='<1198923581.41972151344608186760.JavaMail.new1@agrolait.com>',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: author is A-Raoul (only existing)
self.assertEqual(frog_group.message_ids[0].author_id.id, extra_partner_id,
'message_process: email_from -> author_id wrong')
'message_process: email_from -> author_id wrong')
# Do: post a new message, with a known partner -> duplicate emails -> user
frog_group.message_unsubscribe([extra_partner_id])
raoul_email = self.user_raoul.email
self.res_users.write(cr, uid, self.user_raoul_id, {'email': 'test_raoul@email.com'})
format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik <test_raoul@email.com>',
to='erroneous@example.com>', subject='Re: news (3)',
msg_id='<1198923581.41972151344608186760.JavaMail.new2@agrolait.com>',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
to='erroneous@example.com>', subject='Re: news (3)',
msg_id='<1198923581.41972151344608186760.JavaMail.new2@agrolait.com>',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: author is Raoul (user), not A-Raoul
self.assertEqual(frog_group.message_ids[0].author_id.id, self.partner_raoul_id,
'message_process: email_from -> author_id wrong')
'message_process: email_from -> author_id wrong')
# Do: post a new message, with a known partner -> duplicate emails -> partner because is follower
frog_group.message_unsubscribe([self.partner_raoul_id])
@@ -534,14 +534,14 @@ class TestMailgateway(TestMailBase):
raoul_email = self.user_raoul.email
self.res_users.write(cr, uid, self.user_raoul_id, {'email': 'test_raoul@email.com'})
format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik <test_raoul@email.com>',
to='erroneous@example.com>', subject='Re: news (3)',
msg_id='<1198923581.41972151344608186760.JavaMail.new3@agrolait.com>',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
to='erroneous@example.com>', subject='Re: news (3)',
msg_id='<1198923581.41972151344608186760.JavaMail.new3@agrolait.com>',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
# Test: author is Raoul (user), not A-Raoul
self.assertEqual(frog_group.message_ids[0].author_id.id, extra_partner_id,
'message_process: email_from -> author_id wrong')
'message_process: email_from -> author_id wrong')
self.res_users.write(cr, uid, self.user_raoul_id, {'email': raoul_email})
@@ -551,37 +551,37 @@ class TestMailgateway(TestMailBase):
# Do: incoming email with model that does not accepts incoming emails must raise
self.assertRaises(AssertionError,
format_and_process,
MAIL_TEMPLATE,
to='noone@example.com', subject='spam', extra='', model='res.country',
msg_id='<1198923581.41972151344608186760.JavaMail.new4@agrolait.com>')
format_and_process,
MAIL_TEMPLATE,
to='noone@example.com', subject='spam', extra='', model='res.country',
msg_id='<1198923581.41972151344608186760.JavaMail.new4@agrolait.com>')
# Do: incoming email without model and without alias must raise
self.assertRaises(AssertionError,
format_and_process,
MAIL_TEMPLATE,
to='noone@example.com', subject='spam', extra='',
msg_id='<1198923581.41972151344608186760.JavaMail.new5@agrolait.com>')
format_and_process,
MAIL_TEMPLATE,
to='noone@example.com', subject='spam', extra='',
msg_id='<1198923581.41972151344608186760.JavaMail.new5@agrolait.com>')
# Do: incoming email with model that accepting incoming emails as fallback
frog_groups = format_and_process(MAIL_TEMPLATE,
to='noone@example.com',
subject='Spammy', extra='', model='mail.group',
msg_id='<1198923581.41972151344608186760.JavaMail.new6@agrolait.com>')
to='noone@example.com',
subject='Spammy', extra='', model='mail.group',
msg_id='<1198923581.41972151344608186760.JavaMail.new6@agrolait.com>')
self.assertEqual(len(frog_groups), 1,
'message_process: erroneous email but with a fallback model should have created a new mail.group')
'message_process: erroneous email but with a fallback model should have created a new mail.group')
# Do: incoming email in plaintext should be stored as html
frog_groups = format_and_process(MAIL_TEMPLATE_PLAINTEXT,
to='groups@example.com', subject='Frogs Return', extra='',
msg_id='<deadcafe.1337@smtp.agrolait.com>')
to='groups@example.com', subject='Frogs Return', extra='',
msg_id='<deadcafe.1337@smtp.agrolait.com>')
# Test: one group created with one message
self.assertEqual(len(frog_groups), 1, 'message_process: a new mail.group should have been created')
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
msg = frog_group.message_ids[0]
# Test: plain text content should be wrapped and stored as html
self.assertIn('<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>', msg.body,
'message_process: plaintext incoming email incorrectly parsed')
'message_process: plaintext incoming email incorrectly parsed')
@mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
def test_20_thread_parent_resolution(self):
@@ -602,26 +602,26 @@ class TestMailgateway(TestMailBase):
# Reply to msg1, make sure the reply is properly attached using the various reply identification mechanisms
# 0. Direct alias match
reply_msg1 = format(MAIL_TEMPLATE, to='Pretty Pigs <group+pigs@example.com>',
extra='In-Reply-To: %s' % msg1.message_id,
msg_id='<1198923581.41972151344608186760.JavaMail.2@agrolait.com>')
extra='In-Reply-To: %s' % msg1.message_id,
msg_id='<1198923581.41972151344608186760.JavaMail.2@agrolait.com>')
self.mail_group.message_process(cr, uid, None, reply_msg1)
# 1. In-Reply-To header
reply_msg2 = format(MAIL_TEMPLATE, to='erroneous@example.com',
extra='In-Reply-To: %s' % msg1.message_id,
msg_id='<1198923581.41972151344608186760.JavaMail.3@agrolait.com>')
extra='In-Reply-To: %s' % msg1.message_id,
msg_id='<1198923581.41972151344608186760.JavaMail.3@agrolait.com>')
self.mail_group.message_process(cr, uid, None, reply_msg2)
# 2. References header
reply_msg3 = format(MAIL_TEMPLATE, to='erroneous@example.com',
extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id,
msg_id='<1198923581.41972151344608186760.JavaMail.4@agrolait.com>')
extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id,
msg_id='<1198923581.41972151344608186760.JavaMail.4@agrolait.com>')
self.mail_group.message_process(cr, uid, None, reply_msg3)
# 3. Subject contains [<ID>] + model passed to message+process -> only attached to group, but not to mail (not in msg1.child_ids)
reply_msg4 = format(MAIL_TEMPLATE, to='erroneous@example.com',
extra='', subject='Re: [%s] 1' % self.group_pigs_id,
msg_id='<1198923581.41972151344608186760.JavaMail.5@agrolait.com>')
extra='', subject='Re: [%s] 1' % self.group_pigs_id,
msg_id='<1198923581.41972151344608186760.JavaMail.5@agrolait.com>')
self.mail_group.message_process(cr, uid, 'mail.group', reply_msg4)
group_pigs.refresh()
@@ -633,12 +633,19 @@ class TestMailgateway(TestMailBase):
""" Testing private discussion between partners. """
cr, uid = self.cr, self.uid
def format(template, to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: 1',
extra='', email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>',
msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>'):
return template.format(to=to, subject=subject, extra=extra, email_from=email_from, msg_id=msg_id)
# Do: Raoul writes to Bert and Administrator, with a thread_model in context that should not be taken into account
msg1_pids = [self.partner_admin_id, self.partner_bert_id]
msg1_id = self.mail_thread.message_post(cr, self.user_raoul_id, False,
partner_ids=msg1_pids,
subtype='mail.mt_comment',
context={'thread_model': 'mail.group'})
msg1_id = self.mail_thread.message_post(
cr, self.user_raoul_id, False,
partner_ids=msg1_pids,
subtype='mail.mt_comment',
context={'thread_model': 'mail.group'}
)
# Test: message recipients
msg = self.mail_message.browse(cr, uid, msg1_id)
@@ -647,16 +654,26 @@ class TestMailgateway(TestMailBase):
test_pids = msg1_pids
test_nids = msg1_pids
self.assertEqual(set(msg_pids), set(test_pids),
'message_post: private discussion: incorrect recipients')
'message_post: private discussion: incorrect recipients')
self.assertEqual(set(msg_nids), set(test_nids),
'message_post: private discussion: incorrect notified recipients')
'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')
'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')
self.assertIn('openerp-private', msg.message_id,
'message_post: private discussion: message-id should contain the private keyword')
# Do: Bert replies through mailgateway (is a customer)
msg2_id = self.mail_thread.message_post(cr, uid, False,
author_id=self.partner_bert_id,
parent_id=msg1_id, subtype='mail.mt_comment')
reply_message = format(MAIL_TEMPLATE, to='not_important@mydomain.com',
email_from='bert@bert.fr',
extra='In-Reply-To: %s' % msg.message_id,
msg_id='<test30.JavaMail.0@agrolait.com>')
self.mail_thread.message_process(cr, uid, None, reply_message)
# Test: last mail_message created
msg2_id = self.mail_message.search(cr, uid, [], limit=1)[0]
# Test: message recipients
msg = self.mail_message.browse(cr, uid, msg2_id)
@@ -664,14 +681,32 @@ class TestMailgateway(TestMailBase):
msg_nids = [p.id for p in msg.notified_partner_ids]
test_pids = [self.partner_admin_id, self.partner_raoul_id]
test_nids = test_pids
self.assertEqual(msg.author_id.id, self.partner_bert_id,
'message_post: private discussion: wrong author through mailgatewya based on email')
self.assertEqual(set(msg_pids), set(test_pids),
'message_post: private discussion: incorrect recipients when replying')
'message_post: private discussion: incorrect recipients when replying')
self.assertEqual(set(msg_nids), set(test_nids),
'message_post: private discussion: incorrect notified recipients when replying')
'message_post: private discussion: incorrect notified recipients when replying')
# Do: Bert replies through chatter (is a customer)
msg3_id = self.mail_thread.message_post(
cr, uid, False,
author_id=self.partner_bert_id,
parent_id=msg1_id, subtype='mail.mt_comment')
# Test: message recipients
msg = self.mail_message.browse(cr, uid, msg3_id)
msg_pids = [p.id for p in msg.partner_ids]
msg_nids = [p.id for p in msg.notified_partner_ids]
test_pids = [self.partner_admin_id, self.partner_raoul_id]
test_nids = test_pids
self.assertEqual(set(msg_pids), set(test_pids),
'message_post: private discussion: incorrect recipients when replying')
self.assertEqual(set(msg_nids), set(test_nids),
'message_post: private discussion: incorrect notified recipients when replying')
# Do: Administrator replies
msg3_id = self.mail_thread.message_post(cr, uid, False,
parent_id=msg2_id, subtype='mail.mt_comment')
msg3_id = self.mail_thread.message_post(cr, uid, False, parent_id=msg3_id, subtype='mail.mt_comment')
# Test: message recipients
msg = self.mail_message.browse(cr, uid, msg3_id)
@@ -680,6 +715,6 @@ class TestMailgateway(TestMailBase):
test_pids = [self.partner_bert_id, self.partner_raoul_id]
test_nids = test_pids
self.assertEqual(set(msg_pids), set(test_pids),
'message_post: private discussion: incorrect recipients when replying')
'message_post: private discussion: incorrect recipients when replying')
self.assertEqual(set(msg_nids), set(test_nids),
'message_post: private discussion: incorrect notified recipients when replying')
'message_post: private discussion: incorrect notified recipients when replying')
+15 -3
View File
@@ -73,6 +73,11 @@ class mail_compose_message(osv.TransientModel):
res_id = context.get('default_res_id', context.get('active_id'))
message_id = context.get('default_parent_id', context.get('message_id', context.get('active_id')))
active_ids = context.get('active_ids')
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:
result['active_domain'] = ''
# get default values according to the composition mode
if composition_mode == 'reply':
@@ -112,6 +117,8 @@ class mail_compose_message(osv.TransientModel):
'partner_ids': fields.many2many('res.partner',
'mail_compose_message_res_partner_rel',
'wizard_id', 'partner_id', 'Additional contacts'),
'use_active_domain': fields.boolean('Use active domain'),
'active_domain': fields.char('Active domain', readonly=True),
'post': fields.boolean('Post a copy in the document',
help='Post a copy of the message on the document communication history.'),
'notify': fields.boolean('Notify followers',
@@ -129,7 +136,6 @@ class mail_compose_message(osv.TransientModel):
'body': lambda self, cr, uid, ctx={}: '',
'subject': lambda self, cr, uid, ctx={}: False,
'partner_ids': lambda self, cr, uid, ctx={}: [],
'notify': lambda self, cr, uid, ctx={}: False,
'post': lambda self, cr, uid, ctx={}: True,
'same_thread': lambda self, cr, uid, ctx={}: True,
}
@@ -238,8 +244,14 @@ class mail_compose_message(osv.TransientModel):
context['thread_model'] = wizard.model
active_model_pool = self.pool['mail.thread']
# wizard works in batch mode: [res_id] or active_ids
res_ids = active_ids if mass_mail_mode and wizard.model and active_ids else [wizard.res_id]
# wizard works in batch mode: [res_id] or active_ids or active_domain
if mass_mail_mode and wizard.use_active_domain and wizard.model:
res_ids = self.pool[wizard.model].search(cr, uid, eval(wizard.active_domain), context=context)
elif mass_mail_mode and wizard.model and active_ids:
res_ids = active_ids
else:
res_ids = [wizard.res_id]
for res_id in res_ids:
# mail.message values, according to the wizard options
post_values = {
@@ -12,6 +12,9 @@
<field name="model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="parent_id" invisible="1"/>
<field name="mail_server_id" invisible="1"/>
<field name="use_active_domain" invisible="1"/>
<field name="active_domain" invisible="1"/>
<!-- visible wizard -->
<field name="email_from"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
@@ -1,10 +1,10 @@
<openerp>
<data>
<record id="action_dummy" model="ir.actions.server">
<field name="name">Dummy Action</field>
<field name="name">Dummy Python Code</field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="state">dummy</field>
<field name="code"/>
<field name="state">code</field>
<field name="code">True</field>
<field eval="5" name="sequence"/>
<field eval="True" name="condition"/>
</record>

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