diff --git a/addons/account/wizard/account_report_common.py b/addons/account/wizard/account_report_common.py
index 02da9a6ab1f..9acc09e9026 100644
--- a/addons/account/wizard/account_report_common.py
+++ b/addons/account/wizard/account_report_common.py
@@ -30,9 +30,11 @@ class account_common_report(osv.osv_memory):
_description = "Account Common Report"
def onchange_chart_id(self, cr, uid, ids, chart_account_id=False, context=None):
+ res = {}
if chart_account_id:
company_id = self.pool.get('account.account').browse(cr, uid, chart_account_id, context=context).company_id.id
- return {'value': {'company_id': company_id}}
+ res['value'] = {'company_id': company_id}
+ return res
_columns = {
'chart_account_id': fields.many2one('account.account', 'Chart of Account', help='Select Charts of Accounts', required=True, domain = [('parent_id','=',False)]),
diff --git a/addons/account/wizard/pos_box.py b/addons/account/wizard/pos_box.py
index b4254b5ba84..b5d1cc37776 100644
--- a/addons/account/wizard/pos_box.py
+++ b/addons/account/wizard/pos_box.py
@@ -40,6 +40,10 @@ class CashBox(osv.osv_memory):
return {}
+ def _create_bank_statement_line(self, cr, uid, box, record, context=None):
+ values = self._compute_values_for_statement_line(cr, uid, box, record, context=context)
+ return self.pool.get('account.bank.statement.line').create(cr, uid, values, context=context)
+
class CashBoxIn(CashBox):
_name = 'cash.box.in'
@@ -49,30 +53,24 @@ class CashBoxIn(CashBox):
'ref' : fields.char('Reference', size=32),
})
- def _create_bank_statement_line(self, cr, uid, box, record, context=None):
- absl_proxy = self.pool.get('account.bank.statement.line')
-
- values = {
+ def _compute_values_for_statement_line(self, cr, uid, box, record, context=None):
+ return {
'statement_id' : record.id,
'journal_id' : record.journal_id.id,
'account_id' : record.journal_id.internal_account_id.id,
'amount' : box.amount or 0.0,
- 'ref' : "%s" % (box.ref or ''),
+ 'ref' : '%s' % (box.ref or ''),
'name' : box.name,
}
- return absl_proxy.create(cr, uid, values, context=context)
-
CashBoxIn()
class CashBoxOut(CashBox):
_name = 'cash.box.out'
- def _create_bank_statement_line(self, cr, uid, box, record, context=None):
- absl_proxy = self.pool.get('account.bank.statement.line')
-
+ def _compute_values_for_statement_line(self, cr, uid, box, record, context=None):
amount = box.amount or 0.0
- values = {
+ return {
'statement_id' : record.id,
'journal_id' : record.journal_id.id,
'account_id' : record.journal_id.internal_account_id.id,
@@ -80,6 +78,4 @@ class CashBoxOut(CashBox):
'name' : box.name,
}
- return absl_proxy.create(cr, uid, values, context=context)
-
CashBoxOut()
diff --git a/addons/account_accountant/account_accountant_data.xml b/addons/account_accountant/account_accountant_data.xml
index 383b53cf49e..5b563017c62 100644
--- a/addons/account_accountant/account_accountant_data.xml
+++ b/addons/account_accountant/account_accountant_data.xml
@@ -21,13 +21,14 @@
-
-
-
- Module Accounting and Finance has been installed.
- With OpenERP's accounting, you can get an instant access to all your financial data, setup your analytic accounting, forecast your taxes, control your budgets, easily create and send invoices, record bank statements, etc.
+
+ mail.group
+
+ notification
+ Accounting and Finance application installed!
+ With OpenERP's accounting, you get instant access to your financial data, and can setup analytic accounting, forecast taxes, control budgets, easily create and send invoices, record bank statements, etc.
-The accounting features are fully integrated with others OpenERP applications to automate all your processes: creation of customer invoices, control of supplier invoices, point-of-sale integration, automated follow-ups, etc.
-
+The accounting features are fully integrated with other OpenERP applications to automate all your processes: creation of customer invoices, control of supplier invoices, point-of-sale integration, automated follow-ups, etc.
+
diff --git a/addons/account_analytic_analysis/account_analytic_analysis_view.xml b/addons/account_analytic_analysis/account_analytic_analysis_view.xml
index 63a8e6c7add..12d8fad1b95 100644
--- a/addons/account_analytic_analysis/account_analytic_analysis_view.xml
+++ b/addons/account_analytic_analysis/account_analytic_analysis_view.xml
@@ -6,12 +6,11 @@
Analytic Account form
-->
-
+
+ Sales Orders
+ sale.order
+ account.analytic.account
+
account.analytic.account.invoice.form.inherit
diff --git a/addons/account_asset/account_asset_view.xml b/addons/account_asset/account_asset_view.xml
index c230f63e204..bb0222d5e4f 100644
--- a/addons/account_asset/account_asset_view.xml
+++ b/addons/account_asset/account_asset_view.xml
@@ -100,7 +100,7 @@
-
+
@@ -192,7 +192,7 @@
-
+
@@ -211,7 +211,7 @@
-
+
diff --git a/addons/account_bank_statement_extensions/i18n/pt_BR.po b/addons/account_bank_statement_extensions/i18n/pt_BR.po
new file mode 100644
index 00000000000..ade94336f2a
--- /dev/null
+++ b/addons/account_bank_statement_extensions/i18n/pt_BR.po
@@ -0,0 +1,385 @@
+# Brazilian Portuguese 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 , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:35+0000\n"
+"PO-Revision-Date: 2012-09-11 18:35+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Brazilian Portuguese \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-12 04:36+0000\n"
+"X-Generator: Launchpad (build 15930)\n"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Search Bank Transactions"
+msgstr "Procurar Transações Bancárias"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+#: selection:account.bank.statement.line,state:0
+msgid "Confirmed"
+msgstr "Confirmado"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement:0
+#: view:account.bank.statement.line:0
+msgid "Glob. Id"
+msgstr "ID Global"
+
+#. module: account_bank_statement_extensions
+#: selection:account.bank.statement.line.global,type:0
+msgid "CODA"
+msgstr "CODA"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line.global,parent_id:0
+msgid "Parent Code"
+msgstr "Código da Conta-pai"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Debit"
+msgstr "Débito"
+
+#. 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 "Cancelar linhas de instrução selecionadas"
+
+#. module: account_bank_statement_extensions
+#: constraint:res.partner.bank:0
+msgid "The RIB and/or IBAN is not valid"
+msgstr "A RIB e/ ou IBAN não é válido."
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Group By..."
+msgstr "Agrupar Por..."
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line,state:0
+msgid "State"
+msgstr "Situação"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+#: selection:account.bank.statement.line,state:0
+msgid "Draft"
+msgstr "Provisório"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Statement"
+msgstr "Demonstrativo"
+
+#. 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 "Confirme as linhas do demonstrativo selecionadas"
+
+#. 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 "Relatório de Balanço Bancário"
+
+#. module: account_bank_statement_extensions
+#: view:cancel.statement.line:0
+msgid "Cancel Lines"
+msgstr "Cancelar Linhas"
+
+#. 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 "Informações de Pagamento em Lote"
+
+#. module: account_bank_statement_extensions
+#: view:confirm.statement.line:0
+msgid "Confirm Lines"
+msgstr "Confirmar Linhas"
+
+#. module: account_bank_statement_extensions
+#: code:addons/account_bank_statement_extensions/account_bank_statement.py:130
+#, python-format
+msgid ""
+"Delete operation not allowed ! Please go to the associated bank "
+"statement in order to delete and/or modify this bank statement line"
+msgstr ""
+"Não é permitido excluir! Vá a linha do demonstrativo bancário associada para "
+"excluir ou modificar esta linha do demonstrativo"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line.global,type:0
+msgid "Type"
+msgstr "Tipo"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+#: field:account.bank.statement.line,journal_id:0
+#: report:bank.statement.balance.report:0
+msgid "Journal"
+msgstr "Diário"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Confirmed Statement Lines."
+msgstr "Linhas do Demonstrativo Confirmadas."
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Credit Transactions."
+msgstr "Transações de Crédito"
+
+#. 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 "cancelar linhas do demonstrativo selecionadas."
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line,counterparty_number:0
+msgid "Counterparty Number"
+msgstr "Número da Contrapartida"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line.global:0
+msgid "Transactions"
+msgstr "Transações"
+
+#. module: account_bank_statement_extensions
+#: code:addons/account_bank_statement_extensions/account_bank_statement.py:130
+#, python-format
+msgid "Warning"
+msgstr "Aviso"
+
+#. module: account_bank_statement_extensions
+#: report:bank.statement.balance.report:0
+msgid "Closing Balance"
+msgstr "Saldo final"
+
+#. module: account_bank_statement_extensions
+#: report:bank.statement.balance.report:0
+msgid "Date"
+msgstr "Data"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+#: field:account.bank.statement.line,globalisation_amount:0
+msgid "Glob. Amount"
+msgstr "Valor Global"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Debit Transactions."
+msgstr "Transações de Débito."
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Extended Filters..."
+msgstr "Filtros Extendidos..."
+
+#. module: account_bank_statement_extensions
+#: view:confirm.statement.line:0
+msgid "Confirmed lines cannot be changed anymore."
+msgstr "Linhas confirmadas não podem ser alteradas."
+
+#. module: account_bank_statement_extensions
+#: constraint:res.partner.bank:0
+msgid ""
+"\n"
+"Please define BIC/Swift code on bank for bank type IBAN Account to make "
+"valid payments"
+msgstr ""
+"\n"
+"Por favor defina o BIC/Swift code no Banco para o tipo de conta IBAN para "
+"fazer pagamentos válidos"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line,val_date:0
+msgid "Valuta Date"
+msgstr ""
+
+#. module: account_bank_statement_extensions
+#: model:ir.actions.act_window,help:account_bank_statement_extensions.action_confirm_statement_line
+msgid "Confirm selected statement lines."
+msgstr "Confirmar as linhas do demonstrativo."
+
+#. module: account_bank_statement_extensions
+#: view:cancel.statement.line:0
+msgid "Are you sure you want to cancel the selected Bank Statement lines ?"
+msgstr ""
+"Você tem certeza de que deseja cancelar as Linhas de Demonstrativo Bancário "
+"selecionadas?"
+
+#. module: account_bank_statement_extensions
+#: report:bank.statement.balance.report:0
+msgid "Name"
+msgstr "Nome"
+
+#. 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 "Notas"
+
+#. module: account_bank_statement_extensions
+#: selection:account.bank.statement.line.global,type:0
+msgid "Manual"
+msgstr "Manual"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Credit"
+msgstr "Crédito"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line.global,amount:0
+msgid "Amount"
+msgstr "Valor"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Fin.Account"
+msgstr "Fin.Account"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line,counterparty_currency:0
+msgid "Counterparty Currency"
+msgstr "Moeda da Contrapartida"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line,counterparty_bic:0
+msgid "Counterparty BIC"
+msgstr "BIC da Contrapartida"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line.global,child_ids:0
+msgid "Child Codes"
+msgstr "Códigos derivados (sub-contas)"
+
+#. module: account_bank_statement_extensions
+#: view:confirm.statement.line:0
+msgid "Are you sure you want to confirm the selected Bank Statement lines ?"
+msgstr ""
+"Você deseja confirmar as Linhas do Demonstrativo Bancário selecionadas?"
+
+#. module: account_bank_statement_extensions
+#: constraint:account.bank.statement.line:0
+msgid ""
+"The amount of the voucher must be the same amount as the one on the "
+"statement line"
+msgstr ""
+"O valor do recibo deve ser o mesmo valor da linha equivalente no extrato"
+
+#. 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 ""
+"Código para identificar transações que pertencem ao nível de globalização "
+"dentro de um mesmo lote de pagamento"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Draft Statement Lines."
+msgstr "Linhas de Demonstrativo Provisórias."
+
+#. 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 "Linha do Demonstrativo Bancário"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line.global,code:0
+msgid "Code"
+msgstr "Código"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line,counterparty_name:0
+msgid "Counterparty Name"
+msgstr "Nome da Contrapartida"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line.global,name:0
+msgid "Communication"
+msgstr "Comunicação"
+
+#. module: account_bank_statement_extensions
+#: model:ir.model,name:account_bank_statement_extensions.model_res_partner_bank
+msgid "Bank Accounts"
+msgstr "Contas Bancárias"
+
+#. module: account_bank_statement_extensions
+#: constraint:account.bank.statement:0
+msgid "The journal and period chosen have to belong to the same company."
+msgstr "O diário e o período escolhido tem que pertencer à mesma empresa."
+
+#. module: account_bank_statement_extensions
+#: model:ir.model,name:account_bank_statement_extensions.model_account_bank_statement
+msgid "Bank Statement"
+msgstr "Extrato Bancário"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Statement Line"
+msgstr "Linha do Demonstrativo"
+
+#. module: account_bank_statement_extensions
+#: sql_constraint:account.bank.statement.line.global:0
+msgid "The code must be unique !"
+msgstr "O código precisa ser único!"
+
+#. 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 "Linhas do Demonstrativo Bancário"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line.global:0
+msgid "Child Batch Payments"
+msgstr "Lote de Pagamentos Filho"
+
+#. module: account_bank_statement_extensions
+#: view:cancel.statement.line:0
+#: view:confirm.statement.line:0
+msgid "Cancel"
+msgstr "Cancelar"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Statement Lines"
+msgstr "Linhas do Demonstrativo"
+
+#. module: account_bank_statement_extensions
+#: view:account.bank.statement.line:0
+msgid "Total Amount"
+msgstr "Valor Total"
+
+#. module: account_bank_statement_extensions
+#: field:account.bank.statement.line,globalisation_id:0
+msgid "Globalisation ID"
+msgstr "ID Globalização"
diff --git a/addons/account_check_writing/i18n/es_EC.po b/addons/account_check_writing/i18n/es_EC.po
new file mode 100644
index 00000000000..7543458097f
--- /dev/null
+++ b/addons/account_check_writing/i18n/es_EC.po
@@ -0,0 +1,207 @@
+# Spanish (Ecuador) 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 , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:35+0000\n"
+"PO-Revision-Date: 2012-09-12 01:39+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Spanish (Ecuador) \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-12 04:36+0000\n"
+"X-Generator: Launchpad (build 15930)\n"
+
+#. module: account_check_writing
+#: selection:res.company,check_layout:0
+msgid "Check on Top"
+msgstr "Cheque on Top"
+
+#. module: account_check_writing
+#: model:ir.actions.act_window,help:account_check_writing.action_write_check
+msgid ""
+"The check payment form allows you to track the payment you do to your "
+"suppliers specially by check. When you select a supplier, the payment method "
+"and an amount for the payment, OpenERP will propose to reconcile your "
+"payment with the open supplier invoices or bills.You can print the check"
+msgstr ""
+"El pago de cheques permite rastrear el pago a sus proveedores. Cuando "
+"selecciona un proveedor, el método de pago y monto, OpenERP propondrá "
+"conciliarlo con tu factura, y podrá imprimir el cheque."
+
+#. module: account_check_writing
+#: view:account.voucher:0
+#: model:ir.actions.report.xml,name:account_check_writing.account_print_check_bottom
+#: model:ir.actions.report.xml,name:account_check_writing.account_print_check_middle
+#: model:ir.actions.report.xml,name:account_check_writing.account_print_check_top
+msgid "Print Check"
+msgstr "Imprimir Cheque"
+
+#. module: account_check_writing
+#: selection:res.company,check_layout:0
+msgid "Check in middle"
+msgstr "Cheque in middle"
+
+#. 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 ""
+
+#. module: account_check_writing
+#: selection:res.company,check_layout:0
+msgid "Check on bottom"
+msgstr "Cheque on bottom"
+
+#. module: account_check_writing
+#: constraint:res.company:0
+msgid "Error! You can not create recursive companies."
+msgstr "Error! No puede crear compañías recursivas."
+
+#. 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 "Activar si este diario es usado para emitir cheques"
+
+#. module: account_check_writing
+#: field:account.journal,allow_check_writing:0
+msgid "Allow Check writing"
+msgstr "Permitir emisión de cheques"
+
+#. 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 "Descripción"
+
+#. module: account_check_writing
+#: model:ir.model,name:account_check_writing.model_account_journal
+msgid "Journal"
+msgstr "Diario"
+
+#. 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 "Escribir Cheque"
+
+#. 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 "Descuento"
+
+#. 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 "Monto Inicial"
+
+#. module: account_check_writing
+#: view:res.company:0
+msgid "Configuration"
+msgstr "Configuración"
+
+#. module: account_check_writing
+#: field:account.voucher,allow_check:0
+msgid "Allow Check Writing"
+msgstr "Permitir Emisión de Cheques"
+
+#. 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 "Pagos"
+
+#. module: account_check_writing
+#: field:account.journal,use_preprint_check:0
+msgid "Use Preprinted Check"
+msgstr "Usar cheque preimpreso"
+
+#. module: account_check_writing
+#: sql_constraint:res.company:0
+msgid "The company name must be unique !"
+msgstr "¡El nombre de la compañía debe ser único!"
+
+#. 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 "Fecha de vencimiento"
+
+#. module: account_check_writing
+#: model:ir.model,name:account_check_writing.model_res_company
+msgid "Companies"
+msgstr "Compañias"
+
+#. module: account_check_writing
+#: view:res.company:0
+msgid "Default Check Layout"
+msgstr ""
+
+#. module: account_check_writing
+#: constraint:account.journal:0
+msgid ""
+"Configuration error! The currency chosen should be shared by the default "
+"accounts too."
+msgstr ""
+"Error de Configuración! La moneda seleccionada debe ser compartida por las "
+"cuentas por defecto tambíen"
+
+#. module: account_check_writing
+#: report:account.print.check.bottom:0
+#: report:account.print.check.middle:0
+msgid "Balance Due"
+msgstr "Saldo Deudor"
+
+#. 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 "Monto Cheque"
+
+#. module: account_check_writing
+#: model:ir.model,name:account_check_writing.model_account_voucher
+msgid "Accounting Voucher"
+msgstr "Comprobantes de Pago"
+
+#. module: account_check_writing
+#: sql_constraint:account.journal:0
+msgid "The name of the journal must be unique per company !"
+msgstr "El nombre del diaro debe ser único por compañía !"
+
+#. module: account_check_writing
+#: sql_constraint:account.journal:0
+msgid "The code of the journal must be unique per company !"
+msgstr "El código del diario debe ser único por compañía !"
+
+#. module: account_check_writing
+#: field:account.voucher,amount_in_word:0
+msgid "Amount in Word"
+msgstr "Monto en Letras"
+
+#. module: account_check_writing
+#: report:account.print.check.top:0
+msgid "Open Balance"
+msgstr "Saldo Inicial"
+
+#. module: account_check_writing
+#: field:res.company,check_layout:0
+msgid "Choose Check layout"
+msgstr "Elegir diseño de cheque"
+
+#~ msgid "Default Check layout"
+#~ msgstr "Diseño de cheque por defecto"
diff --git a/addons/account_coda/account_coda_view.xml b/addons/account_coda/account_coda_view.xml
index 0b42ad6352f..23e63b2a4a4 100644
--- a/addons/account_coda/account_coda_view.xml
+++ b/addons/account_coda/account_coda_view.xml
@@ -16,11 +16,11 @@
-
+
-
+
@@ -32,7 +32,7 @@
-
+
@@ -48,7 +48,7 @@
-
+
@@ -305,7 +305,7 @@
-
+
diff --git a/addons/account_followup/wizard/account_followup_print.py b/addons/account_followup/wizard/account_followup_print.py
index d151d2171a4..1e84681b1ea 100644
--- a/addons/account_followup/wizard/account_followup_print.py
+++ b/addons/account_followup/wizard/account_followup_print.py
@@ -213,8 +213,6 @@ class account_followup_print_all(osv.osv_memory):
mod_obj = self.pool.get('ir.model.data')
move_obj = self.pool.get('account.move.line')
user_obj = self.pool.get('res.users')
- line_obj = self.pool.get('account_followup.stat')
- mail_message = self.pool.get('mail.message')
if context is None:
context = {}
@@ -235,13 +233,7 @@ class account_followup_print_all(osv.osv_memory):
total_amt += line.debit - line.credit
dest = False
if partner:
- if partner.type=='contact':
- if adr.email:
- dest = [partner.email]
- if (not dest) and partner.type=='default':
- if partner.email:
- dest = [partner.email]
- src = tools.config.options['email_from']
+ dest = [partner.email]
if not data.partner_lang:
body = data.email_body
else:
@@ -281,7 +273,12 @@ class account_followup_print_all(osv.osv_memory):
msg = ''
if dest:
try:
- mail_message.schedule_with_attach(cr, uid, src, dest, sub, body, context=context)
+ vals = {'state': 'outgoing',
+ 'subject': sub,
+ 'body_html': '%s ' % body,
+ 'email_to': dest,
+ 'email_from': data_user.email or tools.config.options['email_from']}
+ self.pool.get('mail.mail').create(cr, uid, vals, context=context)
msg_sent += partner.name + '\n'
except Exception, e:
raise osv.except_osv('Error !', e )
diff --git a/addons/account_payment/account_payment_view.xml b/addons/account_payment/account_payment_view.xml
index e3e9b2ae558..4d69c2d7315 100644
--- a/addons/account_payment/account_payment_view.xml
+++ b/addons/account_payment/account_payment_view.xml
@@ -30,8 +30,8 @@
-
-
+
+
@@ -131,8 +131,8 @@
-
-
+
+
@@ -151,8 +151,8 @@
-
-
+
+
@@ -171,8 +171,8 @@
-
-
+
+
@@ -257,8 +257,8 @@
-
-
+
+
@@ -277,8 +277,11 @@
-
-
+
+
+
+
+
@@ -302,8 +305,8 @@
-
-
+
+
diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py
index 223705af85e..f75cda8cdde 100644
--- a/addons/account_voucher/account_voucher.py
+++ b/addons/account_voucher/account_voucher.py
@@ -1293,17 +1293,17 @@ class account_voucher(osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
message = "%s created ." % self._document_type[obj.type or False]
- self.message_append_note(cr, uid, [obj.id], body=message, context=context)
+ self.message_post(cr, uid, [obj.id], body=message, context=context)
def post_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
message = "%s '%s' is posted ." % (self._document_type[obj.type or False], obj.move_id.name)
- self.message_append_note(cr, uid, [obj.id], body=message, context=context)
+ self.message_post(cr, uid, [obj.id], body=message, context=context)
def reconcile_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
message = "%s reconciled ." % self._document_type[obj.type or False]
- self.message_append_note(cr, uid, [obj.id], body=message, context=context)
+ self.message_post(cr, uid, [obj.id], body=message, context=context)
account_voucher()
diff --git a/addons/account_voucher/account_voucher_data.xml b/addons/account_voucher/account_voucher_data.xml
index 8be2851a434..656d2c2cd91 100644
--- a/addons/account_voucher/account_voucher_data.xml
+++ b/addons/account_voucher/account_voucher_data.xml
@@ -2,15 +2,16 @@
-
-
-
- Module eInvoicing & Payments has been installed.
- OpenERP's electronic invoicing allows to ease and fasten the creation of invoices and collection of customer payments. Invoices are created in a few clicks and your customers receive them by email. They can pay online and/or import them in their own system.
+
+ mail.group
+
+ notification
+ eInvoicing & Payments application installed!
+ OpenERP's electronic invoicing accelerates the creation of invoices and collection of customer payments. Invoices are created in a few clicks and your customers receive them by email. They can pay online and/or import them in their own system.
-You can track customer payments easily and automate the reminders. You get an overview of the discussion with your customers on each invoice to ensure a full traceability.
+You can track customer payments easily and automate follow-ups. You get an overview of the discussion with your customers on each invoice for easier traceability.
-If you want to use advanced accounting features, you should install the "Accounting and Finance" module.
-
+For advanced accounting features, you should install the "Accounting and Finance" module.
+
diff --git a/addons/account_voucher/account_voucher_view.xml b/addons/account_voucher/account_voucher_view.xml
index 64e1cf22156..ba9d6433888 100644
--- a/addons/account_voucher/account_voucher_view.xml
+++ b/addons/account_voucher/account_voucher_view.xml
@@ -75,7 +75,7 @@
-
+
diff --git a/addons/account_voucher/voucher_payment_receipt_view.xml b/addons/account_voucher/voucher_payment_receipt_view.xml
index 503c3aeaa5e..c41ed6d5577 100644
--- a/addons/account_voucher/voucher_payment_receipt_view.xml
+++ b/addons/account_voucher/voucher_payment_receipt_view.xml
@@ -102,9 +102,9 @@
-
+
-
+
@@ -194,7 +194,7 @@
-
+
@@ -231,8 +231,8 @@
-
-
+
+
@@ -366,7 +366,7 @@
-
+
@@ -401,7 +401,7 @@
-
+
diff --git a/addons/analytic/analytic.py b/addons/analytic/analytic.py
index 7eeb9266e47..d90e2006a71 100644
--- a/addons/analytic/analytic.py
+++ b/addons/analytic/analytic.py
@@ -297,7 +297,7 @@ class account_analytic_account(osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
- self.message_append_note(cr, uid, [obj.id], body=_("Contract for %s has been created .") % (obj.partner_id.name), context=context)
+ self.message_post(cr, uid, [obj.id], body=_("Contract for %s has been created .") % (obj.partner_id.name), context=context)
account_analytic_account()
diff --git a/addons/analytic/analytic_view.xml b/addons/analytic/analytic_view.xml
index c1e59cf3ba3..704d9bb8244 100644
--- a/addons/analytic/analytic_view.xml
+++ b/addons/analytic/analytic_view.xml
@@ -8,12 +8,18 @@
diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py
index b6eacda63c0..7e9bbacab13 100644
--- a/addons/base_action_rule/base_action_rule.py
+++ b/addons/base_action_rule/base_action_rule.py
@@ -302,33 +302,27 @@ the rule to mark CC(mail to any other person defined in actions)."),
return self.format_body(body % data)
def email_send(self, cr, uid, obj, emails, body, emailfrom=None, context=None):
- """ send email
- @param self: The object pointer
- @param cr: the current row, from the database cursor,
- @param uid: the current user’s ID for security checks,
- @param email: pass the emails
- @param emailfrom: Pass name the email From else False
- @param context: A standard dictionary for contextual values """
-
if not emailfrom:
- emailfrom = tools.config.get('email_from', False)
-
- if context is None:
- context = {}
-
- mail_message = self.pool.get('mail.message')
+ emailfrom = tools.config.get('email_from')
body = self.format_mail(obj, body)
- if not emailfrom:
- if hasattr(obj, 'user_id') and obj.user_id and obj.user_id.email:
- emailfrom = obj.user_id.email
-
- name = '[%d] %s' % (obj.id, tools.ustr(obj.name))
+ if not emailfrom and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.email:
+ emailfrom = obj.user_id.email
emailfrom = tools.ustr(emailfrom)
reply_to = emailfrom
if not emailfrom:
raise osv.except_osv(_('Error!'),
- _("No email ID found for your company address."))
- return mail_message.schedule_with_attach(cr, uid, emailfrom, emails, name, body, model='base.action.rule', reply_to=reply_to, res_id=obj.id)
+ _("Missing default email address or missing email on responsible user"))
+ return self.pool.get('mail.mail').create(cr, uid,
+ { 'email_from': emailfrom,
+ 'email_to': emails.join(','),
+ 'reply_to': reply_to,
+ 'state': 'outgoing',
+ 'subject': '[%d] %s' % (obj.id, tools.ustr(obj.name)),
+ 'body_html': '%s ' % body,
+ 'res_id': obj.id,
+ 'model': obj._table_name,
+ 'auto_delete': True
+ }, context=context)
def do_check(self, cr, uid, action, obj, context=None):
@@ -438,11 +432,8 @@ the rule to mark CC(mail to any other person defined in actions)."),
if len(emails) and action.act_mail_body:
emails = list(set(emails))
email_from = safe_eval(action.act_email_from, {}, locals_for_emails)
-
- def to_email(text):
- return re.findall(r'([^ ,<@]+@[^> ,]+)', text or '')
- emails = to_email(','.join(filter(None, emails)))
- email_froms = to_email(email_from)
+ emails = tools.email_split(','.join(filter(None, emails)))
+ email_froms = tools.email_split(email_from)
if email_froms:
self.email_send(cr, uid, obj, emails, action.act_mail_body, emailfrom=email_froms[0])
return True
diff --git a/addons/base_action_rule/i18n/nb.po b/addons/base_action_rule/i18n/nb.po
new file mode 100644
index 00000000000..7aae94c417f
--- /dev/null
+++ b/addons/base_action_rule/i18n/nb.po
@@ -0,0 +1,536 @@
+# Norwegian Bokmal 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 , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-03 16:43+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-04 04:52+0000\n"
+"X-Generator: Launchpad (build 15890)\n"
+
+#. module: base_action_rule
+#: help:base.action.rule,act_mail_to_user:0
+msgid ""
+"Check this if you want the rule to send an email to the responsible person."
+msgstr ""
+"Sjekk dette hvis du vil at regelen skal sende en e-post til ansvarlig person."
+
+#. module: base_action_rule
+#: field:base.action.rule,act_remind_partner:0
+msgid "Remind Partner"
+msgstr "Påminn partner"
+
+#. module: base_action_rule
+#: field:base.action.rule,trg_partner_categ_id:0
+msgid "Partner Category"
+msgstr "Partner Kategori"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_mail_to_watchers:0
+msgid "Mail to Watchers (CC)"
+msgstr "Post til overvåkere (CC)"
+
+#. module: base_action_rule
+#: field:base.action.rule,trg_state_to:0
+msgid "Button Pressed"
+msgstr "Knapp trykket"
+
+#. module: base_action_rule
+#: field:base.action.rule,model_id:0
+msgid "Object"
+msgstr "Objekt"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_mail_to_email:0
+msgid "Mail to these Emails"
+msgstr "Send mail til disse e-postene"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_state:0
+msgid "Set State to"
+msgstr "Still stat til"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_email_from:0
+msgid "Email From"
+msgstr "E-post fra"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Email Body"
+msgstr "E-post kropp"
+
+#. module: base_action_rule
+#: selection:base.action.rule,trg_date_range_type:0
+msgid "Days"
+msgstr "Dager"
+
+#. module: base_action_rule
+#: field:base.action.rule,last_run:0
+msgid "Last Run"
+msgstr "Siste kjøring"
+
+#. module: base_action_rule
+#: code:addons/base_action_rule/base_action_rule.py:328
+#, python-format
+msgid "Error!"
+msgstr "Feil!"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_reply_to:0
+msgid "Reply-To"
+msgstr "Svar til"
+
+#. module: base_action_rule
+#: help:base.action.rule,act_email_cc:0
+msgid ""
+"These people will receive a copy of the future communication between partner "
+"and users by email"
+msgstr ""
+"Disse menneskene vil motta en kopi av den fremtidige kommunikasjon mellom "
+"partner og brukere av e-post"
+
+#. module: base_action_rule
+#: selection:base.action.rule,trg_date_range_type:0
+msgid "Minutes"
+msgstr "Minutter"
+
+#. module: base_action_rule
+#: field:base.action.rule,name:0
+msgid "Rule Name"
+msgstr "Regelnavn"
+
+#. module: base_action_rule
+#: help:base.action.rule,act_remind_partner:0
+msgid ""
+"Check this if you want the rule to send a reminder by email to the partner."
+msgstr ""
+"Sjekk dette hvis du vil at regelen skal sende en påminnelse via e-post til "
+"partneren."
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Conditions on Model Partner"
+msgstr "Forholdene på Modell Partner"
+
+#. module: base_action_rule
+#: selection:base.action.rule,trg_date_type:0
+msgid "Deadline"
+msgstr "Frist"
+
+#. module: base_action_rule
+#: field:base.action.rule,trg_partner_id:0
+msgid "Partner"
+msgstr "Partner"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "%(object_subject)s = Object subject"
+msgstr "%(object_subject)s = Object subject"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Email Reminders"
+msgstr "E-post påminnelser"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Special Keywords to be Used in the Body"
+msgstr ""
+
+#. module: base_action_rule
+#: field:base.action.rule,trg_state_from:0
+msgid "State"
+msgstr "Stat"
+
+#. module: base_action_rule
+#: model:ir.actions.act_window,help:base_action_rule.base_action_rule_act
+msgid ""
+"Use automated actions to automatically trigger actions for various screens. "
+"Example: a lead created by a specific user may be automatically set to a "
+"specific sales team, or an opportunity which still has status pending after "
+"14 days might trigger an automatic reminder email."
+msgstr ""
+"Bruke automatiserte tiltak for å automatisk utløse tiltak for ulike "
+"skjermer. Eksempel: en leder er opprettet av en bestemt bruker kan bli satt "
+"automatisk til en bestemt salgsteam, eller en mulighet som fortsatt har "
+"status påvente etter 14 dager kan utløse en automatisk påminnelse e-post."
+
+#. module: base_action_rule
+#: help:base.action.rule,act_mail_to_email:0
+msgid "Email-id of the persons whom mail is to be sent"
+msgstr "E-post ID av personer som post skal sendes"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Action Rule"
+msgstr "Handling regel"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Fields to Change"
+msgstr "Felter å endre"
+
+#. module: base_action_rule
+#: selection:base.action.rule,trg_date_type:0
+msgid "Creation Date"
+msgstr "Opprettelsesdato"
+
+#. module: base_action_rule
+#: selection:base.action.rule,trg_date_type:0
+msgid "Last Action Date"
+msgstr "Siste handlingsdato"
+
+#. module: base_action_rule
+#: selection:base.action.rule,trg_date_range_type:0
+msgid "Hours"
+msgstr "Timer"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "%(object_id)s = Object ID"
+msgstr "%(object_ID)s = Object ID"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Delay After Trigger Date"
+msgstr "Forsinkelse Etter utløser Dato"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_remind_attach:0
+msgid "Remind with Attachment"
+msgstr "Minn med vedlegg"
+
+#. module: base_action_rule
+#: constraint:ir.cron:0
+msgid "Invalid arguments"
+msgstr "Ugyldige argumenter"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_user_id:0
+msgid "Set Responsible to"
+msgstr "Satt Ansvarlig for å"
+
+#. module: base_action_rule
+#: selection:base.action.rule,trg_date_type:0
+msgid "None"
+msgstr "Ingen"
+
+#. module: base_action_rule
+#: help:base.action.rule,act_email_to:0
+msgid ""
+"Use a python expression to specify the right field on which one than we will "
+"use for the 'To' field of the header"
+msgstr ""
+"Bruk en python uttrykk for å angi høyre feltet på hvilken enn vi vil bruke "
+"for Til-feltet på header."
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "%(object_user_phone)s = Responsible phone"
+msgstr "% (object_bruker_telefonen) s = Ansvarlig telefon"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid ""
+"The rule uses the AND operator. The model must match all non-empty fields so "
+"that the rule executes the action described in the 'Actions' tab."
+msgstr ""
+"Regelen bruker AND-operatoren. Modellen må matche alle-ikke tomme felt, slik "
+"at regelen utfører handlingen som er beskrevet i \"Handlinger\"-fanen."
+
+#. module: base_action_rule
+#: field:base.action.rule,trg_date_range_type:0
+msgid "Delay type"
+msgstr "forsinkelse typen"
+
+#. module: base_action_rule
+#: help:base.action.rule,regex_name:0
+msgid ""
+"Regular expression for matching name of the resource\n"
+"e.g.: 'urgent.*' will search for records having name starting with the "
+"string 'urgent'\n"
+"Note: This is case sensitive search."
+msgstr ""
+"Regulært uttrykk for matchende navnet på ressursen\n"
+"f.eks: \". haster * 'vil søke etter poster som har navn som starter med "
+"strengen\" haster \"\n"
+"Merk: Dette er små bokstaver søk."
+
+#. module: base_action_rule
+#: field:base.action.rule,act_method:0
+msgid "Call Object Method"
+msgstr "Kall objektmetode"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_email_to:0
+msgid "Email To"
+msgstr "E-post til."
+
+#. module: base_action_rule
+#: help:base.action.rule,act_mail_to_watchers:0
+msgid ""
+"Check this if you want the rule to mark CC(mail to any other person defined "
+"in actions)."
+msgstr ""
+"Sjekk dette hvis du vil at regelen skal merkes CC (mail til en annen person "
+"som er definert i handlinger)."
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "%(partner)s = Partner name"
+msgstr "%(partner)s = Navn på partner"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Note"
+msgstr "Notat"
+
+#. module: base_action_rule
+#: help:base.action.rule,act_email_from:0
+msgid ""
+"Use a python expression to specify the right field on which one than we will "
+"use for the 'From' field of the header"
+msgstr ""
+"Bruk en python uttrykk for å angi høyre feltet på hvilken enn vi vil bruke "
+"for Fra-feltet på overskriften"
+
+#. module: base_action_rule
+#: field:base.action.rule,trg_date_range:0
+msgid "Delay after trigger date"
+msgstr "Forsinkelse etter triggerdato"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Conditions"
+msgstr "Betingelser"
+
+#. module: base_action_rule
+#: help:base.action.rule,trg_date_range:0
+msgid ""
+"Delay After Trigger Date,specifies you can put a negative number. If you "
+"need a delay before the trigger date, like sending a reminder 15 minutes "
+"before a meeting."
+msgstr ""
+"Forsinkelse Etter utløser Dato, spesifiserer du kan sette et negativt tall. "
+"Hvis du trenger en forsinkelse før avtrekkeren dato, som å sende en "
+"påminnelse 15 minutter før et møte."
+
+#. module: base_action_rule
+#: field:base.action.rule,active:0
+msgid "Active"
+msgstr "Aktiv"
+
+#. module: base_action_rule
+#: code:addons/base_action_rule/base_action_rule.py:329
+#, python-format
+msgid "No Email ID Found for your Company address!"
+msgstr ""
+
+#. module: base_action_rule
+#: field:base.action.rule,act_remind_user:0
+msgid "Remind Responsible"
+msgstr "Minn Ansvarlig"
+
+#. module: base_action_rule
+#: help:base.action.rule,sequence:0
+msgid "Gives the sequence order when displaying a list of rules."
+msgstr "Gir sekvens ordre når du viser en liste over regler."
+
+#. module: base_action_rule
+#: selection:base.action.rule,trg_date_range_type:0
+msgid "Months"
+msgstr "Måneder"
+
+#. module: base_action_rule
+#: field:base.action.rule,filter_id:0
+msgid "Filter"
+msgstr "Filtrer"
+
+#. module: base_action_rule
+#: selection:base.action.rule,trg_date_type:0
+msgid "Date"
+msgstr "Dato"
+
+#. module: base_action_rule
+#: help:base.action.rule,server_action_id:0
+msgid ""
+"Describes the action name.\n"
+"eg:on which object which action to be taken on basis of which condition"
+msgstr ""
+"Beskriver handlingens navn.\n"
+"f.eks: på hvilket objekt som tiltak som skal iverksettes på grunnlag av "
+"hvilken tilstand"
+
+#. module: base_action_rule
+#: model:ir.model,name:base_action_rule.model_ir_cron
+msgid "ir.cron"
+msgstr "ir.actions.actions"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "%(object_description)s = Object description"
+msgstr "% (object_beskrivelse) s = Object beskrivelse"
+
+#. module: base_action_rule
+#: constraint:base.action.rule:0
+msgid "Error: The mail is not well formated"
+msgstr "Feil: E-posten er ikke godt nok formatert"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Email Actions"
+msgstr "E-post handlinger"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Email Information"
+msgstr "E-post Informasjon"
+
+#. module: base_action_rule
+#: model:ir.model,name:base_action_rule.model_base_action_rule
+msgid "Action Rules"
+msgstr "Handlingsregler"
+
+#. module: base_action_rule
+#: help:base.action.rule,act_mail_body:0
+msgid "Content of mail"
+msgstr "Innholdet av post"
+
+#. module: base_action_rule
+#: field:base.action.rule,trg_user_id:0
+msgid "Responsible"
+msgstr "Ansvarlig"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "%(partner_email)s = Partner Email"
+msgstr "% (partner_e-post) s = Partner E-post"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "%(object_date)s = Creation date"
+msgstr "%(object_dato)s = opprettelsesdato"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "%(object_user_email)s = Responsible Email"
+msgstr "% (object_brukerens_e-post) s = Ansvarlig e-post"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_mail_body:0
+msgid "Mail body"
+msgstr "Mail kropp"
+
+#. module: base_action_rule
+#: help:base.action.rule,act_remind_user:0
+msgid ""
+"Check this if you want the rule to send a reminder by email to the user."
+msgstr ""
+"Kryss av her hvis du vil at regelen skal sende en påminnelse til brukeren "
+"via e-post."
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Server Action to be Triggered"
+msgstr "Server Tiltak som skal Utløses"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_mail_to_user:0
+msgid "Mail to Responsible"
+msgstr "send mail til ansvarlig"
+
+#. module: base_action_rule
+#: field:base.action.rule,act_email_cc:0
+msgid "Add Watchers (Cc)"
+msgstr "Legg til overvåkere (Cc)"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Conditions on Model Fields"
+msgstr "Forholdene på Modell Felter"
+
+#. module: base_action_rule
+#: model:ir.actions.act_window,name:base_action_rule.base_action_rule_act
+#: model:ir.ui.menu,name:base_action_rule.menu_base_action_rule_form
+msgid "Automated Actions"
+msgstr "Automatiserte handinger"
+
+#. module: base_action_rule
+#: field:base.action.rule,server_action_id:0
+msgid "Server Action"
+msgstr "Tjenerhandling"
+
+#. module: base_action_rule
+#: field:base.action.rule,regex_name:0
+msgid "Regex on Resource Name"
+msgstr "Regex på Ressursnavn"
+
+#. module: base_action_rule
+#: help:base.action.rule,act_remind_attach:0
+msgid ""
+"Check this if you want that all documents attached to the object be attached "
+"to the reminder email sent."
+msgstr ""
+"Kryss av her om du vil at alle dokumenter knyttet til objektet festes til "
+"påminnelse e-post sendt."
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Conditions on Timing"
+msgstr "Vilkår for timing"
+
+#. module: base_action_rule
+#: field:base.action.rule,sequence:0
+msgid "Sequence"
+msgstr "Sekvens"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Actions"
+msgstr "Handlinger"
+
+#. module: base_action_rule
+#: help:base.action.rule,active:0
+msgid ""
+"If the active field is set to False, it will allow you to hide the rule "
+"without removing it."
+msgstr ""
+"Hvis det aktive feltet er satt til False, vil det tillate deg å skjule "
+"regelen uten å fjerne den."
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "%(object_user)s = Responsible name"
+msgstr "% (object_bruker) s = Ansvarlig navn"
+
+#. module: base_action_rule
+#: field:base.action.rule,create_date:0
+msgid "Create Date"
+msgstr "Opprettet dato"
+
+#. module: base_action_rule
+#: view:base.action.rule:0
+msgid "Conditions on States"
+msgstr "Vilkår for tilstander"
+
+#. module: base_action_rule
+#: field:base.action.rule,trg_date_type:0
+msgid "Trigger Date"
+msgstr "Uttløser dato"
+
+#~ msgid "Special Keywords to Be Used in The Body"
+#~ msgstr "Spesielle nøkkelord for bruk i meldingsinnhold"
+
+#, python-format
+#~ msgid "No E-Mail ID Found for your Company address!"
+#~ msgstr "Ingen E-post ID funnet for din firmaadresse!"
diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py
index be3244f8a5b..a6d5f6859af 100644
--- a/addons/base_calendar/base_calendar.py
+++ b/addons/base_calendar/base_calendar.py
@@ -471,18 +471,10 @@ property or property parameter."),
def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context=None):
"""
Send mail for event invitation to event attendees.
- @param cr: the current row, from the database cursor,
- @param uid: the current user’s ID for security checks,
- @param ids: List of attendee’s IDs.
@param email_from: Email address for user sending the mail
- @param context: A standard dictionary for contextual values
@return: True
"""
- if context is None:
- context = {}
-
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
- mail_message = self.pool.get('mail.message')
for att in self.browse(cr, uid, ids, context=context):
sign = att.sent_by_uid and att.sent_by_uid.signature or ''
sign = ' '.join(sign and sign.split('\n') or [])
@@ -508,17 +500,18 @@ property or property parameter."),
}
body = html_invitation % body_vals
if mail_to and email_from:
- attach = self.get_ics_file(cr, uid, res_obj, context=context)
- mail_message.schedule_with_attach(cr, uid,
- email_from,
- mail_to,
- sub,
- body,
- attachments=attach and {'invitation.ics': attach} or None,
- content_subtype='html',
- reply_to=email_from,
- context=context
- )
+ ics_file = self.get_ics_file(cr, uid, res_obj, context=context)
+ vals = {'email_from': email_from,
+ 'email_to': mail_to,
+ 'state': 'outgoing',
+ 'subject': sub,
+ 'body_html': body,
+ 'auto_delete': True}
+ if ics_file:
+ vals['attachment_ids'] = [(0,0,{'name': 'invitation.ics',
+ 'datas_fname': 'invitation.ics',
+ 'datas': str(ics_file).encode('base64')})]
+ self.pool.get('mail.mail').create(cr, uid, vals, context=context)
return True
def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
@@ -812,7 +805,6 @@ class calendar_alarm(osv.osv):
"""
if context is None:
context = {}
- mail_message = self.pool.get('mail.message')
current_datetime = datetime.now()
alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
@@ -849,36 +841,10 @@ class calendar_alarm(osv.osv):
else:
re_dates = [alarm.trigger_date]
- for r_date in re_dates:
- ref = alarm.model_id.model + ',' + str(alarm.res_id)
-
- # search for alreay sent requests
- #if request_obj.search(cr, uid, [('trigger_date', '=', r_date), ('ref_doc1', '=', ref)], context=context):
- #continue
-
- # Deactivated because of the removing of res.request
- # TODO: when cleaning calendar module, re-add this in a new mechanism
- #if alarm.action == 'display':
- #value = {
- #'name': alarm.name,
- #'act_from': alarm.user_id.id,
- #'act_to': alarm.user_id.id,
- #'body': alarm.description,
- #'trigger_date': r_date,
- #'ref_doc1': ref
- #}
- #request_id = request_obj.create(cr, uid, value)
- #request_ids = [request_id]
- #for attendee in res_obj.attendee_ids:
- #if attendee.user_id:
- #value['act_to'] = attendee.user_id.id
- #request_id = request_obj.create(cr, uid, value)
- #request_ids.append(request_id)
- #request_obj.request_send(cr, uid, request_ids)
-
+ if re_dates:
if alarm.action == 'email':
- sub = '[Openobject Reminder] %s' % (alarm.name)
- body = """
+ sub = '[OpenERP Reminder] %s' % (alarm.name)
+ body = """
Event: %s
Event Date: %s
Description: %s
@@ -888,20 +854,21 @@ From:
----
%s
-
+
""" % (alarm.name, alarm.trigger_date, alarm.description, \
alarm.user_id.name, alarm.user_id.signature)
mail_to = [alarm.user_id.email]
for att in alarm.attendee_ids:
mail_to.append(att.user_id.email)
if mail_to:
- mail_message.schedule_with_attach(cr, uid,
- tools.config.get('email_from', False),
- mail_to,
- sub,
- body,
- context=context
- )
+ vals = {
+ 'state': 'outgoing',
+ 'subject': sub,
+ 'body_html': body,
+ 'email_to': mail_to,
+ 'email_from': tools.config.get('email_from', mail_to),
+ }
+ self.pool.get('mail.mail').create(cr, uid, vals, context=context)
if next_trigger_date:
update_vals.update({'trigger_date': next_trigger_date})
else:
@@ -1616,36 +1583,6 @@ class calendar_todo(osv.osv):
calendar_todo()
-class ir_attachment(osv.osv):
- _name = 'ir.attachment'
- _inherit = 'ir.attachment'
-
- def search_count(self, cr, user, args, context=None):
- new_args = []
- for domain_item in args:
- if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
- new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
- else:
- new_args.append(domain_item)
- return super(ir_attachment, self).search_count(cr, user, new_args, context)
-
- def create(self, cr, uid, vals, context=None):
- if context:
- id = context.get('default_res_id', False)
- context.update({'default_res_id' : base_calendar_id2real_id(id)})
- return super(ir_attachment, self).create(cr, uid, vals, context=context)
-
- def search(self, cr, uid, args, offset=0, limit=None, order=None,
- context=None, count=False):
- new_args = []
- for domain_item in args:
- if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
- new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
- else:
- new_args.append(domain_item)
- return super(ir_attachment, self).search(cr, uid, new_args, offset=offset,
- limit=limit, order=order, context=context, count=False)
-ir_attachment()
class ir_values(osv.osv):
_inherit = 'ir.values'
diff --git a/addons/base_calendar/crm_meeting.py b/addons/base_calendar/crm_meeting.py
index cb96ff2a4d0..e1c66148ae7 100644
--- a/addons/base_calendar/crm_meeting.py
+++ b/addons/base_calendar/crm_meeting.py
@@ -43,7 +43,7 @@ class crm_meeting(base_state, osv.Model):
_name = 'crm.meeting'
_description = "Meeting"
_order = "id desc"
- _inherit = ["calendar.event", 'ir.needaction_mixin', "mail.thread"]
+ _inherit = ["calendar.event", "mail.thread", 'ir.needaction_mixin']
_columns = {
# base_state required fields
'create_date': fields.datetime('Creation Date', readonly=True),
@@ -70,13 +70,17 @@ class crm_meeting(base_state, osv.Model):
# OpenChatter
# ----------------------------------------
+ # shows events of the day for this user
+ def needaction_domain_get(self, cr, uid, domain=[], context={}):
+ return [('date','<=',time.strftime('%Y-%M-%D 23:59:59')), ('date_deadline','>=', time.strftime('%Y-%M-%D 00:00:00')), ('user_id','=',uid)]
+
def case_get_note_msg_prefix(self, cr, uid, id, context=None):
return 'Meeting'
def case_open_send_note(self, cr, uid, ids, context=None):
- return self.message_append_note(cr, uid, ids, body=_("Meeting has been confirmed ."), context=context)
+ return self.message_post(cr, uid, ids, body=_("Meeting confirmed ."), context=context)
def case_close_send_note(self, cr, uid, ids, context=None):
- return self.message_append_note(cr, uid, ids, body=_("Meeting has been done ."), context=context)
+ return self.message_post(cr, uid, ids, body=_("Meeting completed ."), context=context)
diff --git a/addons/base_calendar/crm_meeting_view.xml b/addons/base_calendar/crm_meeting_view.xml
index 6009251a460..928f8d94d5c 100644
--- a/addons/base_calendar/crm_meeting_view.xml
+++ b/addons/base_calendar/crm_meeting_view.xml
@@ -170,7 +170,8 @@
@@ -182,17 +183,16 @@
+ />
+ type="object" />
+ type="object" />
@@ -200,16 +200,16 @@
+ string="Uncertain" />
+ string="Accept" />
+ string="Decline" />
@@ -244,13 +244,13 @@
CRM - Meetings Tree
crm.meeting
-
+
-
+
@@ -287,9 +287,9 @@
-
+
-
+
diff --git a/addons/base_import/__init__.py b/addons/base_import/__init__.py
new file mode 100644
index 00000000000..939a8173e38
--- /dev/null
+++ b/addons/base_import/__init__.py
@@ -0,0 +1,3 @@
+import controllers
+import models
+import test_models
diff --git a/addons/base_import/__openerp__.py b/addons/base_import/__openerp__.py
new file mode 100644
index 00000000000..eff20bc90ab
--- /dev/null
+++ b/addons/base_import/__openerp__.py
@@ -0,0 +1,39 @@
+{
+ 'name': 'Base import',
+ 'description': """
+New extensible file import for OpenERP
+======================================
+
+Re-implement openerp's file import system:
+
+* Server side, the previous system forces most of the logic into the
+ client which duplicates the effort (between clients), makes the
+ import system much harder to use without a client (direct RPC or
+ other forms of automation) and makes knowledge about the
+ import/export system much harder to gather as it is spread over
+ 3+ different projects.
+
+* In a more extensible manner, so users and partners can build their
+ own front-end to import from other file formats (e.g. OpenDocument
+ files) which may be simpler to handle in their work flow or from
+ their data production sources.
+
+* In a module, so that administrators and users of OpenERP who do not
+ need or want an online import can avoid it being available to users.
+""",
+ 'category': 'Uncategorized',
+ 'website': 'http://www.openerp.com',
+ 'author': 'OpenERP SA',
+ 'depends': ['base'],
+ 'installable': True,
+ 'auto_install': False, # set to true and allow uninstall?
+ 'css': [
+ 'static/lib/select2/select2.css',
+ 'static/src/css/import.css',
+ ],
+ 'js': [
+ 'static/lib/select2/select2.js',
+ 'static/src/js/import.js',
+ ],
+ 'qweb': ['static/src/xml/import.xml'],
+}
diff --git a/addons/base_import/controllers.py b/addons/base_import/controllers.py
new file mode 100644
index 00000000000..85ff993e92e
--- /dev/null
+++ b/addons/base_import/controllers.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+import simplejson
+
+try:
+ import openerp.addons.web.common.http as openerpweb
+except ImportError:
+ import web.common.http as openerpweb
+
+class ImportController(openerpweb.Controller):
+ _cp_path = '/base_import'
+
+ @openerpweb.httprequest
+ def set_file(self, req, file, import_id, jsonp='callback'):
+ import_id = int(import_id)
+
+ written = req.session.model('base_import.import').write(import_id, {
+ 'file': file.read(),
+ 'file_name': file.filename,
+ 'file_type': file.content_type,
+ }, req.session.eval_context(req.context))
+
+ return 'window.top.%s(%s)' % (
+ jsonp, simplejson.dumps({'result': written}))
diff --git a/addons/base_import/models.py b/addons/base_import/models.py
new file mode 100644
index 00000000000..1e5ad002b84
--- /dev/null
+++ b/addons/base_import/models.py
@@ -0,0 +1,352 @@
+import csv
+import itertools
+import logging
+import operator
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+import psycopg2
+
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+
+FIELDS_RECURSION_LIMIT = 2
+ERROR_PREVIEW_BYTES = 200
+_logger = logging.getLogger(__name__)
+class ir_import(orm.TransientModel):
+ _name = 'base_import.import'
+ # allow imports to survive for 12h in case user is slow
+ _transient_max_hours = 12.0
+
+ _columns = {
+ 'res_model': fields.char('Model', size=64),
+ 'file': fields.binary(
+ 'File', help="File to check and/or import, raw binary (not base64)"),
+ 'file_name': fields.char('File Name', size=None),
+ 'file_type': fields.char('File Type', size=None),
+ }
+
+ def get_fields(self, cr, uid, model, context=None,
+ depth=FIELDS_RECURSION_LIMIT):
+ """ Recursively get fields for the provided model (through
+ fields_get) and filter them according to importability
+
+ The output format is a list of ``Field``, with ``Field``
+ defined as:
+
+ .. class:: Field
+
+ .. attribute:: id (str)
+
+ A non-unique identifier for the field, used to compute
+ the span of the ``required`` attribute: if multiple
+ ``required`` fields have the same id, only one of them
+ is necessary.
+
+ .. attribute:: name (str)
+
+ The field's logical (OpenERP) name within the scope of
+ its parent.
+
+ .. attribute:: string (str)
+
+ The field's human-readable name (``@string``)
+
+ .. attribute:: required (bool)
+
+ Whether the field is marked as required in the
+ model. Clients must provide non-empty import values
+ for all required fields or the import will error out.
+
+ .. attribute:: fields (list(Field))
+
+ The current field's subfields. The database and
+ external identifiers for m2o and m2m fields; a
+ filtered and transformed fields_get for o2m fields (to
+ a variable depth defined by ``depth``).
+
+ Fields with no sub-fields will have an empty list of
+ sub-fields.
+
+ :param str model: name of the model to get fields form
+ :param int landing: depth of recursion into o2m fields
+ """
+ fields = [{
+ 'id': 'id',
+ 'name': 'id',
+ 'string': _("External ID"),
+ 'required': False,
+ 'fields': [],
+ }]
+ fields_got = self.pool[model].fields_get(cr, uid, context=context)
+ for name, field in fields_got.iteritems():
+ if field.get('readonly'):
+ states = field.get('states')
+ if not states:
+ continue
+ # states = {state: [(attr, value), (attr2, value2)], state2:...}
+ if not any(attr == 'readonly' and value is False
+ for attr, value in itertools.chain.from_iterable(
+ states.itervalues())):
+ continue
+
+ f = {
+ 'id': name,
+ 'name': name,
+ 'string': field['string'],
+ # Y U NO ALWAYS HAVE REQUIRED
+ 'required': bool(field.get('required')),
+ 'fields': [],
+ }
+
+ if field['type'] in ('many2many', 'many2one'):
+ f['fields'] = [
+ dict(f, name='id', string=_("External ID")),
+ dict(f, name='.id', string=_("Database ID")),
+ ]
+ elif field['type'] == 'one2many' and depth:
+ f['fields'] = self.get_fields(
+ cr, uid, field['relation'], context=context, depth=depth-1)
+
+ fields.append(f)
+
+ # TODO: cache on model?
+ return fields
+
+ def _read_csv(self, record, options):
+ """ Returns a CSV-parsed iterator of all empty lines in the file
+
+ :throws csv.Error: if an error is detected during CSV parsing
+ :throws UnicodeDecodeError: if ``options.encoding`` is incorrect
+ """
+ csv_iterator = csv.reader(
+ StringIO(record.file),
+ quotechar=options['quoting'],
+ delimiter=options['separator'])
+ csv_nonempty = itertools.ifilter(None, csv_iterator)
+ # TODO: guess encoding with chardet? Or https://github.com/aadsm/jschardet
+ encoding = options.get('encoding', 'utf-8')
+ return itertools.imap(
+ lambda row: [item.decode(encoding) for item in row],
+ csv_nonempty)
+
+ def _match_header(self, header, fields, options):
+ """ Attempts to match a given header to a field of the
+ imported model.
+
+ :param str header: header name from the CSV file
+ :param fields:
+ :param dict options:
+ :returns: an empty list if the header couldn't be matched, or
+ all the fields to traverse
+ :rtype: list(Field)
+ """
+ for field in fields:
+ # FIXME: should match all translations & original
+ # TODO: use string distance (levenshtein? hamming?)
+ if header == field['name'] \
+ or header.lower() == field['string'].lower():
+ return [field]
+
+ if '/' not in header:
+ return []
+
+ # relational field path
+ traversal = []
+ subfields = fields
+ # Iteratively dive into fields tree
+ for section in header.split('/'):
+ # Strip section in case spaces are added around '/' for
+ # readability of paths
+ match = self._match_header(section.strip(), subfields, options)
+ # Any match failure, exit
+ if not match: return []
+ # prep subfields for next iteration within match[0]
+ field = match[0]
+ subfields = field['fields']
+ traversal.append(field)
+ return traversal
+
+ def _match_headers(self, rows, fields, options):
+ """ Attempts to match the imported model's fields to the
+ titles of the parsed CSV file, if the file is supposed to have
+ headers.
+
+ Will consume the first line of the ``rows`` iterator.
+
+ Returns a pair of (None, None) if headers were not requested
+ or the list of headers and a dict mapping cell indices
+ to key paths in the ``fields`` tree
+
+ :param Iterator rows:
+ :param dict fields:
+ :param dict options:
+ :rtype: (None, None) | (list(str), dict(int: list(str)))
+ """
+ if not options.get('headers'):
+ return None, None
+
+ headers = next(rows)
+ return headers, dict(
+ (index, [field['name'] for field in self._match_header(header, fields, options)] or None)
+ for index, header in enumerate(headers)
+ )
+
+ def parse_preview(self, cr, uid, id, options, count=10, context=None):
+ """ Generates a preview of the uploaded files, and performs
+ fields-matching between the import's file data and the model's
+ columns.
+
+ If the headers are not requested (not options.headers),
+ ``matches`` and ``headers`` are both ``False``.
+
+ :param id: identifier of the import
+ :param int count: number of preview lines to generate
+ :param options: format-specific options.
+ CSV: {encoding, quoting, separator, headers}
+ :type options: {str, str, str, bool}
+ :returns: {fields, matches, headers, preview} | {error, preview}
+ :rtype: {dict(str: dict(...)), dict(int, list(str)), list(str), list(list(str))} | {str, str}
+ """
+ (record,) = self.browse(cr, uid, [id], context=context)
+ fields = self.get_fields(cr, uid, record.res_model, context=context)
+
+ try:
+ rows = self._read_csv(record, options)
+
+ headers, matches = self._match_headers(rows, fields, options)
+ # Match should have consumed the first row (iif headers), get
+ # the ``count`` next rows for preview
+ preview = itertools.islice(rows, count)
+ return {
+ 'fields': fields,
+ 'matches': matches or False,
+ 'headers': headers or False,
+ 'preview': list(preview),
+ }
+ except Exception, e:
+ # Due to lazy generators, UnicodeDecodeError (for
+ # instance) may only be raised when serializing the
+ # preview to a list in the return.
+ _logger.debug("Error during CSV parsing preview", exc_info=True)
+ return {
+ 'error': str(e),
+ # iso-8859-1 ensures decoding will always succeed,
+ # even if it yields non-printable characters. This is
+ # in case of UnicodeDecodeError (or csv.Error
+ # compounded with UnicodeDecodeError)
+ 'preview': record.file[:ERROR_PREVIEW_BYTES]
+ .decode( 'iso-8859-1'),
+ }
+
+ def _convert_import_data(self, record, fields, options, context=None):
+ """ Extracts the input browse_record and fields list (with
+ ``False``-y placeholders for fields to *not* import) into a
+ format Model.import_data can use: a fields list without holes
+ and the precisely matching data matrix
+
+ :param browse_record record:
+ :param list(str|bool): fields
+ :returns: (data, fields)
+ :rtype: (list(list(str)), list(str))
+ :raises ValueError: in case the import data could not be converted
+ """
+ # Get indices for non-empty fields
+ indices = [index for index, field in enumerate(fields) if field]
+ if not indices:
+ raise ValueError(_("You must configure at least one field to import"))
+ # If only one index, itemgetter will return an atom rather
+ # than a 1-tuple
+ if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
+ else: mapper = operator.itemgetter(*indices)
+ # Get only list of actually imported fields
+ import_fields = filter(None, fields)
+
+ rows_to_import = self._read_csv(record, options)
+ if options.get('headers'):
+ rows_to_import = itertools.islice(
+ rows_to_import, 1, None)
+ data = [
+ row for row in itertools.imap(mapper, rows_to_import)
+ # don't try inserting completely empty rows (e.g. from
+ # filtering out o2m fields)
+ if any(row)
+ ]
+
+ return data, import_fields
+
+ def do(self, cr, uid, id, fields, options, dryrun=False, context=None):
+ """ Actual execution of the import
+
+ :param fields: import mapping: maps each column to a field,
+ ``False`` for the columns to ignore
+ :type fields: list(str|bool)
+ :param dict options:
+ :param bool dryrun: performs all import operations (and
+ validations) but rollbacks writes, allows
+ getting as much errors as possible without
+ the risk of clobbering the database.
+ :returns: A list of errors. If the list is empty the import
+ executed fully and correctly. If the list is
+ non-empty it contains dicts with 3 keys ``type`` the
+ type of error (``error|warning``); ``message`` the
+ error message associated with the error (a string)
+ and ``record`` the data which failed to import (or
+ ``false`` if that data isn't available or provided)
+ :rtype: list({type, message, record})
+ """
+ cr.execute('SAVEPOINT import')
+
+ (record,) = self.browse(cr, uid, [id], context=context)
+ try:
+ data, import_fields = self._convert_import_data(
+ record, fields, options, context=context)
+ except ValueError, e:
+ return [{
+ 'type': 'error',
+ 'message': str(e),
+ 'record': False,
+ }]
+
+ try:
+ _logger.info('importing %d rows...', len(data))
+ (code, record, message, _wat) = self.pool[record.res_model].import_data(
+ cr, uid, import_fields, data, context=context)
+ _logger.info('done')
+
+ except Exception, e:
+ _logger.exception("Import failed")
+ # TODO: remove when exceptions stop being an "expected"
+ # behavior of import_data on some (most) invalid
+ # input.
+ code, record, message = -1, None, str(e)
+
+ # If transaction aborted, RELEASE SAVEPOINT is going to raise
+ # an InternalError (ROLLBACK should work, maybe). Ignore that.
+ # TODO: to handle multiple errors, create savepoint around
+ # write and release it in case of write error (after
+ # adding error to errors array) => can keep on trying to
+ # import stuff, and rollback at the end if there is any
+ # error in the results.
+ try:
+ if dryrun:
+ cr.execute('ROLLBACK TO SAVEPOINT import')
+ else:
+ cr.execute('RELEASE SAVEPOINT import')
+ except psycopg2.InternalError:
+ pass
+
+ if code != -1:
+ return []
+
+ # TODO: add key for error location?
+ # TODO: error not within normal preview, how to display? Re-preview
+ # with higher ``count``?
+ return [{
+ 'type': 'error',
+ 'message': message,
+ 'record': record or False
+ }]
diff --git a/addons/base_import/static/lib/select2/LICENSE b/addons/base_import/static/lib/select2/LICENSE
new file mode 100644
index 00000000000..627fddef413
--- /dev/null
+++ b/addons/base_import/static/lib/select2/LICENSE
@@ -0,0 +1,12 @@
+Copyright 2012 Igor Vaynberg
+
+Version: @@ver@@ Timestamp: @@timestamp@@
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
+compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is
+distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and limitations under the License.
\ No newline at end of file
diff --git a/addons/base_import/static/lib/select2/README.md b/addons/base_import/static/lib/select2/README.md
new file mode 100755
index 00000000000..3a6dcaf1ba4
--- /dev/null
+++ b/addons/base_import/static/lib/select2/README.md
@@ -0,0 +1,68 @@
+Select2
+=================
+
+Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results. Look and feel of Select2 is based on the excellent [Chosen](http://harvesthq.github.com/chosen/) library.
+
+To get started -- checkout http://ivaynberg.github.com/select2!
+
+What Does Select2 Support That Chosen Does Not?
+-------------------------------------------------
+
+* Working with large datasets: Chosen requires the entire dataset to be loaded as `option` tags in the DOM, which limits
+it to working with small-ish datasets. Select2 uses a function to find results on-the-fly, which allows it to partially
+load results.
+* Paging of results: Since Select2 works with large datasets and only loads a small amount of matching results at a time
+it has to support paging. Select2 will call the search function when the user scrolls to the bottom of currently loaded
+result set allowing for the 'infinite scrolling' of results.
+* Custom markup for results: Chosen only supports rendering text results because that is the only markup supported by
+`option` tags. Select2 provides an extension point which can be used to produce any kind of markup to represent results.
+* Ability to add results on the fly: Select2 provides the ability to add results from the search term entered by the user, which allows it to be used for
+tagging.
+
+Browser Compatibility
+--------------------
+* IE 8+ (7 mostly works except for [issue with z-index](https://github.com/ivaynberg/select2/issues/37))
+* Chrome 8+
+* Firefox 3.5+
+* Safari 3+
+* Opera 10.6+
+
+Integrations
+------------
+
+* [Wicket-Select2](https://github.com/ivaynberg/wicket-select2) (Java / Apache Wicket)
+* [select2-rails](https://github.com/argerim/select2-rails) (Ruby on Rails)
+* [AngularUI](http://angular-ui.github.com/#directives-select2) ([AngularJS](angularjs.org))
+* [Django](https://github.com/applegrew/django-select2)
+
+Bug tracker
+-----------
+
+Have a bug? Please create an issue here on GitHub!
+
+https://github.com/ivaynberg/select2/issues
+
+
+Mailing list
+------------
+
+Have a question? Ask on our mailing list!
+
+select2@googlegroups.com
+
+https://groups.google.com/d/forum/select2
+
+
+Copyright and License
+---------------------
+
+Copyright 2012 Igor Vaynberg
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
+compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is
+distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and limitations under the License.
\ No newline at end of file
diff --git a/addons/base_import/static/lib/select2/release.sh b/addons/base_import/static/lib/select2/release.sh
new file mode 100755
index 00000000000..79eaabd621d
--- /dev/null
+++ b/addons/base_import/static/lib/select2/release.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+set -e
+
+echo -n "Enter the version for this release: "
+
+read ver
+
+if [ ! $ver ]; then
+ echo "Invalid version."
+ exit
+fi
+
+name="select2"
+js="$name.js"
+mini="$name.min.js"
+css="$name.css"
+release="$name-$ver"
+releasedir="/tmp/$release"
+tag="release-$ver"
+branch="build-$ver"
+curbranch=`git branch | grep "*" | sed "s/* //"`
+timestamp=$(date)
+tokens="s/@@ver@@/$ver/g;s/\@@timestamp@@/$timestamp/g"
+remote="github"
+
+git branch "$branch"
+git checkout "$branch"
+
+echo "Tokenizing..."
+
+find . -name "$js" | xargs sed -i -e "$tokens"
+find . -name "$css" | xargs sed -i -e "$tokens"
+
+git add "$js"
+git add "$css"
+
+echo "Minifying..."
+
+echo "/*" > "$mini"
+cat LICENSE | sed "$tokens" >> "$mini"
+echo "*/" >> "$mini"
+
+curl -s \
+ -d compilation_level=SIMPLE_OPTIMIZATIONS \
+ -d output_format=text \
+ -d output_info=compiled_code \
+ --data-urlencode "js_code@$js" \
+ http://closure-compiler.appspot.com/compile \
+ >> "$mini"
+
+git add "$mini"
+
+git commit -m "release $ver"
+
+echo "Tagging..."
+
+git tag -a "$tag" -m "tagged version $ver"
+git push "$remote" --tags
+
+echo "Archiving..."
+
+rm -rf "$releasedir"
+mkdir "$releasedir"
+
+cp $name.* "$releasedir"
+cp spinner.gif "$releasedir"
+cp README.* "$releasedir"
+
+zip -r "$releasedir.zip" "$releasedir"
+rm -rf "$releasedir"
+
+echo "Cleaning Up..."
+
+git checkout "$curbranch"
+git branch -D "$branch"
+
+echo "Done. Release archive created: $releasedir.zip"
diff --git a/addons/base_import/static/lib/select2/select2.css b/addons/base_import/static/lib/select2/select2.css
new file mode 100755
index 00000000000..f0bdfb82f04
--- /dev/null
+++ b/addons/base_import/static/lib/select2/select2.css
@@ -0,0 +1,524 @@
+/*
+Version: @@ver@@ Timestamp: @@timestamp@@
+*/
+.select2-container {
+ position: relative;
+ display: inline-block;
+ /* inline-block for ie7 */
+ zoom: 1;
+ *display: inline;
+ vertical-align: top;
+}
+
+.select2-container,
+.select2-drop,
+.select2-search,
+.select2-search input{
+ /*
+ Force border-box so that % widths fit the parent
+ container without overlap because of margin/padding.
+
+ More Info : http://www.quirksmode.org/css/box.html
+ */
+ -moz-box-sizing: border-box; /* firefox */
+ -ms-box-sizing: border-box; /* ie */
+ -webkit-box-sizing: border-box; /* webkit */
+ -khtml-box-sizing: border-box; /* konqueror */
+ box-sizing: border-box; /* css3 */
+}
+
+.select2-container .select2-choice {
+ background-color: #fff;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white));
+ background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%);
+ background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%);
+ background-image: -o-linear-gradient(bottom, #eeeeee 0%, #ffffff 50%);
+ background-image: -ms-linear-gradient(top, #eeeeee 0%, #ffffff 50%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#ffffff', GradientType = 0);
+ background-image: linear-gradient(top, #eeeeee 0%, #ffffff 50%);
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ -moz-background-clip: padding;
+ -webkit-background-clip: padding-box;
+ background-clip: padding-box;
+ border: 1px solid #aaa;
+ display: block;
+ overflow: hidden;
+ white-space: nowrap;
+ position: relative;
+ height: 26px;
+ line-height: 26px;
+ padding: 0 0 0 8px;
+ color: #444;
+ text-decoration: none;
+}
+
+.select2-container.select2-drop-above .select2-choice
+{
+ border-bottom-color: #aaa;
+ -webkit-border-radius:0px 0px 4px 4px;
+ -moz-border-radius:0px 0px 4px 4px;
+ border-radius:0px 0px 4px 4px;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.9, white));
+ background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 90%);
+ background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 90%);
+ background-image: -o-linear-gradient(bottom, #eeeeee 0%, white 90%);
+ background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 90%);
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 );
+ background-image: linear-gradient(top, #eeeeee 0%,#ffffff 90%);
+}
+
+.select2-container .select2-choice span {
+ margin-right: 26px;
+ display: block;
+ overflow: hidden;
+ white-space: nowrap;
+ -o-text-overflow: ellipsis;
+ -ms-text-overflow: ellipsis;
+ text-overflow: ellipsis;
+}
+
+.select2-container .select2-choice abbr {
+ display: block;
+ position: absolute;
+ right: 26px;
+ top: 8px;
+ width: 12px;
+ height: 12px;
+ font-size: 1px;
+ background: url('select2.png') right top no-repeat;
+ cursor: pointer;
+ text-decoration: none;
+ border:0;
+ outline: 0;
+}
+.select2-container .select2-choice abbr:hover {
+ background-position: right -11px;
+ cursor: pointer;
+}
+
+.select2-drop {
+ background: #fff;
+ color: #000;
+ border: 1px solid #aaa;
+ border-top: 0;
+ position: absolute;
+ top: 100%;
+ -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+ -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+ -o-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+ box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+ z-index: 9999;
+ width:100%;
+ margin-top:-1px;
+
+ -webkit-border-radius: 0 0 4px 4px;
+ -moz-border-radius: 0 0 4px 4px;
+ border-radius: 0 0 4px 4px;
+}
+
+.select2-drop.select2-drop-above {
+ -webkit-border-radius: 4px 4px 0px 0px;
+ -moz-border-radius: 4px 4px 0px 0px;
+ border-radius: 4px 4px 0px 0px;
+ margin-top:1px;
+ border-top: 1px solid #aaa;
+ border-bottom: 0;
+
+ -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+ -moz-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+ -o-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+ box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+}
+
+.select2-container .select2-choice div {
+ -webkit-border-radius: 0 4px 4px 0;
+ -moz-border-radius: 0 4px 4px 0;
+ border-radius: 0 4px 4px 0;
+ -moz-background-clip: padding;
+ -webkit-background-clip: padding-box;
+ background-clip: padding-box;
+ background: #ccc;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee));
+ background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%);
+ background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%);
+ background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%);
+ background-image: -ms-linear-gradient(top, #cccccc 0%, #eeeeee 60%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#cccccc', endColorstr = '#eeeeee', GradientType = 0);
+ background-image: linear-gradient(top, #cccccc 0%, #eeeeee 60%);
+ border-left: 1px solid #aaa;
+ position: absolute;
+ right: 0;
+ top: 0;
+ display: block;
+ height: 100%;
+ width: 18px;
+}
+
+.select2-container .select2-choice div b {
+ background: url('select2.png') no-repeat 0 1px;
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+.select2-search {
+ display: inline-block;
+ white-space: nowrap;
+ z-index: 10000;
+ min-height: 26px;
+ width: 100%;
+ margin: 0;
+ padding-left: 4px;
+ padding-right: 4px;
+}
+
+.select2-search-hidden {
+ display: block;
+ position: absolute;
+ left: -10000px;
+}
+
+.select2-search input {
+ background: #fff url('select2.png') no-repeat 100% -22px;
+ background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
+ background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+ background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+ background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
+ background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+ background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+ padding: 4px 20px 4px 5px;
+ outline: 0;
+ border: 1px solid #aaa;
+ font-family: sans-serif;
+ font-size: 1em;
+ width:100%;
+ margin:0;
+ height:auto !important;
+ min-height: 26px;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ border-radius: 0;
+ -moz-border-radius: 0;
+ -webkit-border-radius: 0;
+}
+
+.select2-drop.select2-drop-above .select2-search input
+{
+ margin-top:4px;
+}
+
+.select2-search input.select2-active {
+ background: #fff url('spinner.gif') no-repeat 100%;
+ background: url('spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
+ background: url('spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+ background: url('spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+ background: url('spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
+ background: url('spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+ background: url('spinner.gif') no-repeat 100%, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+}
+
+
+.select2-container-active .select2-choice,
+.select2-container-active .select2-choices {
+ -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
+ -moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ -o-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ box-shadow : 0 0 5px rgba(0,0,0,.3);
+ border: 1px solid #5897fb;
+ outline: none;
+}
+
+.select2-dropdown-open .select2-choice {
+ border: 1px solid #aaa;
+ border-bottom-color: transparent;
+ -webkit-box-shadow: 0 1px 0 #fff inset;
+ -moz-box-shadow : 0 1px 0 #fff inset;
+ -o-box-shadow : 0 1px 0 #fff inset;
+ box-shadow : 0 1px 0 #fff inset;
+ background-color: #eee;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee));
+ background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%);
+ background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%);
+ background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%);
+ background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%);
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 );
+ background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%);
+ -webkit-border-bottom-left-radius : 0;
+ -webkit-border-bottom-right-radius: 0;
+ -moz-border-radius-bottomleft : 0;
+ -moz-border-radius-bottomright: 0;
+ border-bottom-left-radius : 0;
+ border-bottom-right-radius: 0;
+}
+
+.select2-dropdown-open .select2-choice div {
+ background: transparent;
+ border-left: none;
+}
+.select2-dropdown-open .select2-choice div b {
+ background-position: -18px 1px;
+}
+
+/* results */
+.select2-results {
+ margin: 4px 4px 4px 0;
+ padding: 0 0 0 4px;
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: auto;
+ max-height: 200px;
+}
+
+.select2-results ul.select2-result-sub {
+ margin: 0 0 0 0;
+}
+
+.select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px }
+.select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 40px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 60px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 80px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 100px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 110px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 120px }
+
+.select2-results li {
+ list-style: none;
+ display: list-item;
+}
+
+.select2-results li.select2-result-with-children > .select2-result-label {
+ font-weight: bold;
+}
+
+.select2-results .select2-result-label {
+ padding: 3px 7px 4px;
+ margin: 0;
+ cursor: pointer;
+}
+
+.select2-results .select2-highlighted {
+ background: #3875d7;
+ color: #fff;
+}
+.select2-results li em {
+ background: #feffde;
+ font-style: normal;
+}
+.select2-results .select2-highlighted em {
+ background: transparent;
+}
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-selection-limit {
+ background: #f4f4f4;
+ display: list-item;
+}
+
+/*
+disabled look for already selected choices in the results dropdown
+.select2-results .select2-disabled.select2-highlighted {
+ color: #666;
+ background: #f4f4f4;
+ display: list-item;
+ cursor: default;
+}
+.select2-results .select2-disabled {
+ background: #f4f4f4;
+ display: list-item;
+ cursor: default;
+}
+*/
+.select2-results .select2-disabled {
+ display: none;
+}
+
+.select2-more-results.select2-active {
+ background: #f4f4f4 url('spinner.gif') no-repeat 100%;
+}
+
+.select2-more-results {
+ background: #f4f4f4;
+ display: list-item;
+}
+
+/* disabled styles */
+
+.select2-container.select2-container-disabled .select2-choice {
+ background-color: #f4f4f4;
+ background-image: none;
+ border: 1px solid #ddd;
+ cursor: default;
+}
+
+.select2-container.select2-container-disabled .select2-choice div {
+ background-color: #f4f4f4;
+ background-image: none;
+ border-left: 0;
+}
+
+
+/* multiselect */
+
+.select2-container-multi .select2-choices {
+ background-color: #fff;
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
+ background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ border: 1px solid #aaa;
+ margin: 0;
+ padding: 0;
+ cursor: text;
+ overflow: hidden;
+ height: auto !important;
+ height: 1%;
+ position: relative;
+}
+
+.select2-container-multi .select2-choices {
+ min-height: 26px;
+}
+
+.select2-container-multi.select2-container-active .select2-choices {
+ -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
+ -moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ -o-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ box-shadow : 0 0 5px rgba(0,0,0,.3);
+ border: 1px solid #5897fb;
+ outline: none;
+}
+.select2-container-multi .select2-choices li {
+ float: left;
+ list-style: none;
+}
+.select2-container-multi .select2-choices .select2-search-field {
+ white-space: nowrap;
+ margin: 0;
+ padding: 0;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input {
+ color: #666;
+ background: transparent !important;
+ font-family: sans-serif;
+ font-size: 100%;
+ height: 15px;
+ padding: 5px;
+ margin: 1px 0;
+ outline: 0;
+ border: 0;
+ -webkit-box-shadow: none;
+ -moz-box-shadow : none;
+ -o-box-shadow : none;
+ box-shadow : none;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input.select2-active {
+ background: #fff url('spinner.gif') no-repeat 100% !important;
+}
+
+.select2-default {
+ color: #999 !important;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice {
+ -webkit-border-radius: 3px;
+ -moz-border-radius : 3px;
+ border-radius : 3px;
+ -moz-background-clip : padding;
+ -webkit-background-clip: padding-box;
+ background-clip : padding-box;
+ background-color: #e4e4e4;
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f4f4', endColorstr='#eeeeee', GradientType=0 );
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
+ background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+ -moz-box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+ box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+ color: #333;
+ border: 1px solid #aaaaaa;
+ line-height: 13px;
+ padding: 3px 5px 3px 18px;
+ margin: 3px 0 3px 5px;
+ position: relative;
+ cursor: default;
+}
+.select2-container-multi .select2-choices .select2-search-choice span {
+ cursor: default;
+}
+.select2-container-multi .select2-choices .select2-search-choice-focus {
+ background: #d4d4d4;
+}
+
+.select2-search-choice-close {
+ display: block;
+ position: absolute;
+ right: 3px;
+ top: 4px;
+ width: 12px;
+ height: 13px;
+ font-size: 1px;
+ background: url('select2.png') right top no-repeat;
+ outline: none;
+}
+
+.select2-container-multi .select2-search-choice-close {
+ left: 3px;
+}
+
+
+.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {
+ background-position: right -11px;
+}
+.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {
+ background-position: right -11px;
+}
+
+/* disabled styles */
+
+.select2-container-multi.select2-container-disabled .select2-choices{
+ background-color: #f4f4f4;
+ background-image: none;
+ border: 1px solid #ddd;
+ cursor: default;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {
+ background-image: none;
+ background-color: #f4f4f4;
+ border: 1px solid #ddd;
+ padding: 3px 5px 3px 5px;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close {
+ display: none;
+}
+/* end multiselect */
+
+.select2-result-selectable .select2-match,
+.select2-result-unselectable .select2-result-selectable .select2-match { text-decoration: underline; }
+.select2-result-unselectable .select2-match { text-decoration: none; }
+
+.select2-offscreen { position: absolute; left: -10000px; }
+
+/* Retina-ize icons */
+
+@media only screen and (-webkit-min-device-pixel-ratio: 1.5) {
+ .select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice div b {
+ background-image: url(select2x2.png) !important;
+ background-repeat: no-repeat !important;
+ background-size: 60px 40px !important;
+ }
+ .select2-search input {
+ background-position: 100% -21px !important;
+ }
+}
\ No newline at end of file
diff --git a/addons/base_import/static/lib/select2/select2.js b/addons/base_import/static/lib/select2/select2.js
new file mode 100755
index 00000000000..42ce0a928b8
--- /dev/null
+++ b/addons/base_import/static/lib/select2/select2.js
@@ -0,0 +1,2407 @@
+/*
+ Copyright 2012 Igor Vaynberg
+
+ Version: @@ver@@ Timestamp: @@timestamp@@
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
+ compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the License is
+ distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and limitations under the License.
+ */
+ (function ($) {
+ if(typeof $.fn.each2 == "undefined"){
+ $.fn.extend({
+ /*
+ * 4-10 times faster .each replacement
+ * use it carefully, as it overrides jQuery context of element on each iteration
+ */
+ each2 : function (c) {
+ var j = $([0]), i = -1, l = this.length;
+ while (
+ ++i < l
+ && (j.context = j[0] = this[i])
+ && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
+ );
+ return this;
+ }
+ });
+ }
+})(jQuery);
+
+(function ($, undefined) {
+ "use strict";
+ /*global document, window, jQuery, console */
+
+ if (window.Select2 !== undefined) {
+ return;
+ }
+
+ var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer;
+
+ KEY = {
+ TAB: 9,
+ ENTER: 13,
+ ESC: 27,
+ SPACE: 32,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ SHIFT: 16,
+ CTRL: 17,
+ ALT: 18,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
+ HOME: 36,
+ END: 35,
+ BACKSPACE: 8,
+ DELETE: 46,
+ isArrow: function (k) {
+ k = k.which ? k.which : k;
+ switch (k) {
+ case KEY.LEFT:
+ case KEY.RIGHT:
+ case KEY.UP:
+ case KEY.DOWN:
+ return true;
+ }
+ return false;
+ },
+ isControl: function (e) {
+ var k = e.which;
+ switch (k) {
+ case KEY.SHIFT:
+ case KEY.CTRL:
+ case KEY.ALT:
+ return true;
+ }
+
+ if (e.metaKey) return true;
+
+ return false;
+ },
+ isFunctionKey: function (k) {
+ k = k.which ? k.which : k;
+ return k >= 112 && k <= 123;
+ }
+ };
+
+ nextUid=(function() { var counter=1; return function() { return counter++; }; }());
+
+ function indexOf(value, array) {
+ var i = 0, l = array.length, v;
+
+ if (typeof value === "undefined") {
+ return -1;
+ }
+
+ if (value.constructor === String) {
+ for (; i < l; i = i + 1) if (value.localeCompare(array[i]) === 0) return i;
+ } else {
+ for (; i < l; i = i + 1) {
+ v = array[i];
+ if (v.constructor === String) {
+ if (v.localeCompare(value) === 0) return i;
+ } else {
+ if (v === value) return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Compares equality of a and b taking into account that a and b may be strings, in which case localeCompare is used
+ * @param a
+ * @param b
+ */
+ function equal(a, b) {
+ if (a === b) return true;
+ if (a === undefined || b === undefined) return false;
+ if (a === null || b === null) return false;
+ if (a.constructor === String) return a.localeCompare(b) === 0;
+ if (b.constructor === String) return b.localeCompare(a) === 0;
+ return false;
+ }
+
+ /**
+ * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
+ * strings
+ * @param string
+ * @param separator
+ */
+ function splitVal(string, separator) {
+ var val, i, l;
+ if (string === null || string.length < 1) return [];
+ val = string.split(separator);
+ for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
+ return val;
+ }
+
+ function getSideBorderPadding(element) {
+ return element.outerWidth() - element.width();
+ }
+
+ function installKeyUpChangeEvent(element) {
+ var key="keyup-change-value";
+ element.bind("keydown", function () {
+ if ($.data(element, key) === undefined) {
+ $.data(element, key, element.val());
+ }
+ });
+ element.bind("keyup", function () {
+ var val= $.data(element, key);
+ if (val !== undefined && element.val() !== val) {
+ $.removeData(element, key);
+ element.trigger("keyup-change");
+ }
+ });
+ }
+
+ $(document).delegate("*", "mousemove", function (e) {
+ $.data(document, "select2-lastpos", {x: e.pageX, y: e.pageY});
+ });
+
+ /**
+ * filters mouse events so an event is fired only if the mouse moved.
+ *
+ * filters out mouse events that occur when mouse is stationary but
+ * the elements under the pointer are scrolled.
+ */
+ function installFilteredMouseMove(element) {
+ element.bind("mousemove", function (e) {
+ var lastpos = $.data(document, "select2-lastpos");
+ if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
+ $(e.target).trigger("mousemove-filtered", e);
+ }
+ });
+ }
+
+ /**
+ * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
+ * within the last quietMillis milliseconds.
+ *
+ * @param quietMillis number of milliseconds to wait before invoking fn
+ * @param fn function to be debounced
+ * @param ctx object to be used as this reference within fn
+ * @return debounced version of fn
+ */
+ function debounce(quietMillis, fn, ctx) {
+ ctx = ctx || undefined;
+ var timeout;
+ return function () {
+ var args = arguments;
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(function() {
+ fn.apply(ctx, args);
+ }, quietMillis);
+ };
+ }
+
+ /**
+ * A simple implementation of a thunk
+ * @param formula function used to lazily initialize the thunk
+ * @return {Function}
+ */
+ function thunk(formula) {
+ var evaluated = false,
+ value;
+ return function() {
+ if (evaluated === false) { value = formula(); evaluated = true; }
+ return value;
+ };
+ };
+
+ function installDebouncedScroll(threshold, element) {
+ var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
+ element.bind("scroll", function (e) {
+ if (indexOf(e.target, element.get()) >= 0) notify(e);
+ });
+ }
+
+ function killEvent(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ function measureTextWidth(e) {
+ if (!sizer){
+ var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
+ sizer = $("
").css({
+ position: "absolute",
+ left: "-10000px",
+ top: "-10000px",
+ display: "none",
+ fontSize: style.fontSize,
+ fontFamily: style.fontFamily,
+ fontStyle: style.fontStyle,
+ fontWeight: style.fontWeight,
+ letterSpacing: style.letterSpacing,
+ textTransform: style.textTransform,
+ whiteSpace: "nowrap"
+ });
+ $("body").append(sizer);
+ }
+ sizer.text(e.val());
+ return sizer.width();
+ }
+
+ function markMatch(text, term, markup) {
+ var match=text.toUpperCase().indexOf(term.toUpperCase()),
+ tl=term.length;
+
+ if (match<0) {
+ markup.push(text);
+ return;
+ }
+
+ markup.push(text.substring(0, match));
+ markup.push("");
+ markup.push(text.substring(match, match + tl));
+ markup.push(" ");
+ markup.push(text.substring(match + tl, text.length));
+ }
+
+ /**
+ * Produces an ajax-based query function
+ *
+ * @param options object containing configuration paramters
+ * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
+ * @param options.url url for the data
+ * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
+ * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified
+ * @param options.traditional a boolean flag that should be true if you wish to use the traditional style of param serialization for the ajax request
+ * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
+ * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2.
+ * The expected format is an object containing the following keys:
+ * results array of objects that will be used as choices
+ * more (optional) boolean indicating whether there are more results available
+ * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
+ */
+ function ajax(options) {
+ var timeout, // current scheduled but not yet executed request
+ requestSequence = 0, // sequence used to drop out-of-order responses
+ handler = null,
+ quietMillis = options.quietMillis || 100;
+
+ return function (query) {
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(function () {
+ requestSequence += 1; // increment the sequence
+ var requestNumber = requestSequence, // this request's sequence number
+ data = options.data, // ajax data function
+ transport = options.transport || $.ajax,
+ traditional = options.traditional || false,
+ type = options.type || 'GET'; // set type of request (GET or POST)
+
+ data = data.call(this, query.term, query.page, query.context);
+
+ if( null !== handler) { handler.abort(); }
+
+ handler = transport.call(null, {
+ url: options.url,
+ dataType: options.dataType,
+ data: data,
+ type: type,
+ traditional: traditional,
+ success: function (data) {
+ if (requestNumber < requestSequence) {
+ return;
+ }
+ // TODO 3.0 - replace query.page with query so users have access to term, page, etc.
+ var results = options.results(data, query.page);
+ query.callback(results);
+ }
+ });
+ }, quietMillis);
+ };
+ }
+
+ /**
+ * Produces a query function that works with a local array
+ *
+ * @param options object containing configuration parameters. The options parameter can either be an array or an
+ * object.
+ *
+ * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
+ *
+ * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
+ * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
+ * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
+ * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
+ * the text.
+ */
+ function local(options) {
+ var data = options, // data elements
+ dataText,
+ text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
+
+ if (!$.isArray(data)) {
+ text = data.text;
+ // if text is not a function we assume it to be a key name
+ if (!$.isFunction(text)) {
+ dataText = data.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available
+ text = function (item) { return item[dataText]; };
+ }
+ data = data.results;
+ }
+
+ return function (query) {
+ var t = query.term, filtered = { results: [] }, process;
+ if (t === "") {
+ query.callback({results: data});
+ return;
+ }
+
+ process = function(datum, collection) {
+ var group, attr;
+ datum = datum[0];
+ if (datum.children) {
+ group = {};
+ for (attr in datum) {
+ if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
+ }
+ group.children=[];
+ $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
+ if (group.children.length) {
+ collection.push(group);
+ }
+ } else {
+ if (query.matcher(t, text(datum))) {
+ collection.push(datum);
+ }
+ }
+ };
+
+ $(data).each2(function(i, datum) { process(datum, filtered.results); });
+ query.callback(filtered);
+ };
+ }
+
+ // TODO javadoc
+ function tags(data) {
+ // TODO even for a function we should probably return a wrapper that does the same object/string check as
+ // the function for arrays. otherwise only functions that return objects are supported.
+ if ($.isFunction(data)) {
+ return data;
+ }
+
+ // if not a function we assume it to be an array
+
+ return function (query) {
+ var t = query.term, filtered = {results: []};
+ $(data).each(function () {
+ var isObject = this.text !== undefined,
+ text = isObject ? this.text : this;
+ if (t === "" || query.matcher(t, text)) {
+ filtered.results.push(isObject ? this : {id: this, text: this});
+ }
+ });
+ query.callback(filtered);
+ };
+ }
+
+ /**
+ * Checks if the formatter function should be used.
+ *
+ * Throws an error if it is not a function. Returns true if it should be used,
+ * false if no formatting should be performed.
+ *
+ * @param formatter
+ */
+ function checkFormatter(formatter, formatterName) {
+ if ($.isFunction(formatter)) return true;
+ if (!formatter) return false;
+ throw new Error("formatterName must be a function or a falsy value");
+ }
+
+ function evaluate(val) {
+ return $.isFunction(val) ? val() : val;
+ }
+
+ function countResults(results) {
+ var count = 0;
+ $.each(results, function(i, item) {
+ if (item.children) {
+ count += countResults(item.children);
+ } else {
+ count++;
+ }
+ });
+ return count;
+ }
+
+ /**
+ * Default tokenizer. This function uses breaks the input on substring match of any string from the
+ * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
+ * two options have to be defined in order for the tokenizer to work.
+ *
+ * @param input text user has typed so far or pasted into the search field
+ * @param selection currently selected choices
+ * @param selectCallback function(choice) callback tho add the choice to selection
+ * @param opts select2's opts
+ * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
+ */
+ function defaultTokenizer(input, selection, selectCallback, opts) {
+ var original = input, // store the original so we can compare and know if we need to tell the search to update its text
+ dupe = false, // check for whether a token we extracted represents a duplicate selected choice
+ token, // token
+ index, // position at which the separator was found
+ i, l, // looping variables
+ separator; // the matched separator
+
+ if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
+
+ while (true) {
+ index = -1;
+
+ for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
+ separator = opts.tokenSeparators[i];
+ index = input.indexOf(separator);
+ if (index >= 0) break;
+ }
+
+ if (index < 0) break; // did not find any token separator in the input string, bail
+
+ token = input.substring(0, index);
+ input = input.substring(index + separator.length);
+
+ if (token.length > 0) {
+ token = opts.createSearchChoice(token, selection);
+ if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
+ dupe = false;
+ for (i = 0, l = selection.length; i < l; i++) {
+ if (equal(opts.id(token), opts.id(selection[i]))) {
+ dupe = true; break;
+ }
+ }
+
+ if (!dupe) selectCallback(token);
+ }
+ }
+ }
+
+ if (original.localeCompare(input) != 0) return input;
+ }
+
+ /**
+ * blurs any Select2 container that has focus when an element outside them was clicked or received focus
+ *
+ * also takes care of clicks on label tags that point to the source element
+ */
+ $(document).ready(function () {
+ $(document).delegate("*", "mousedown touchend", function (e) {
+ var target = $(e.target).closest("div.select2-container").get(0), attr;
+ if (target) {
+ $(document).find("div.select2-container-active").each(function () {
+ if (this !== target) $(this).data("select2").blur();
+ });
+ } else {
+ target = $(e.target).closest("div.select2-drop").get(0);
+ $(document).find("div.select2-drop-active").each(function () {
+ if (this !== target) $(this).data("select2").blur();
+ });
+ }
+
+ target=$(e.target);
+ attr = target.attr("for");
+ if ("LABEL" === e.target.tagName && attr && attr.length > 0) {
+ target = $("#"+attr);
+ target = target.data("select2");
+ if (target !== undefined) { target.focus(); e.preventDefault();}
+ }
+ });
+ });
+
+ /**
+ * Creates a new class
+ *
+ * @param superClass
+ * @param methods
+ */
+ function clazz(SuperClass, methods) {
+ var constructor = function () {};
+ constructor.prototype = new SuperClass;
+ constructor.prototype.constructor = constructor;
+ constructor.prototype.parent = SuperClass.prototype;
+ constructor.prototype = $.extend(constructor.prototype, methods);
+ return constructor;
+ }
+
+ AbstractSelect2 = clazz(Object, {
+
+ // abstract
+ bind: function (func) {
+ var self = this;
+ return function () {
+ func.apply(self, arguments);
+ };
+ },
+
+ // abstract
+ init: function (opts) {
+ var results, search, resultsSelector = ".select2-results";
+
+ // prepare options
+ this.opts = opts = this.prepareOpts(opts);
+
+ this.id=opts.id;
+
+ // destroy if called on an existing component
+ if (opts.element.data("select2") !== undefined &&
+ opts.element.data("select2") !== null) {
+ this.destroy();
+ }
+
+ this.enabled=true;
+ this.container = this.createContainer();
+
+ this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
+ this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
+ this.container.attr("id", this.containerId);
+
+ // cache the body so future lookups are cheap
+ this.body = thunk(function() { return opts.element.closest("body"); });
+
+ if (opts.element.attr("class") !== undefined) {
+ this.container.addClass(opts.element.attr("class").replace(/validate\[[\S ]+] ?/, ''));
+ }
+
+ this.container.css(evaluate(opts.containerCss));
+ this.container.addClass(evaluate(opts.containerCssClass));
+
+ // swap container for the element
+ this.opts.element
+ .data("select2", this)
+ .hide()
+ .before(this.container);
+ this.container.data("select2", this);
+
+ this.dropdown = this.container.find(".select2-drop");
+ this.dropdown.addClass(evaluate(opts.dropdownCssClass));
+ this.dropdown.data("select2", this);
+
+ this.results = results = this.container.find(resultsSelector);
+ this.search = search = this.container.find("input.select2-input");
+
+ search.attr("tabIndex", this.opts.element.attr("tabIndex"));
+
+ this.resultsPage = 0;
+ this.context = null;
+
+ // initialize the container
+ this.initContainer();
+ this.initContainerWidth();
+
+ installFilteredMouseMove(this.results);
+ this.dropdown.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent));
+
+ installDebouncedScroll(80, this.results);
+ this.dropdown.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded));
+
+ // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
+ if ($.fn.mousewheel) {
+ results.mousewheel(function (e, delta, deltaX, deltaY) {
+ var top = results.scrollTop(), height;
+ if (deltaY > 0 && top - deltaY <= 0) {
+ results.scrollTop(0);
+ killEvent(e);
+ } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
+ results.scrollTop(results.get(0).scrollHeight - results.height());
+ killEvent(e);
+ }
+ });
+ }
+
+ installKeyUpChangeEvent(search);
+ search.bind("keyup-change", this.bind(this.updateResults));
+ search.bind("focus", function () { search.addClass("select2-focused"); if (search.val() === " ") search.val(""); });
+ search.bind("blur", function () { search.removeClass("select2-focused");});
+
+ this.dropdown.delegate(resultsSelector, "mouseup", this.bind(function (e) {
+ if ($(e.target).closest(".select2-result-selectable:not(.select2-disabled)").length > 0) {
+ this.highlightUnderEvent(e);
+ this.selectHighlighted(e);
+ } else {
+ this.focusSearch();
+ }
+ killEvent(e);
+ }));
+
+ // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
+ // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
+ // dom it will trigger the popup close, which is not what we want
+ this.dropdown.bind("click mouseup mousedown", function (e) { e.stopPropagation(); });
+
+ if ($.isFunction(this.opts.initSelection)) {
+ // initialize selection based on the current value of the source element
+ this.initSelection();
+
+ // if the user has provided a function that can set selection based on the value of the source element
+ // we monitor the change event on the element and trigger it, allowing for two way synchronization
+ this.monitorSource();
+ }
+
+ if (opts.element.is(":disabled") || opts.element.is("[readonly='readonly']")) this.disable();
+ },
+
+ // abstract
+ destroy: function () {
+ var select2 = this.opts.element.data("select2");
+ if (select2 !== undefined) {
+ select2.container.remove();
+ select2.dropdown.remove();
+ select2.opts.element
+ .removeData("select2")
+ .unbind(".select2")
+ .show();
+ }
+ },
+
+ // abstract
+ prepareOpts: function (opts) {
+ var element, select, idKey, ajaxUrl;
+
+ element = opts.element;
+
+ if (element.get(0).tagName.toLowerCase() === "select") {
+ this.select = select = opts.element;
+ }
+
+ if (select) {
+ // these options are not allowed when attached to a select because they are picked up off the element itself
+ $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
+ if (this in opts) {
+ throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a element.");
+ }
+ });
+ }
+
+ opts = $.extend({}, {
+ populateResults: function(container, results, query) {
+ var populate, data, result, children, id=this.opts.id, self=this;
+
+ populate=function(results, container, depth) {
+
+ var i, l, result, selectable, compound, node, label, innerContainer, formatted;
+ for (i = 0, l = results.length; i < l; i = i + 1) {
+
+ result=results[i];
+ selectable=id(result) !== undefined;
+ compound=("children" in result) && result.children.length > 0;
+
+ node=$(" ");
+ node.addClass("select2-results-dept-"+depth);
+ node.addClass("select2-result");
+ node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
+ if (compound) { node.addClass("select2-result-with-children"); }
+ node.addClass(self.opts.formatResultCssClass(result));
+
+ label=$("
");
+ label.addClass("select2-result-label");
+
+ formatted=opts.formatResult(result, label, query);
+ if (formatted!==undefined) {
+ label.html(self.opts.escapeMarkup(formatted));
+ }
+
+ node.append(label);
+
+ if (compound) {
+
+ innerContainer=$("");
+ innerContainer.addClass("select2-result-sub");
+ populate(result.children, innerContainer, depth+1);
+ node.append(innerContainer);
+ }
+
+ node.data("select2-data", result);
+ container.append(node);
+ }
+ };
+
+ populate(results, container, 0);
+ }
+ }, $.fn.select2.defaults, opts);
+
+ if (typeof(opts.id) !== "function") {
+ idKey = opts.id;
+ opts.id = function (e) { return e[idKey]; };
+ }
+
+ if (select) {
+ opts.query = this.bind(function (query) {
+ var data = { results: [], more: false },
+ term = query.term,
+ children, firstChild, process;
+
+ process=function(element, collection) {
+ var group;
+ if (element.is("option")) {
+ if (query.matcher(term, element.text(), element)) {
+ collection.push({id:element.attr("value"), text:element.text(), element: element.get(), css: element.attr("class")});
+ }
+ } else if (element.is("optgroup")) {
+ group={text:element.attr("label"), children:[], element: element.get(), css: element.attr("class")};
+ element.children().each2(function(i, elm) { process(elm, group.children); });
+ if (group.children.length>0) {
+ collection.push(group);
+ }
+ }
+ };
+
+ children=element.children();
+
+ // ignore the placeholder option if there is one
+ if (this.getPlaceholder() !== undefined && children.length > 0) {
+ firstChild = children[0];
+ if ($(firstChild).text() === "") {
+ children=children.not(firstChild);
+ }
+ }
+
+ children.each2(function(i, elm) { process(elm, data.results); });
+
+ query.callback(data);
+ });
+ // this is needed because inside val() we construct choices from options and there id is hardcoded
+ opts.id=function(e) { return e.id; };
+ opts.formatResultCssClass = function(data) { return data.css; }
+ } else {
+ if (!("query" in opts)) {
+ if ("ajax" in opts) {
+ ajaxUrl = opts.element.data("ajax-url");
+ if (ajaxUrl && ajaxUrl.length > 0) {
+ opts.ajax.url = ajaxUrl;
+ }
+ opts.query = ajax(opts.ajax);
+ } else if ("data" in opts) {
+ opts.query = local(opts.data);
+ } else if ("tags" in opts) {
+ opts.query = tags(opts.tags);
+ opts.createSearchChoice = function (term) { return {id: term, text: term}; };
+ opts.initSelection = function (element, callback) {
+ var data = [];
+ $(splitVal(element.val(), opts.separator)).each(function () {
+ var id = this, text = this, tags=opts.tags;
+ if ($.isFunction(tags)) tags=tags();
+ $(tags).each(function() { if (equal(this.id, id)) { text = this.text; return false; } });
+ data.push({id: id, text: text});
+ });
+
+ callback(data);
+ };
+ }
+ }
+ }
+ if (typeof(opts.query) !== "function") {
+ throw "query function not defined for Select2 " + opts.element.attr("id");
+ }
+
+ return opts;
+ },
+
+ /**
+ * Monitor the original element for changes and update select2 accordingly
+ */
+ // abstract
+ monitorSource: function () {
+ this.opts.element.bind("change.select2", this.bind(function (e) {
+ if (this.opts.element.data("select2-change-triggered") !== true) {
+ this.initSelection();
+ }
+ }));
+ },
+
+ /**
+ * Triggers the change event on the source element
+ */
+ // abstract
+ triggerChange: function (details) {
+
+ details = details || {};
+ details= $.extend({}, details, { type: "change", val: this.val() });
+ // prevents recursive triggering
+ this.opts.element.data("select2-change-triggered", true);
+ this.opts.element.trigger(details);
+ this.opts.element.data("select2-change-triggered", false);
+
+ // some validation frameworks ignore the change event and listen instead to keyup, click for selects
+ // so here we trigger the click event manually
+ this.opts.element.click();
+
+ // ValidationEngine ignorea the change event and listens instead to blur
+ // so here we trigger the blur event manually if so desired
+ if (this.opts.blurOnChange)
+ this.opts.element.blur();
+ },
+
+
+ // abstract
+ enable: function() {
+ if (this.enabled) return;
+
+ this.enabled=true;
+ this.container.removeClass("select2-container-disabled");
+ },
+
+ // abstract
+ disable: function() {
+ if (!this.enabled) return;
+
+ this.close();
+
+ this.enabled=false;
+ this.container.addClass("select2-container-disabled");
+ },
+
+ // abstract
+ opened: function () {
+ return this.container.hasClass("select2-dropdown-open");
+ },
+
+ // abstract
+ positionDropdown: function() {
+ var offset = this.container.offset(),
+ height = this.container.outerHeight(),
+ width = this.container.outerWidth(),
+ dropHeight = this.dropdown.outerHeight(),
+ viewportBottom = $(window).scrollTop() + document.documentElement.clientHeight,
+ dropTop = offset.top + height,
+ dropLeft = offset.left,
+ enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
+ enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(),
+ aboveNow = this.dropdown.hasClass("select2-drop-above"),
+ bodyOffset,
+ above,
+ css;
+
+ // console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
+ // console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove);
+
+ // fix positioning when body has an offset and is not position: static
+
+ if (this.body().css('position') !== 'static') {
+ bodyOffset = this.body().offset();
+ dropTop -= bodyOffset.top;
+ dropLeft -= bodyOffset.left;
+ }
+
+ // always prefer the current above/below alignment, unless there is not enough room
+
+ if (aboveNow) {
+ above = true;
+ if (!enoughRoomAbove && enoughRoomBelow) above = false;
+ } else {
+ above = false;
+ if (!enoughRoomBelow && enoughRoomAbove) above = true;
+ }
+
+ if (above) {
+ dropTop = offset.top - dropHeight;
+ this.container.addClass("select2-drop-above");
+ this.dropdown.addClass("select2-drop-above");
+ }
+ else {
+ this.container.removeClass("select2-drop-above");
+ this.dropdown.removeClass("select2-drop-above");
+ }
+
+ css = $.extend({
+ top: dropTop,
+ left: dropLeft,
+ width: width
+ }, evaluate(this.opts.dropdownCss));
+
+ this.dropdown.css(css);
+ },
+
+ // abstract
+ shouldOpen: function() {
+ var event;
+
+ if (this.opened()) return false;
+
+ event = jQuery.Event("open");
+ this.opts.element.trigger(event);
+ return !event.isDefaultPrevented();
+ },
+
+ // abstract
+ clearDropdownAlignmentPreference: function() {
+ // clear the classes used to figure out the preference of where the dropdown should be opened
+ this.container.removeClass("select2-drop-above");
+ this.dropdown.removeClass("select2-drop-above");
+ },
+
+ /**
+ * Opens the dropdown
+ *
+ * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
+ * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
+ */
+ // abstract
+ open: function () {
+
+ if (!this.shouldOpen()) return false;
+
+ window.setTimeout(this.bind(this.opening), 1);
+
+ return true;
+ },
+
+ /**
+ * Performs the opening of the dropdown
+ */
+ // abstract
+ opening: function() {
+ var cid = this.containerId, selector = this.containerSelector,
+ scroll = "scroll." + cid, resize = "resize." + cid;
+
+ this.container.parents().each(function() {
+ $(this).bind(scroll, function() {
+ var s2 = $(selector);
+ if (s2.length == 0) {
+ $(this).unbind(scroll);
+ }
+ s2.select2("close");
+ });
+ });
+
+ $(window).bind(resize, function() {
+ var s2 = $(selector);
+ if (s2.length == 0) {
+ $(window).unbind(resize);
+ }
+ s2.select2("close");
+ });
+
+ this.clearDropdownAlignmentPreference();
+
+ if (this.search.val() === " ") { this.search.val(""); }
+
+ this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
+
+ this.updateResults(true);
+
+ if(this.dropdown[0] !== this.body().children().last()[0]) {
+ this.dropdown.detach().appendTo(this.body());
+ }
+
+ this.dropdown.show();
+
+ this.positionDropdown();
+ this.dropdown.addClass("select2-drop-active");
+
+ this.ensureHighlightVisible();
+
+ this.focusSearch();
+ },
+
+ // abstract
+ close: function () {
+ if (!this.opened()) return;
+
+ var self = this;
+
+ this.container.parents().each(function() {
+ $(this).unbind("scroll." + self.containerId);
+ });
+ $(window).unbind("resize." + this.containerId);
+
+ this.clearDropdownAlignmentPreference();
+
+ this.dropdown.hide();
+ this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
+ this.results.empty();
+ this.clearSearch();
+
+ this.opts.element.trigger(jQuery.Event("close"));
+ },
+
+ // abstract
+ clearSearch: function () {
+
+ },
+
+ // abstract
+ ensureHighlightVisible: function () {
+ var results = this.results, children, index, child, hb, rb, y, more;
+
+ index = this.highlight();
+
+ if (index < 0) return;
+
+ if (index == 0) {
+
+ // if the first element is highlighted scroll all the way to the top,
+ // that way any unselectable headers above it will also be scrolled
+ // into view
+
+ results.scrollTop(0);
+ return;
+ }
+
+ children = results.find(".select2-result-selectable");
+
+ child = $(children[index]);
+
+ hb = child.offset().top + child.outerHeight();
+
+ // if this is the last child lets also make sure select2-more-results is visible
+ if (index === children.length - 1) {
+ more = results.find("li.select2-more-results");
+ if (more.length > 0) {
+ hb = more.offset().top + more.outerHeight();
+ }
+ }
+
+ rb = results.offset().top + results.outerHeight();
+ if (hb > rb) {
+ results.scrollTop(results.scrollTop() + (hb - rb));
+ }
+ y = child.offset().top - results.offset().top;
+
+ // make sure the top of the element is visible
+ if (y < 0) {
+ results.scrollTop(results.scrollTop() + y); // y is negative
+ }
+ },
+
+ // abstract
+ moveHighlight: function (delta) {
+ var choices = this.results.find(".select2-result-selectable"),
+ index = this.highlight();
+
+ while (index > -1 && index < choices.length) {
+ index += delta;
+ var choice = $(choices[index]);
+ if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled")) {
+ this.highlight(index);
+ break;
+ }
+ }
+ },
+
+ // abstract
+ highlight: function (index) {
+ var choices = this.results.find(".select2-result-selectable").not(".select2-disabled");
+
+ if (arguments.length === 0) {
+ return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
+ }
+
+ if (index >= choices.length) index = choices.length - 1;
+ if (index < 0) index = 0;
+
+ choices.removeClass("select2-highlighted");
+
+ $(choices[index]).addClass("select2-highlighted");
+ this.ensureHighlightVisible();
+
+ },
+
+ // abstract
+ countSelectableResults: function() {
+ return this.results.find(".select2-result-selectable").not(".select2-disabled").length;
+ },
+
+ // abstract
+ highlightUnderEvent: function (event) {
+ var el = $(event.target).closest(".select2-result-selectable");
+ if (el.length > 0 && !el.is(".select2-highlighted")) {
+ var choices = this.results.find('.select2-result-selectable');
+ this.highlight(choices.index(el));
+ } else if (el.length == 0) {
+ // if we are over an unselectable item remove al highlights
+ this.results.find(".select2-highlighted").removeClass("select2-highlighted");
+ }
+ },
+
+ // abstract
+ loadMoreIfNeeded: function () {
+ var results = this.results,
+ more = results.find("li.select2-more-results"),
+ below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
+ offset = -1, // index of first element without data
+ page = this.resultsPage + 1,
+ self=this,
+ term=this.search.val(),
+ context=this.context;
+
+ if (more.length === 0) return;
+ below = more.offset().top - results.offset().top - results.height();
+
+ if (below <= 0) {
+ more.addClass("select2-active");
+ this.opts.query({
+ term: term,
+ page: page,
+ context: context,
+ matcher: this.opts.matcher,
+ callback: this.bind(function (data) {
+
+ // ignore a response if the select2 has been closed before it was received
+ if (!self.opened()) return;
+
+
+ self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
+
+ if (data.more===true) {
+ more.detach().appendTo(results).text(self.opts.formatLoadMore(page+1));
+ window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
+ } else {
+ more.remove();
+ }
+ self.positionDropdown();
+ self.resultsPage = page;
+ })});
+ }
+ },
+
+ /**
+ * Default tokenizer function which does nothing
+ */
+ tokenize: function() {
+
+ },
+
+ /**
+ * @param initial whether or not this is the call to this method right after the dropdown has been opened
+ */
+ // abstract
+ updateResults: function (initial) {
+ var search = this.search, results = this.results, opts = this.opts, data, self=this, input;
+
+ // if the search is currently hidden we do not alter the results
+ if (initial !== true && (this.showSearchInput === false || !this.opened())) {
+ return;
+ }
+
+ search.addClass("select2-active");
+
+ function postRender() {
+ results.scrollTop(0);
+ search.removeClass("select2-active");
+ self.positionDropdown();
+ }
+
+ function render(html) {
+ results.html(self.opts.escapeMarkup(html));
+ postRender();
+ }
+
+ if (opts.maximumSelectionSize >=1) {
+ data = this.data();
+ if ($.isArray(data) && data.length >= opts.maximumSelectionSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
+ render("" + opts.formatSelectionTooBig(opts.maximumSelectionSize) + " ");
+ return;
+ }
+ }
+
+ if (search.val().length < opts.minimumInputLength && checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
+ render("" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + " ");
+ return;
+ }
+ else {
+ render("" + opts.formatSearching() + " ");
+ }
+
+ // give the tokenizer a chance to pre-process the input
+ input = this.tokenize();
+ if (input != undefined && input != null) {
+ search.val(input);
+ }
+
+ this.resultsPage = 1;
+ opts.query({
+ term: search.val(),
+ page: this.resultsPage,
+ context: null,
+ matcher: opts.matcher,
+ callback: this.bind(function (data) {
+ var def; // default choice
+
+ // ignore a response if the select2 has been closed before it was received
+ if (!this.opened()) return;
+
+ // save context, if any
+ this.context = (data.context===undefined) ? null : data.context;
+
+ // create a default choice and prepend it to the list
+ if (this.opts.createSearchChoice && search.val() !== "") {
+ def = this.opts.createSearchChoice.call(null, search.val(), data.results);
+ if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
+ if ($(data.results).filter(
+ function () {
+ return equal(self.id(this), self.id(def));
+ }).length === 0) {
+ data.results.unshift(def);
+ }
+ }
+ }
+
+ if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
+ render("" + opts.formatNoMatches(search.val()) + " ");
+ return;
+ }
+
+ results.empty();
+ self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
+
+ if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
+ results.append("" + self.opts.escapeMarkup(opts.formatLoadMore(this.resultsPage)) + " ");
+ window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
+ }
+
+ this.postprocessResults(data, initial);
+
+ postRender();
+ })});
+ },
+
+ // abstract
+ cancel: function () {
+ this.close();
+ },
+
+ // abstract
+ blur: function () {
+ this.close();
+ this.container.removeClass("select2-container-active");
+ this.dropdown.removeClass("select2-drop-active");
+ // synonymous to .is(':focus'), which is available in jquery >= 1.6
+ if (this.search[0] === document.activeElement) { this.search.blur(); }
+ this.clearSearch();
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
+ },
+
+ // abstract
+ focusSearch: function () {
+ // need to do it here as well as in timeout so it works in IE
+ this.search.show();
+ this.search.focus();
+
+ /* we do this in a timeout so that current event processing can complete before this code is executed.
+ this makes sure the search field is focussed even if the current event would blur it */
+ window.setTimeout(this.bind(function () {
+ // reset the value so IE places the cursor at the end of the input box
+ this.search.show();
+ this.search.focus();
+ this.search.val(this.search.val());
+ }), 10);
+ },
+
+ // abstract
+ selectHighlighted: function () {
+ var index=this.highlight(),
+ highlighted=this.results.find(".select2-highlighted").not(".select2-disabled"),
+ data = highlighted.closest('.select2-result-selectable').data("select2-data");
+ if (data) {
+ highlighted.addClass("select2-disabled");
+ this.highlight(index);
+ this.onSelect(data);
+ }
+ },
+
+ // abstract
+ getPlaceholder: function () {
+ return this.opts.element.attr("placeholder") ||
+ this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
+ this.opts.element.data("placeholder") ||
+ this.opts.placeholder;
+ },
+
+ /**
+ * Get the desired width for the container element. This is
+ * derived first from option `width` passed to select2, then
+ * the inline 'style' on the original element, and finally
+ * falls back to the jQuery calculated element width.
+ */
+ // abstract
+ initContainerWidth: function () {
+ function resolveContainerWidth() {
+ var style, attrs, matches, i, l;
+
+ if (this.opts.width === "off") {
+ return null;
+ } else if (this.opts.width === "element"){
+ return this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px';
+ } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
+ // check if there is inline style on the element that contains width
+ style = this.opts.element.attr('style');
+ if (style !== undefined) {
+ attrs = style.split(';');
+ for (i = 0, l = attrs.length; i < l; i = i + 1) {
+ matches = attrs[i].replace(/\s/g, '')
+ .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/);
+ if (matches !== null && matches.length >= 1)
+ return matches[1];
+ }
+ }
+
+ if (this.opts.width === "resolve") {
+ // next check if css('width') can resolve a width that is percent based, this is sometimes possible
+ // when attached to input type=hidden or elements hidden via css
+ style = this.opts.element.css('width');
+ if (style.indexOf("%") > 0) return style;
+
+ // finally, fallback on the calculated width of the element
+ return (this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px');
+ }
+
+ return null;
+ } else if ($.isFunction(this.opts.width)) {
+ return this.opts.width();
+ } else {
+ return this.opts.width;
+ }
+ };
+
+ var width = resolveContainerWidth.call(this);
+ if (width !== null) {
+ this.container.attr("style", "width: "+width);
+ }
+ }
+ });
+
+ SingleSelect2 = clazz(AbstractSelect2, {
+
+ // single
+
+ createContainer: function () {
+ var container = $("
", {
+ "class": "select2-container"
+ }).html([
+ " ",
+ " ",
+ "
" ,
+ " ",
+ " " ,
+ "
" ,
+ " " ,
+ "
" ,
+ "
" ,
+ "
"].join(""));
+ return container;
+ },
+
+ // single
+ opening: function () {
+ this.search.show();
+ this.parent.opening.apply(this, arguments);
+ this.dropdown.removeClass("select2-offscreen");
+ },
+
+ // single
+ close: function () {
+ if (!this.opened()) return;
+ this.parent.close.apply(this, arguments);
+ this.dropdown.removeAttr("style").addClass("select2-offscreen").insertAfter(this.selection).show();
+ },
+
+ // single
+ focus: function () {
+ this.close();
+ this.selection.focus();
+ },
+
+ // single
+ isFocused: function () {
+ return this.selection[0] === document.activeElement;
+ },
+
+ // single
+ cancel: function () {
+ this.parent.cancel.apply(this, arguments);
+ this.selection.focus();
+ },
+
+ // single
+ initContainer: function () {
+
+ var selection,
+ container = this.container,
+ dropdown = this.dropdown,
+ clickingInside = false;
+
+ this.selection = selection = container.find(".select2-choice");
+
+ this.search.bind("keydown", this.bind(function (e) {
+ if (!this.enabled) return;
+
+ if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
+ // prevent the page from scrolling
+ killEvent(e);
+ return;
+ }
+
+ if (this.opened()) {
+ switch (e.which) {
+ case KEY.UP:
+ case KEY.DOWN:
+ this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
+ killEvent(e);
+ return;
+ case KEY.TAB:
+ case KEY.ENTER:
+ this.selectHighlighted();
+ killEvent(e);
+ return;
+ case KEY.ESC:
+ this.cancel(e);
+ killEvent(e);
+ return;
+ }
+ } else {
+
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
+ return;
+ }
+
+ if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
+ return;
+ }
+
+ this.open();
+
+ if (e.which === KEY.ENTER) {
+ // do not propagate the event otherwise we open, and propagate enter which closes
+ return;
+ }
+ }
+ }));
+
+ this.search.bind("focus", this.bind(function() {
+ this.selection.attr("tabIndex", "-1");
+ }));
+ this.search.bind("blur", this.bind(function() {
+ if (!this.opened()) this.container.removeClass("select2-container-active");
+ window.setTimeout(this.bind(function() { this.selection.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10);
+ }));
+
+ selection.bind("mousedown", this.bind(function (e) {
+ clickingInside = true;
+
+ if (this.opened()) {
+ this.close();
+ this.selection.focus();
+ } else if (this.enabled) {
+ this.open();
+ }
+
+ clickingInside = false;
+ }));
+
+ dropdown.bind("mousedown", this.bind(function() { this.search.focus(); }));
+
+ selection.bind("focus", this.bind(function() {
+ this.container.addClass("select2-container-active");
+ // hide the search so the tab key does not focus on it
+ this.search.attr("tabIndex", "-1");
+ }));
+
+ selection.bind("blur", this.bind(function() {
+ if (!this.opened()) {
+ this.container.removeClass("select2-container-active");
+ }
+ window.setTimeout(this.bind(function() { this.search.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10);
+ }));
+
+ selection.bind("keydown", this.bind(function(e) {
+ if (!this.enabled) return;
+
+ if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
+ // prevent the page from scrolling
+ killEvent(e);
+ return;
+ }
+
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
+ || e.which === KEY.ESC) {
+ return;
+ }
+
+ if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
+ return;
+ }
+
+ if (e.which == KEY.DELETE) {
+ if (this.opts.allowClear) {
+ this.clear();
+ }
+ return;
+ }
+
+ this.open();
+
+ if (e.which === KEY.ENTER) {
+ // do not propagate the event otherwise we open, and propagate enter which closes
+ killEvent(e);
+ return;
+ }
+
+ // do not set the search input value for non-alpha-numeric keys
+ // otherwise pressing down results in a '(' being set in the search field
+ if (e.which < 48 ) { // '0' == 48
+ killEvent(e);
+ return;
+ }
+
+ var keyWritten = String.fromCharCode(e.which).toLowerCase();
+
+ if (e.shiftKey) {
+ keyWritten = keyWritten.toUpperCase();
+ }
+
+ // focus the field before calling val so the cursor ends up after the value instead of before
+ this.search.focus();
+ this.search.val(keyWritten);
+
+ // prevent event propagation so it doesnt replay on the now focussed search field and result in double key entry
+ killEvent(e);
+ }));
+
+ selection.delegate("abbr", "mousedown", this.bind(function (e) {
+ if (!this.enabled) return;
+ this.clear();
+ killEvent(e);
+ this.close();
+ this.triggerChange();
+ this.selection.focus();
+ }));
+
+ this.setPlaceholder();
+
+ this.search.bind("focus", this.bind(function() {
+ this.container.addClass("select2-container-active");
+ }));
+ },
+
+ // single
+ clear: function() {
+ this.opts.element.val("");
+ this.selection.find("span").empty();
+ this.selection.removeData("select2-data");
+ this.setPlaceholder();
+ },
+
+ /**
+ * Sets selection based on source element's value
+ */
+ // single
+ initSelection: function () {
+ var selected;
+ if (this.opts.element.val() === "") {
+ this.close();
+ this.setPlaceholder();
+ } else {
+ var self = this;
+ this.opts.initSelection.call(null, this.opts.element, function(selected){
+ if (selected !== undefined && selected !== null) {
+ self.updateSelection(selected);
+ self.close();
+ self.setPlaceholder();
+ }
+ });
+ }
+ },
+
+ // single
+ prepareOpts: function () {
+ var opts = this.parent.prepareOpts.apply(this, arguments);
+
+ if (opts.element.get(0).tagName.toLowerCase() === "select") {
+ // install the selection initializer
+ opts.initSelection = function (element, callback) {
+ var selected = element.find(":selected");
+ // a single select box always has a value, no need to null check 'selected'
+ if ($.isFunction(callback))
+ callback({id: selected.attr("value"), text: selected.text()});
+ };
+ }
+
+ return opts;
+ },
+
+ // single
+ setPlaceholder: function () {
+ var placeholder = this.getPlaceholder();
+
+ if (this.opts.element.val() === "" && placeholder !== undefined) {
+
+ // check for a first blank option if attached to a select
+ if (this.select && this.select.find("option:first").text() !== "") return;
+
+ this.selection.find("span").html(this.opts.escapeMarkup(placeholder));
+
+ this.selection.addClass("select2-default");
+
+ this.selection.find("abbr").hide();
+ }
+ },
+
+ // single
+ postprocessResults: function (data, initial) {
+ var selected = 0, self = this, showSearchInput = true;
+
+ // find the selected element in the result list
+
+ this.results.find(".select2-result-selectable").each2(function (i, elm) {
+ if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
+ selected = i;
+ return false;
+ }
+ });
+
+ // and highlight it
+
+ this.highlight(selected);
+
+ // hide the search box if this is the first we got the results and there are a few of them
+
+ if (initial === true) {
+ showSearchInput = this.showSearchInput = countResults(data.results) >= this.opts.minimumResultsForSearch;
+ this.dropdown.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden");
+
+ //add "select2-with-searchbox" to the container if search box is shown
+ $(this.dropdown, this.container)[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox");
+ }
+
+ },
+
+ // single
+ onSelect: function (data) {
+ var old = this.opts.element.val();
+
+ this.opts.element.val(this.id(data));
+ this.updateSelection(data);
+ this.close();
+ this.selection.focus();
+
+ if (!equal(old, this.id(data))) { this.triggerChange(); }
+ },
+
+ // single
+ updateSelection: function (data) {
+
+ var container=this.selection.find("span"), formatted;
+
+ this.selection.data("select2-data", data);
+
+ container.empty();
+ formatted=this.opts.formatSelection(data, container);
+ if (formatted !== undefined) {
+ container.append(this.opts.escapeMarkup(formatted));
+ }
+
+ this.selection.removeClass("select2-default");
+
+ if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
+ this.selection.find("abbr").show();
+ }
+ },
+
+ // single
+ val: function () {
+ var val, data = null, self = this;
+
+ if (arguments.length === 0) {
+ return this.opts.element.val();
+ }
+
+ val = arguments[0];
+
+ if (this.select) {
+ this.select
+ .val(val)
+ .find(":selected").each2(function (i, elm) {
+ data = {id: elm.attr("value"), text: elm.text()};
+ return false;
+ });
+ this.updateSelection(data);
+ this.setPlaceholder();
+ } else {
+ if (this.opts.initSelection === undefined) {
+ throw new Error("cannot call val() if initSelection() is not defined");
+ }
+ // val is an id. !val is true for [undefined,null,'']
+ if (!val) {
+ this.clear();
+ return;
+ }
+ this.opts.element.val(val);
+ this.opts.initSelection(this.opts.element, function(data){
+ self.opts.element.val(!data ? "" : self.id(data));
+ self.updateSelection(data);
+ self.setPlaceholder();
+ });
+ }
+ },
+
+ // single
+ clearSearch: function () {
+ this.search.val("");
+ },
+
+ // single
+ data: function(value) {
+ var data;
+
+ if (arguments.length === 0) {
+ data = this.selection.data("select2-data");
+ if (data == undefined) data = null;
+ return data;
+ } else {
+ if (!value || value === "") {
+ this.clear();
+ } else {
+ this.opts.element.val(!value ? "" : this.id(value));
+ this.updateSelection(value);
+ }
+ }
+ }
+ });
+
+ MultiSelect2 = clazz(AbstractSelect2, {
+
+ // multi
+ createContainer: function () {
+ var container = $("
", {
+ "class": "select2-container select2-container-multi"
+ }).html([
+ " " ,
+ ""].join(""));
+ return container;
+ },
+
+ // multi
+ prepareOpts: function () {
+ var opts = this.parent.prepareOpts.apply(this, arguments);
+
+ // TODO validate placeholder is a string if specified
+
+ if (opts.element.get(0).tagName.toLowerCase() === "select") {
+ // install sthe selection initializer
+ opts.initSelection = function (element,callback) {
+
+ var data = [];
+ element.find(":selected").each2(function (i, elm) {
+ data.push({id: elm.attr("value"), text: elm.text()});
+ });
+
+ if ($.isFunction(callback))
+ callback(data);
+ };
+ }
+
+ return opts;
+ },
+
+ // multi
+ initContainer: function () {
+
+ var selector = ".select2-choices", selection;
+
+ this.searchContainer = this.container.find(".select2-search-field");
+ this.selection = selection = this.container.find(selector);
+
+ this.search.bind("keydown", this.bind(function (e) {
+ if (!this.enabled) return;
+
+ if (e.which === KEY.BACKSPACE && this.search.val() === "") {
+ this.close();
+
+ var choices,
+ selected = selection.find(".select2-search-choice-focus");
+ if (selected.length > 0) {
+ this.unselect(selected.first());
+ this.search.width(10);
+ killEvent(e);
+ return;
+ }
+
+ choices = selection.find(".select2-search-choice");
+ if (choices.length > 0) {
+ choices.last().addClass("select2-search-choice-focus");
+ }
+ } else {
+ selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
+ }
+
+ if (this.opened()) {
+ switch (e.which) {
+ case KEY.UP:
+ case KEY.DOWN:
+ this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
+ killEvent(e);
+ return;
+ case KEY.ENTER:
+ case KEY.TAB:
+ this.selectHighlighted();
+ killEvent(e);
+ return;
+ case KEY.ESC:
+ this.cancel(e);
+ killEvent(e);
+ return;
+ }
+ }
+
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
+ || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
+ return;
+ }
+
+ if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
+ return;
+ }
+
+ this.open();
+
+ if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
+ // prevent the page from scrolling
+ killEvent(e);
+ }
+ }));
+
+ this.search.bind("keyup", this.bind(this.resizeSearch));
+
+ this.search.bind("blur", this.bind(function(e) {
+ this.container.removeClass("select2-container-active");
+ this.search.removeClass("select2-focused");
+ this.clearSearch();
+ e.stopImmediatePropagation();
+ }));
+
+ this.container.delegate(selector, "mousedown", this.bind(function (e) {
+ if (!this.enabled) return;
+ if ($(e.target).closest(".select2-search-choice").length > 0) {
+ // clicked inside a select2 search choice, do not open
+ return;
+ }
+ this.clearPlaceholder();
+ this.open();
+ this.focusSearch();
+ e.preventDefault();
+ }));
+
+ this.container.delegate(selector, "focus", this.bind(function () {
+ if (!this.enabled) return;
+ this.container.addClass("select2-container-active");
+ this.dropdown.addClass("select2-drop-active");
+ this.clearPlaceholder();
+ }));
+
+ // set the placeholder if necessary
+ this.clearSearch();
+ },
+
+ // multi
+ enable: function() {
+ if (this.enabled) return;
+
+ this.parent.enable.apply(this, arguments);
+
+ this.search.removeAttr("disabled");
+ },
+
+ // multi
+ disable: function() {
+ if (!this.enabled) return;
+
+ this.parent.disable.apply(this, arguments);
+
+ this.search.attr("disabled", true);
+ },
+
+ // multi
+ initSelection: function () {
+ var data;
+ if (this.opts.element.val() === "") {
+ this.updateSelection([]);
+ this.close();
+ // set the placeholder if necessary
+ this.clearSearch();
+ }
+ if (this.select || this.opts.element.val() !== "") {
+ var self = this;
+ this.opts.initSelection.call(null, this.opts.element, function(data){
+ if (data !== undefined && data !== null) {
+ self.updateSelection(data);
+ self.close();
+ // set the placeholder if necessary
+ self.clearSearch();
+ }
+ });
+ }
+ },
+
+ // multi
+ clearSearch: function () {
+ var placeholder = this.getPlaceholder();
+
+ if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
+ this.search.val(placeholder).addClass("select2-default");
+ // stretch the search box to full width of the container so as much of the placeholder is visible as possible
+ this.resizeSearch();
+ } else {
+ // we set this to " " instead of "" and later clear it on focus() because there is a firefox bug
+ // that does not properly render the caret when the field starts out blank
+ this.search.val(" ").width(10);
+ }
+ },
+
+ // multi
+ clearPlaceholder: function () {
+ if (this.search.hasClass("select2-default")) {
+ this.search.val("").removeClass("select2-default");
+ } else {
+ // work around for the space character we set to avoid firefox caret bug
+ if (this.search.val() === " ") this.search.val("");
+ }
+ },
+
+ // multi
+ opening: function () {
+ this.parent.opening.apply(this, arguments);
+
+ this.clearPlaceholder();
+ this.resizeSearch();
+ this.focusSearch();
+ },
+
+ // multi
+ close: function () {
+ if (!this.opened()) return;
+ this.parent.close.apply(this, arguments);
+ },
+
+ // multi
+ focus: function () {
+ this.close();
+ this.search.focus();
+ },
+
+ // multi
+ isFocused: function () {
+ return this.search.hasClass("select2-focused");
+ },
+
+ // multi
+ updateSelection: function (data) {
+ var ids = [], filtered = [], self = this;
+
+ // filter out duplicates
+ $(data).each(function () {
+ if (indexOf(self.id(this), ids) < 0) {
+ ids.push(self.id(this));
+ filtered.push(this);
+ }
+ });
+ data = filtered;
+
+ this.selection.find(".select2-search-choice").remove();
+ $(data).each(function () {
+ self.addSelectedChoice(this);
+ });
+ self.postprocessResults();
+ },
+
+ tokenize: function() {
+ var input = this.search.val();
+ input = this.opts.tokenizer(input, this.data(), this.bind(this.onSelect), this.opts);
+ if (input != null && input != undefined) {
+ this.search.val(input);
+ if (input.length > 0) {
+ this.open();
+ }
+ }
+
+ },
+
+ // multi
+ onSelect: function (data) {
+ this.addSelectedChoice(data);
+ if (this.select) { this.postprocessResults(); }
+
+ if (this.opts.closeOnSelect) {
+ this.close();
+ this.search.width(10);
+ } else {
+ if (this.countSelectableResults()>0) {
+ this.search.width(10);
+ this.resizeSearch();
+ this.positionDropdown();
+ } else {
+ // if nothing left to select close
+ this.close();
+ }
+ }
+
+ // since its not possible to select an element that has already been
+ // added we do not need to check if this is a new element before firing change
+ this.triggerChange({ added: data });
+
+ this.focusSearch();
+ },
+
+ // multi
+ cancel: function () {
+ this.close();
+ this.focusSearch();
+ },
+
+ // multi
+ addSelectedChoice: function (data) {
+ var choice=$(
+ "" +
+ "
" +
+ " " +
+ " "),
+ id = this.id(data),
+ val = this.getVal(),
+ formatted;
+
+ formatted=this.opts.formatSelection(data, choice);
+ choice.find("div").replaceWith(""+this.opts.escapeMarkup(formatted)+"
");
+ choice.find(".select2-search-choice-close")
+ .bind("mousedown", killEvent)
+ .bind("click dblclick", this.bind(function (e) {
+ if (!this.enabled) return;
+
+ $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){
+ this.unselect($(e.target));
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
+ this.close();
+ this.focusSearch();
+ })).dequeue();
+ killEvent(e);
+ })).bind("focus", this.bind(function () {
+ if (!this.enabled) return;
+ this.container.addClass("select2-container-active");
+ this.dropdown.addClass("select2-drop-active");
+ }));
+
+ choice.data("select2-data", data);
+ choice.insertBefore(this.searchContainer);
+
+ val.push(id);
+ this.setVal(val);
+ },
+
+ // multi
+ unselect: function (selected) {
+ var val = this.getVal(),
+ data,
+ index;
+
+ selected = selected.closest(".select2-search-choice");
+
+ if (selected.length === 0) {
+ throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
+ }
+
+ data = selected.data("select2-data");
+
+ index = indexOf(this.id(data), val);
+
+ if (index >= 0) {
+ val.splice(index, 1);
+ this.setVal(val);
+ if (this.select) this.postprocessResults();
+ }
+ selected.remove();
+ this.triggerChange({ removed: data });
+ },
+
+ // multi
+ postprocessResults: function () {
+ var val = this.getVal(),
+ choices = this.results.find(".select2-result-selectable"),
+ compound = this.results.find(".select2-result-with-children"),
+ self = this;
+
+ choices.each2(function (i, choice) {
+ var id = self.id(choice.data("select2-data"));
+ if (indexOf(id, val) >= 0) {
+ choice.addClass("select2-disabled").removeClass("select2-result-selectable");
+ } else {
+ choice.removeClass("select2-disabled").addClass("select2-result-selectable");
+ }
+ });
+
+ compound.each2(function(i, e) {
+ if (e.find(".select2-result-selectable").length==0) {
+ e.addClass("select2-disabled");
+ } else {
+ e.removeClass("select2-disabled");
+ }
+ });
+
+ choices.each2(function (i, choice) {
+ if (!choice.hasClass("select2-disabled") && choice.hasClass("select2-result-selectable")) {
+ self.highlight(0);
+ return false;
+ }
+ });
+
+ },
+
+ // multi
+ resizeSearch: function () {
+
+ var minimumWidth, left, maxWidth, containerLeft, searchWidth,
+ sideBorderPadding = getSideBorderPadding(this.search);
+
+ minimumWidth = measureTextWidth(this.search) + 10;
+
+ left = this.search.offset().left;
+
+ maxWidth = this.selection.width();
+ containerLeft = this.selection.offset().left;
+
+ searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
+ if (searchWidth < minimumWidth) {
+ searchWidth = maxWidth - sideBorderPadding;
+ }
+
+ if (searchWidth < 40) {
+ searchWidth = maxWidth - sideBorderPadding;
+ }
+ this.search.width(searchWidth);
+ },
+
+ // multi
+ getVal: function () {
+ var val;
+ if (this.select) {
+ val = this.select.val();
+ return val === null ? [] : val;
+ } else {
+ val = this.opts.element.val();
+ return splitVal(val, this.opts.separator);
+ }
+ },
+
+ // multi
+ setVal: function (val) {
+ var unique;
+ if (this.select) {
+ this.select.val(val);
+ } else {
+ unique = [];
+ // filter out duplicates
+ $(val).each(function () {
+ if (indexOf(this, unique) < 0) unique.push(this);
+ });
+ this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
+ }
+ },
+
+ // multi
+ val: function () {
+ var val, data = [], self=this;
+
+ if (arguments.length === 0) {
+ return this.getVal();
+ }
+
+ val = arguments[0];
+
+ if (!val) {
+ this.opts.element.val("");
+ this.updateSelection([]);
+ this.clearSearch();
+ return;
+ }
+
+ // val is a list of ids
+ this.setVal(val);
+
+ if (this.select) {
+ this.select.find(":selected").each(function () {
+ data.push({id: $(this).attr("value"), text: $(this).text()});
+ });
+ this.updateSelection(data);
+ } else {
+ if (this.opts.initSelection === undefined) {
+ throw new Error("val() cannot be called if initSelection() is not defined")
+ }
+
+ this.opts.initSelection(this.opts.element, function(data){
+ var ids=$(data).map(self.id);
+ self.setVal(ids);
+ self.updateSelection(data);
+ self.clearSearch();
+ });
+ }
+ this.clearSearch();
+ },
+
+ // multi
+ onSortStart: function() {
+ if (this.select) {
+ throw new Error("Sorting of elements is not supported when attached to . Attach to instead.");
+ }
+
+ // collapse search field into 0 width so its container can be collapsed as well
+ this.search.width(0);
+ // hide the container
+ this.searchContainer.hide();
+ },
+
+ // multi
+ onSortEnd:function() {
+
+ var val=[], self=this;
+
+ // show search and move it to the end of the list
+ this.searchContainer.show();
+ // make sure the search container is the last item in the list
+ this.searchContainer.appendTo(this.searchContainer.parent());
+ // since we collapsed the width in dragStarted, we resize it here
+ this.resizeSearch();
+
+ // update selection
+
+ this.selection.find(".select2-search-choice").each(function() {
+ val.push(self.opts.id($(this).data("select2-data")));
+ });
+ this.setVal(val);
+ this.triggerChange();
+ },
+
+ // multi
+ data: function(values) {
+ var self=this, ids;
+ if (arguments.length === 0) {
+ return this.selection
+ .find(".select2-search-choice")
+ .map(function() { return $(this).data("select2-data"); })
+ .get();
+ } else {
+ if (!values) { values = []; }
+ ids = $.map(values, function(e) { return self.opts.id(e)});
+ this.setVal(ids);
+ this.updateSelection(values);
+ this.clearSearch();
+ }
+ }
+ });
+
+ $.fn.select2 = function () {
+
+ var args = Array.prototype.slice.call(arguments, 0),
+ opts,
+ select2,
+ value, multiple, allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable", "positionDropdown", "data"];
+
+ this.each(function () {
+ if (args.length === 0 || typeof(args[0]) === "object") {
+ opts = args.length === 0 ? {} : $.extend({}, args[0]);
+ opts.element = $(this);
+
+ if (opts.element.get(0).tagName.toLowerCase() === "select") {
+ multiple = opts.element.attr("multiple");
+ } else {
+ multiple = opts.multiple || false;
+ if ("tags" in opts) {opts.multiple = multiple = true;}
+ }
+
+ select2 = multiple ? new MultiSelect2() : new SingleSelect2();
+ select2.init(opts);
+ } else if (typeof(args[0]) === "string") {
+
+ if (indexOf(args[0], allowedMethods) < 0) {
+ throw "Unknown method: " + args[0];
+ }
+
+ value = undefined;
+ select2 = $(this).data("select2");
+ if (select2 === undefined) return;
+ if (args[0] === "container") {
+ value=select2.container;
+ } else {
+ value = select2[args[0]].apply(select2, args.slice(1));
+ }
+ if (value !== undefined) {return false;}
+ } else {
+ throw "Invalid arguments to select2 plugin: " + args;
+ }
+ });
+ return (value === undefined) ? this : value;
+ };
+
+ // plugin defaults, accessible to users
+ $.fn.select2.defaults = {
+ width: "copy",
+ closeOnSelect: true,
+ openOnEnter: true,
+ containerCss: {},
+ dropdownCss: {},
+ containerCssClass: "",
+ dropdownCssClass: "",
+ formatResult: function(result, container, query) {
+ var markup=[];
+ markMatch(result.text, query.term, markup);
+ return markup.join("");
+ },
+ formatSelection: function (data, container) {
+ return data.text;
+ },
+ formatResultCssClass: function(data) {return undefined;},
+ formatNoMatches: function () { return "No matches found"; },
+ formatInputTooShort: function (input, min) { return "Please enter " + (min - input.length) + " more characters"; },
+ formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
+ formatLoadMore: function (pageNumber) { return "Loading more results..."; },
+ formatSearching: function () { return "Searching..."; },
+ minimumResultsForSearch: 0,
+ minimumInputLength: 0,
+ maximumSelectionSize: 0,
+ id: function (e) { return e.id; },
+ matcher: function(term, text) {
+ return text.toUpperCase().indexOf(term.toUpperCase()) >= 0;
+ },
+ separator: ",",
+ tokenSeparators: [],
+ tokenizer: defaultTokenizer,
+ escapeMarkup: function (markup) {
+ if (markup && typeof(markup) === "string") {
+ return markup.replace(/&/g, "&");
+ }
+ return markup;
+ },
+ blurOnChange: false
+ };
+
+ // exports
+ window.Select2 = {
+ query: {
+ ajax: ajax,
+ local: local,
+ tags: tags
+ }, util: {
+ debounce: debounce,
+ markMatch: markMatch
+ }, "class": {
+ "abstract": AbstractSelect2,
+ "single": SingleSelect2,
+ "multi": MultiSelect2
+ }
+ };
+
+}(jQuery));
diff --git a/addons/base_import/static/lib/select2/select2.png b/addons/base_import/static/lib/select2/select2.png
new file mode 100644
index 00000000000..1d804ffb996
Binary files /dev/null and b/addons/base_import/static/lib/select2/select2.png differ
diff --git a/addons/base_import/static/lib/select2/select2x2.png b/addons/base_import/static/lib/select2/select2x2.png
new file mode 100644
index 00000000000..4bdd5c961d4
Binary files /dev/null and b/addons/base_import/static/lib/select2/select2x2.png differ
diff --git a/addons/base_import/static/lib/select2/spinner.gif b/addons/base_import/static/lib/select2/spinner.gif
new file mode 100755
index 00000000000..5b33f7e54f4
Binary files /dev/null and b/addons/base_import/static/lib/select2/spinner.gif differ
diff --git a/addons/base_import/static/src/css/import.css b/addons/base_import/static/src/css/import.css
new file mode 100644
index 00000000000..f6258f5aa50
--- /dev/null
+++ b/addons/base_import/static/src/css/import.css
@@ -0,0 +1,57 @@
+.openerp .oe_list_buttons .oe_alternative {
+ visibility: visible;
+}
+.openerp .oe_list_buttons.oe_editing .oe_list_button_import {
+ display: none;
+}
+
+.oe_import dd,
+.oe_import .oe_import_toggled,
+.oe_import .oe_import_grid,
+.oe_import .oe_import_error_report,
+.oe_import .oe_import_with_file,
+.oe_import .oe_import_noheaders {
+ display: none;
+}
+
+.oe_import.oe_import_preview .oe_import_grid {
+ display: table;
+}
+.oe_import.oe_import_error .oe_import_error_report,
+.oe_import.oe_import_with_file .oe_import_with_file,
+.oe_import.oe_import_noheaders .oe_import_noheaders {
+ display: block;
+}
+
+.oe_import .oe_import_error_report ul .oe_import_report_error {
+ background-color: #FFD9DB;
+}
+.oe_import .oe_import_error_report ul .oe_import_report_warning {
+ background-color: #FEFFD9;
+}
+
+.oe_import .oe_import_noheaders {
+ color: #888;
+}
+
+.oe_import a.oe_import_toggle {
+ display: block;
+}
+.oe_import a.oe_import_toggle:before {
+ content: '> '
+}
+
+.oe_import .oe_import_options p {
+ margin: 0;
+ padding: 0;
+}
+.oe_import .oe_import_options label {
+ display: inline-block;
+ width: 10em;
+ text-align: right;
+}
+
+.oe_import_selector ul,
+.oe_import_selector li {
+ margin: 0; padding: 0;
+}
diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js
new file mode 100644
index 00000000000..c2deb660ff2
--- /dev/null
+++ b/addons/base_import/static/src/js/import.js
@@ -0,0 +1,291 @@
+openerp.base_import = function (instance) {
+ var QWeb = instance.web.qweb;
+ var _t = instance.web._t;
+ var _lt = instance.web._lt;
+
+ /**
+ * Safari does not deal well at all with raw JSON data being
+ * returned. As a result, we're going to cheat by using a
+ * pseudo-jsonp: instead of getting JSON data in the iframe, we're
+ * getting a ``script`` tag which consists of a function call and
+ * the returned data (the json dump).
+ *
+ * The function is an auto-generated name bound to ``window``,
+ * which calls back into the callback provided here.
+ *
+ * @param {Object} form the form element (DOM or jQuery) to use in the call
+ * @param {Object} attributes jquery.form attributes object
+ * @param {Function} callback function to call with the returned data
+ */
+ function jsonp(form, attributes, callback) {
+ attributes = attributes || {};
+ var options = {jsonp: _.uniqueId('import_callback_')};
+ window[options.jsonp] = function () {
+ delete window[options.jsonp];
+ callback.apply(null, arguments);
+ };
+ if ('data' in attributes) {
+ _.extend(attributes.data, options);
+ } else {
+ _.extend(attributes, {data: options});
+ }
+ _.extend(attributes, {
+ dataType: 'script',
+ });
+ $(form).ajaxSubmit(attributes);
+ }
+
+ // if true, the 'Import', 'Export', etc... buttons will be shown
+ instance.web.ListView.prototype.defaults.import_enabled = true;
+ instance.web.ListView.include({
+ on_loaded: function () {
+ var self = this;
+ var add_button = false;
+ if (!this.$buttons) {
+ add_button = true;
+ }
+ this._super.apply(this, arguments);
+ if(add_button) {
+ this.$buttons.on('click', '.oe_list_button_import', function() {
+ new instance.web.DataImport(self, self.dataset).open();
+ return false;
+ });
+ }
+ }
+ });
+
+ instance.web.DataImport = instance.web.Dialog.extend({
+ template: 'ImportView',
+ dialog_title: _lt("Import Data"),
+ opts: [
+ {name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'},
+ {name: 'separator', label: _lt("Separator:"), value: ','},
+ {name: 'quoting', label: _lt("Quoting:"), value: '"'}
+ ],
+ events: {
+ 'change .oe_import_grid input': 'import_dryrun',
+ 'change input.oe_import_file': 'file_update',
+ 'change input.oe_import_has_header, .oe_import_options input': 'settings_updated',
+ 'click a.oe_import_csv': function (e) {
+ e.preventDefault();
+ },
+ 'click a.oe_import_export': function (e) {
+ e.preventDefault();
+ },
+ 'click a.oe_import_toggle': function (e) {
+ e.preventDefault();
+ var $el = $(e.target);
+ ($el.next().length
+ ? $el.next()
+ : $el.parent().next())
+ .toggle();
+ }
+ },
+ init: function (parent, dataset) {
+ var self = this;
+ this._super(parent, {
+ buttons: [
+ {text: _t("Import File"), click: function () {
+ self.do_import();
+ }, 'class': 'oe_import_dialog_button'}
+ ]
+ });
+ this.res_model = parent.model;
+ // import object id
+ this.id = null;
+ this.Import = new instance.web.Model('base_import.import');
+ },
+ start: function () {
+ var self = this;
+ return this.Import.call('create', [{
+ 'res_model': this.res_model
+ }]).then(function (id) {
+ self.id = id;
+ self.$('input[name=import_id]').val(id);
+ });
+ },
+
+ import_options: function () {
+ var self = this;
+ var options = {
+ headers: this.$('input.oe_import_has_header').prop('checked')
+ };
+ _(this.opts).each(function (opt) {
+ options[opt.name] =
+ self.$('input.oe_import_' + opt.name).val();
+ });
+ return options;
+ },
+
+ //- File & settings change section
+ file_update: function (e) {
+ if (!this.$('input.oe_import_file').val()) { return; }
+
+ this.$el.removeClass('oe_import_preview oe_import_error');
+ jsonp(this.$el, {
+ url: '/base_import/set_file'
+ }, this.proxy('settings_updated'));
+ },
+ settings_updated: function () {
+ this.$el.addClass('oe_import_with_file');
+ // TODO: test that write // succeeded?
+ this.Import.call(
+ 'parse_preview', [this.id, this.import_options()])
+ .then(this.proxy('preview'));
+ },
+ preview: function (result) {
+ this.$el.toggleClass(
+ 'oe_import_noheaders',
+ !this.$('input.oe_import_has_header').prop('checked'));
+ if (result.error) {
+ this.$el.addClass('oe_import_error');
+ this.$('.oe_import_error_report').html(
+ QWeb.render('ImportView.preview.error', result));
+ return;
+ }
+ this.$el.addClass('oe_import_preview');
+ this.$('table').html(QWeb.render('ImportView.preview', result));
+
+ var $fields = this.$('.oe_import_fields input');
+ this.render_fields_matches(result, $fields);
+ var data = this.generate_fields_completion(result);
+ var item_finder = function (id, items) {
+ items = items || data;
+ for (var i=0; i < items.length; ++i) {
+ var item = items[i];
+ if (item.id === id) {
+ return item;
+ }
+ var val;
+ if (item.children && (val = item_finder(id, item.children))) {
+ return val;
+ }
+ }
+ return '';
+ };
+ $fields.select2({
+ allowClear: true,
+ minimumInputLength: 0,
+ data: data,
+ initSelection: function (element, callback) {
+ var default_value = element.val();
+ if (!default_value) {
+ callback('');
+ return;
+ }
+
+ callback(item_finder(default_value));
+ },
+
+ width: 'resolve',
+ dropdownCssClass: 'oe_import_selector'
+ });
+ this.import_dryrun();
+ },
+ generate_fields_completion: function (root) {
+ var basic = [];
+ var regulars = [];
+ var o2m = [];
+ function traverse(field, ancestors, collection) {
+ var subfields = field.fields;
+ var field_path = ancestors.concat(field);
+ var label = _(field_path).pluck('string').join(' / ');
+ var id = _(field_path).pluck('name').join('/');
+
+ // If non-relational, m2o or m2m, collection is regulars
+ if (!collection) {
+ if (field.name === 'id') {
+ collection = basic
+ } else if (_.isEmpty(subfields)
+ || _.isEqual(_.pluck(subfields, 'name'), ['id', '.id'])) {
+ collection = regulars;
+ } else {
+ collection = o2m;
+ }
+ }
+
+ collection.push({
+ id: id,
+ text: label,
+ required: field.required
+ });
+
+ for(var i=0, end=subfields.length; i
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Import preview failed due to:
+ Here is the start of the file we could not import:
+
+
+
+
+
+ this.attr('t-if', 'widget.options.import_enabled');
+
+
+ Import
+
+
+
diff --git a/addons/base_import/test_models.py b/addons/base_import/test_models.py
new file mode 100644
index 00000000000..0720ae11eda
--- /dev/null
+++ b/addons/base_import/test_models.py
@@ -0,0 +1,101 @@
+from openerp.osv import orm, fields
+
+def name(n): return 'base_import.tests.models.%s' % n
+
+class char(orm.Model):
+ _name = name('char')
+
+ _columns = {
+ 'value': fields.char('unknown', size=None)
+ }
+
+class char_required(orm.Model):
+ _name = name('char.required')
+
+ _columns = {
+ 'value': fields.char('unknown', size=None, required=True)
+ }
+
+class char_readonly(orm.Model):
+ _name = name('char.readonly')
+
+ _columns = {
+ 'value': fields.char('unknown', size=None, readonly=True)
+ }
+
+class char_states(orm.Model):
+ _name = name('char.states')
+
+ _columns = {
+ 'value': fields.char('unknown', size=None, readonly=True, states={'draft': [('readonly', False)]})
+ }
+
+class char_noreadonly(orm.Model):
+ _name = name('char.noreadonly')
+
+ _columns = {
+ 'value': fields.char('unknown', size=None, readonly=True, states={'draft': [('invisible', True)]})
+ }
+
+class char_stillreadonly(orm.Model):
+ _name = name('char.stillreadonly')
+
+ _columns = {
+ 'value': fields.char('unknown', size=None, readonly=True, states={'draft': [('readonly', True)]})
+ }
+
+# TODO: complex field (m2m, o2m, m2o)
+class m2o(orm.Model):
+ _name = name('m2o')
+
+ _columns = {
+ 'value': fields.many2one(name('m2o.related'))
+ }
+class m2o_related(orm.Model):
+ _name = name('m2o.related')
+
+ _columns = {
+ 'value': fields.integer()
+ }
+ _defaults = {
+ 'value': 42
+ }
+
+class m2o_required(orm.Model):
+ _name = name('m2o.required')
+
+ _columns = {
+ 'value': fields.many2one(name('m2o.required.related'), required=True)
+ }
+class m2o_required_related(orm.Model):
+ _name = name('m2o.required.related')
+
+ _columns = {
+ 'value': fields.integer()
+ }
+ _defaults = {
+ 'value': 42
+ }
+
+class o2m(orm.Model):
+ _name = name('o2m')
+
+ _columns = {
+ 'value': fields.one2many(name('o2m.child'), 'parent_id')
+ }
+class o2m_child(orm.Model):
+ _name = name('o2m.child')
+
+ _columns = {
+ 'parent_id': fields.many2one(name('o2m')),
+ 'value': fields.integer()
+ }
+
+class preview_model(orm.Model):
+ _name = name('preview')
+
+ _columns = {
+ 'name': fields.char('Name', size=None),
+ 'somevalue': fields.integer('Some Value', required=True),
+ 'othervalue': fields.integer('Other Variable'),
+ }
diff --git a/addons/base_import/tests/__init__.py b/addons/base_import/tests/__init__.py
new file mode 100644
index 00000000000..abef28c8ca1
--- /dev/null
+++ b/addons/base_import/tests/__init__.py
@@ -0,0 +1,3 @@
+from . import test_cases
+
+checks = [test_cases]
diff --git a/addons/base_import/tests/test_cases.py b/addons/base_import/tests/test_cases.py
new file mode 100644
index 00000000000..5479ae94428
--- /dev/null
+++ b/addons/base_import/tests/test_cases.py
@@ -0,0 +1,342 @@
+# -*- encoding: utf-8 -*-
+import unittest2
+from openerp.tests.common import TransactionCase
+
+from .. import models
+
+ID_FIELD = {'id': 'id', 'name': 'id', 'string': "External ID", 'required': False, 'fields': []}
+def make_field(name='value', string='unknown', required=False, fields=[]):
+ return [
+ ID_FIELD,
+ {'id': name, 'name': name, 'string': string, 'required': required, 'fields': fields},
+ ]
+
+class test_basic_fields(TransactionCase):
+ def get_fields(self, field):
+ return self.registry('base_import.import')\
+ .get_fields(self.cr, self.uid, 'base_import.tests.models.' + field)
+
+ def test_base(self):
+ """ A basic field is not required """
+ self.assertEqual(self.get_fields('char'), make_field())
+
+ def test_required(self):
+ """ Required fields should be flagged (so they can be fill-required) """
+ self.assertEqual(self.get_fields('char.required'), make_field(required=True))
+
+ def test_readonly(self):
+ """ Readonly fields should be filtered out"""
+ self.assertEqual(self.get_fields('char.readonly'), [ID_FIELD])
+
+ def test_readonly_states(self):
+ """ Readonly fields with states should not be filtered out"""
+ self.assertEqual(self.get_fields('char.states'), make_field())
+
+ def test_readonly_states_noreadonly(self):
+ """ Readonly fields with states having nothing to do with
+ readonly should still be filtered out"""
+ self.assertEqual(self.get_fields('char.noreadonly'), [ID_FIELD])
+
+ def test_readonly_states_stillreadonly(self):
+ """ Readonly fields with readonly states leaving them readonly
+ always... filtered out"""
+ self.assertEqual(self.get_fields('char.stillreadonly'), [ID_FIELD])
+
+ def test_m2o(self):
+ """ M2O fields should allow import of themselves (name_get),
+ their id and their xid"""
+ self.assertEqual(self.get_fields('m2o'), make_field(fields=[
+ {'id': 'value', 'name': 'id', 'string': 'External ID', 'required': False, 'fields': []},
+ {'id': 'value', 'name': '.id', 'string': 'Database ID', 'required': False, 'fields': []},
+ ]))
+
+ def test_m2o_required(self):
+ """ If an m2o field is required, its three sub-fields are
+ required as well (the client has to handle that: requiredness
+ is id-based)
+ """
+ self.assertEqual(self.get_fields('m2o.required'), make_field(required=True, fields=[
+ {'id': 'value', 'name': 'id', 'string': 'External ID', 'required': True, 'fields': []},
+ {'id': 'value', 'name': '.id', 'string': 'Database ID', 'required': True, 'fields': []},
+ ]))
+
+class test_o2m(TransactionCase):
+ def get_fields(self, field):
+ return self.registry('base_import.import')\
+ .get_fields(self.cr, self.uid, 'base_import.tests.models.' + field)
+
+ def test_shallow(self):
+ self.assertEqual(self.get_fields('o2m'), make_field(fields=[
+ {'id': 'id', 'name': 'id', 'string': 'External ID', 'required': False, 'fields': []},
+ # FIXME: should reverse field be ignored?
+ {'id': 'parent_id', 'name': 'parent_id', 'string': 'unknown', 'required': False, 'fields': [
+ {'id': 'parent_id', 'name': 'id', 'string': 'External ID', 'required': False, 'fields': []},
+ {'id': 'parent_id', 'name': '.id', 'string': 'Database ID', 'required': False, 'fields': []},
+ ]},
+ {'id': 'value', 'name': 'value', 'string': 'unknown', 'required': False, 'fields': []},
+ ]))
+
+class test_match_headers_single(TransactionCase):
+ def test_match_by_name(self):
+ match = self.registry('base_import.import')._match_header(
+ 'f0', [{'name': 'f0'}], {})
+
+ self.assertEqual(match, [{'name': 'f0'}])
+
+ def test_match_by_string(self):
+ match = self.registry('base_import.import')._match_header(
+ 'some field', [{'name': 'bob', 'string': "Some Field"}], {})
+
+ self.assertEqual(match, [{'name': 'bob', 'string': "Some Field"}])
+
+ def test_nomatch(self):
+ match = self.registry('base_import.import')._match_header(
+ 'should not be', [{'name': 'bob', 'string': "wheee"}], {})
+
+ self.assertEqual(match, [])
+
+ def test_recursive_match(self):
+ f = {
+ 'name': 'f0',
+ 'string': "My Field",
+ 'fields': [
+ {'name': 'f0', 'string': "Sub field 0", 'fields': []},
+ {'name': 'f1', 'string': "Sub field 2", 'fields': []},
+ ]
+ }
+ match = self.registry('base_import.import')._match_header(
+ 'f0/f1', [f], {})
+
+ self.assertEqual(match, [f, f['fields'][1]])
+
+ def test_recursive_nomatch(self):
+ """ Match first level, fail to match second level
+ """
+ f = {
+ 'name': 'f0',
+ 'string': "My Field",
+ 'fields': [
+ {'name': 'f0', 'string': "Sub field 0", 'fields': []},
+ {'name': 'f1', 'string': "Sub field 2", 'fields': []},
+ ]
+ }
+ match = self.registry('base_import.import')._match_header(
+ 'f0/f2', [f], {})
+
+ self.assertEqual(match, [])
+
+class test_match_headers_multiple(TransactionCase):
+ def test_noheaders(self):
+ self.assertEqual(
+ self.registry('base_import.import')._match_headers(
+ [], [], {}),
+ (None, None)
+ )
+ def test_nomatch(self):
+ self.assertEqual(
+ self.registry('base_import.import')._match_headers(
+ iter([
+ ['foo', 'bar', 'baz', 'qux'],
+ ['v1', 'v2', 'v3', 'v4'],
+ ]),
+ [],
+ {'headers': True}),
+ (
+ ['foo', 'bar', 'baz', 'qux'],
+ dict.fromkeys(range(4))
+ )
+ )
+
+ def test_mixed(self):
+ self.assertEqual(
+ self.registry('base_import.import')._match_headers(
+ iter(['foo bar baz qux/corge'.split()]),
+ [
+ {'name': 'bar', 'string': 'Bar'},
+ {'name': 'bob', 'string': 'Baz'},
+ {'name': 'qux', 'string': 'Qux', 'fields': [
+ {'name': 'corge', 'fields': []},
+ ]}
+ ],
+ {'headers': True}),
+ (['foo', 'bar', 'baz', 'qux/corge'], {
+ 0: None,
+ 1: ['bar'],
+ 2: ['bob'],
+ 3: ['qux', 'corge'],
+ })
+ )
+
+class test_preview(TransactionCase):
+ def make_import(self):
+ Import = self.registry('base_import.import')
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'res.users',
+ 'file': u"로그인,언어\nbob,1\n".encode('euc_kr'),
+ })
+ return Import, id
+
+ def test_encoding(self):
+ Import, id = self.make_import()
+ result = Import.parse_preview(self.cr, self.uid, id, {
+ 'quoting': '"',
+ 'separator': ',',
+ })
+ self.assertTrue('error' in result)
+
+ def test_csv_errors(self):
+ Import, id = self.make_import()
+
+ result = Import.parse_preview(self.cr, self.uid, id, {
+ 'quoting': 'foo',
+ 'separator': ',',
+ 'encoding': 'euc_kr',
+ })
+ self.assertTrue('error' in result)
+
+ def test_csv_errors(self):
+ Import, id = self.make_import()
+
+ result = Import.parse_preview(self.cr, self.uid, id, {
+ 'quoting': '"',
+ 'separator': 'bob',
+ 'encoding': 'euc_kr',
+ })
+ self.assertTrue('error' in result)
+
+ def test_success(self):
+ Import = self.registry('base_import.import')
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'base_import.tests.models.preview',
+ 'file': 'name,Some Value,Counter\n'
+ 'foo,1,2\n'
+ 'bar,3,4\n'
+ 'qux,5,6\n'
+ })
+
+ result = Import.parse_preview(self.cr, self.uid, id, {
+ 'quoting': '"',
+ 'separator': ',',
+ 'headers': True,
+ })
+
+ self.assertEqual(result['matches'], {0: ['name'], 1: ['somevalue'], 2: None})
+ self.assertEqual(result['headers'], ['name', 'Some Value', 'Counter'])
+ # Order depends on iteration order of fields_get
+ self.assertItemsEqual(result['fields'], [
+ {'id': 'id', 'name': 'id', 'string': 'External ID', 'required':False, 'fields': []},
+ {'id': 'name', 'name': 'name', 'string': 'Name', 'required':False, 'fields': []},
+ {'id': 'somevalue', 'name': 'somevalue', 'string': 'Some Value', 'required':True, 'fields': []},
+ {'id': 'othervalue', 'name': 'othervalue', 'string': 'Other Variable', 'required':False, 'fields': []},
+ ])
+ self.assertEqual(result['preview'], [
+ ['foo', '1', '2'],
+ ['bar', '3', '4'],
+ ['qux', '5', '6'],
+ ])
+ # Ensure we only have the response fields we expect
+ self.assertItemsEqual(result.keys(), ['matches', 'headers', 'fields', 'preview'])
+
+class test_convert_import_data(TransactionCase):
+ """ Tests conversion of base_import.import input into data which
+ can be fed to Model.import_data
+ """
+ def test_all(self):
+ Import = self.registry('base_import.import')
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'base_import.tests.models.preview',
+ 'file': 'name,Some Value,Counter\n'
+ 'foo,1,2\n'
+ 'bar,3,4\n'
+ 'qux,5,6\n'
+ })
+ record = Import.browse(self.cr, self.uid, id)
+ data, fields = Import._convert_import_data(
+ record, ['name', 'somevalue', 'othervalue'],
+ {'quoting': '"', 'separator': ',', 'headers': True,})
+
+ self.assertItemsEqual(fields, ['name', 'somevalue', 'othervalue'])
+ self.assertItemsEqual(data, [
+ ('foo', '1', '2'),
+ ('bar', '3', '4'),
+ ('qux', '5', '6'),
+ ])
+
+ def test_filtered(self):
+ """ If ``False`` is provided as field mapping for a column,
+ that column should be removed from importable data
+ """
+ Import = self.registry('base_import.import')
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'base_import.tests.models.preview',
+ 'file': 'name,Some Value,Counter\n'
+ 'foo,1,2\n'
+ 'bar,3,4\n'
+ 'qux,5,6\n'
+ })
+ record = Import.browse(self.cr, self.uid, id)
+ data, fields = Import._convert_import_data(
+ record, ['name', False, 'othervalue'],
+ {'quoting': '"', 'separator': ',', 'headers': True,})
+
+ self.assertItemsEqual(fields, ['name', 'othervalue'])
+ self.assertItemsEqual(data, [
+ ('foo', '2'),
+ ('bar', '4'),
+ ('qux', '6'),
+ ])
+
+ def test_norow(self):
+ """ If a row is composed only of empty values (due to having
+ filtered out non-empty values from it), it should be removed
+ """
+ Import = self.registry('base_import.import')
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'base_import.tests.models.preview',
+ 'file': 'name,Some Value,Counter\n'
+ 'foo,1,2\n'
+ ',3,\n'
+ ',5,6\n'
+ })
+ record = Import.browse(self.cr, self.uid, id)
+ data, fields = Import._convert_import_data(
+ record, ['name', False, 'othervalue'],
+ {'quoting': '"', 'separator': ',', 'headers': True,})
+
+ self.assertItemsEqual(fields, ['name', 'othervalue'])
+ self.assertItemsEqual(data, [
+ ('foo', '2'),
+ ('', '6'),
+ ])
+
+ def test_nofield(self):
+ Import = self.registry('base_import.import')
+
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'base_import.tests.models.preview',
+ 'file': 'name,Some Value,Counter\n'
+ 'foo,1,2\n'
+ })
+
+ record = Import.browse(self.cr, self.uid, id)
+ self.assertRaises(
+ ValueError,
+ Import._convert_import_data,
+ record, [],
+ {'quoting': '"', 'separator': ',', 'headers': True,})
+
+ def test_falsefields(self):
+ Import = self.registry('base_import.import')
+
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'base_import.tests.models.preview',
+ 'file': 'name,Some Value,Counter\n'
+ 'foo,1,2\n'
+ })
+
+ record = Import.browse(self.cr, self.uid, id)
+ self.assertRaises(
+ ValueError,
+ Import._convert_import_data,
+ record, [False, False, False],
+ {'quoting': '"', 'separator': ',', 'headers': True,})
diff --git a/addons/base_module_record/i18n/nb.po b/addons/base_module_record/i18n/nb.po
new file mode 100644
index 00000000000..8465596f21b
--- /dev/null
+++ b/addons/base_module_record/i18n/nb.po
@@ -0,0 +1,271 @@
+# Norwegian Bokmal 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 , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-04 13:40+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-05 04:46+0000\n"
+"X-Generator: Launchpad (build 15901)\n"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,info,category:0
+msgid "Category"
+msgstr "Kategori"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_objects,save:0
+msgid "Information"
+msgstr "Informasjon"
+
+#. module: base_module_record
+#: model:ir.model,name:base_module_record.model_ir_module_record
+msgid "ir.module.record"
+msgstr "ir.modul.opptak"
+
+#. module: base_module_record
+#: wizard_button:base_module_record.module_record_data,info,end:0
+#: wizard_button:base_module_record.module_record_data,save_yaml,end:0
+msgid "End"
+msgstr "Slutt"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_data,init:0
+#: wizard_view:base_module_record.module_record_objects,init:0
+msgid "Choose objects to record"
+msgstr "Velg objekter til opptak"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,info,author:0
+msgid "Author"
+msgstr "Forfatter"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,info,directory_name:0
+msgid "Directory Name"
+msgstr "Navn på katalog"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_data,init,filter_cond:0
+#: wizard_field:base_module_record.module_record_objects,init,filter_cond:0
+msgid "Records only"
+msgstr "Bare opptak"
+
+#. module: base_module_record
+#: selection:base_module_record.module_record_objects,info,data_kind:0
+msgid "Demo Data"
+msgstr "Demo data"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,save,module_filename:0
+msgid "Filename"
+msgstr "Filnavn"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,info,version:0
+msgid "Version"
+msgstr "Versjon"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_data,info:0
+#: wizard_view:base_module_record.module_record_data,init:0
+#: wizard_view:base_module_record.module_record_data,save_yaml:0
+#: wizard_view:base_module_record.module_record_objects,init:0
+msgid "Objects Recording"
+msgstr "objekter Innspilling"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_objects,save:0
+msgid ""
+"If you think your module could interest other people, we'd like you to "
+"publish it on http://www.openerp.com, in the 'Modules' section. You can do "
+"it through the website or using features of the 'base_module_publish' module."
+msgstr ""
+"Hvis du tror din modul kan interessere andre mennesker, vil vi gjerne at du "
+"publisere den på http://www.openerp.com, i 'Moduler-delen. Du kan gjøre det "
+"gjennom nettstedet eller bruke funksjonene i «base_module_publish»-modulen."
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_data,init,check_date:0
+#: wizard_field:base_module_record.module_record_objects,init,check_date:0
+msgid "Record from Date"
+msgstr "Dato fra opptak"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_data,end:0
+#: wizard_view:base_module_record.module_record_objects,end:0
+#: wizard_view:base_module_record.module_record_objects,info:0
+#: wizard_view:base_module_record.module_record_objects,save:0
+#: wizard_view:base_module_record.module_record_objects,save_yaml:0
+msgid "Module Recording"
+msgstr "Modul innspilling"
+
+#. module: base_module_record
+#: model:ir.actions.wizard,name:base_module_record.wizard_base_module_record_objects
+#: model:ir.ui.menu,name:base_module_record.menu_wizard_base_module_record_objects
+msgid "Export Customizations As a Module"
+msgstr "Eksporter Tilpasninger som en modul"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_objects,save:0
+msgid "Thanks in advance for your contribution."
+msgstr "Takk på forhånd for ditt bidrag."
+
+#. module: base_module_record
+#: help:base_module_record.module_record_data,init,objects:0
+#: help:base_module_record.module_record_objects,init,objects:0
+msgid "List of objects to be recorded"
+msgstr "Liste over objekter som skal spilles inn"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,info,description:0
+msgid "Full Description"
+msgstr "Full beskrivelse"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,info,name:0
+msgid "Module Name"
+msgstr "Modulnavn"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_data,init,objects:0
+#: wizard_field:base_module_record.module_record_objects,init,objects:0
+msgid "Objects"
+msgstr "Objekter"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,save,module_file:0
+#: wizard_field:base_module_record.module_record_objects,save_yaml,yaml_file:0
+msgid "Module .zip File"
+msgstr "Modul .zip fil"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_objects,save:0
+msgid "Module successfully created!"
+msgstr ""
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_objects,save_yaml:0
+msgid "YAML file successfully created !"
+msgstr "YAML fil opprettet!"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_data,info:0
+#: wizard_view:base_module_record.module_record_data,save_yaml:0
+msgid "Result, paste this to your module's xml"
+msgstr "Resultatet, lim denne til modulen xml"
+
+#. module: base_module_record
+#: selection:base_module_record.module_record_data,init,filter_cond:0
+#: selection:base_module_record.module_record_objects,init,filter_cond:0
+msgid "Created"
+msgstr "Opprettet"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_data,end:0
+#: wizard_view:base_module_record.module_record_objects,end:0
+msgid "Thanks For using Module Recorder"
+msgstr "Takk for at du brukte Modul opptaker."
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,info,website:0
+msgid "Documentation URL"
+msgstr "Dokumentasjon URL"
+
+#. module: base_module_record
+#: selection:base_module_record.module_record_data,init,filter_cond:0
+#: selection:base_module_record.module_record_objects,init,filter_cond:0
+msgid "Modified"
+msgstr "Modifisert"
+
+#. module: base_module_record
+#: wizard_button:base_module_record.module_record_data,init,record:0
+#: wizard_button:base_module_record.module_record_objects,init,record:0
+msgid "Record"
+msgstr "Opptak"
+
+#. module: base_module_record
+#: wizard_button:base_module_record.module_record_objects,info,save:0
+msgid "Continue"
+msgstr "Fortsett"
+
+#. module: base_module_record
+#: model:ir.actions.wizard,name:base_module_record.wizard_base_module_record_data
+#: model:ir.ui.menu,name:base_module_record.menu_wizard_base_module_record_data
+msgid "Export Customizations As Data File"
+msgstr "Eksport Tilpasninger Som datafil"
+
+#. module: base_module_record
+#: code:addons/base_module_record/wizard/base_module_save.py:129
+#, python-format
+msgid "Error"
+msgstr "Feil"
+
+#. module: base_module_record
+#: selection:base_module_record.module_record_objects,info,data_kind:0
+msgid "Normal Data"
+msgstr "Normal data"
+
+#. module: base_module_record
+#: wizard_button:base_module_record.module_record_data,end,end:0
+#: wizard_button:base_module_record.module_record_objects,end,end:0
+msgid "OK"
+msgstr "Ok"
+
+#. module: base_module_record
+#: model:ir.ui.menu,name:base_module_record.menu_wizard_base_mod_rec
+msgid "Module Creation"
+msgstr "modul Skapelsen"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_objects,info,data_kind:0
+msgid "Type of Data"
+msgstr "Type data"
+
+#. module: base_module_record
+#: wizard_view:base_module_record.module_record_objects,info:0
+msgid "Module Information"
+msgstr "Modul informasjon"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_data,init,info_yaml:0
+#: wizard_field:base_module_record.module_record_objects,init,info_yaml:0
+msgid "YAML"
+msgstr "YAML"
+
+#. module: base_module_record
+#: wizard_field:base_module_record.module_record_data,info,res_text:0
+#: wizard_field:base_module_record.module_record_data,save_yaml,res_text:0
+msgid "Result"
+msgstr "Resultat"
+
+#. module: base_module_record
+#: wizard_button:base_module_record.module_record_data,init,end:0
+#: wizard_button:base_module_record.module_record_objects,info,end:0
+#: wizard_button:base_module_record.module_record_objects,init,end:0
+msgid "Cancel"
+msgstr "Kanseller"
+
+#. module: base_module_record
+#: wizard_button:base_module_record.module_record_objects,save,end:0
+#: wizard_button:base_module_record.module_record_objects,save_yaml,end:0
+msgid "Close"
+msgstr "Lukke"
+
+#. module: base_module_record
+#: selection:base_module_record.module_record_data,init,filter_cond:0
+#: selection:base_module_record.module_record_objects,init,filter_cond:0
+msgid "Created & Modified"
+msgstr "Laget & Modifisert"
+
+#~ msgid "Module successfully created !"
+#~ msgstr "Modulen opprettet!"
diff --git a/addons/base_report_designer/i18n/nb.po b/addons/base_report_designer/i18n/nb.po
new file mode 100644
index 00000000000..9bf2b572a5f
--- /dev/null
+++ b/addons/base_report_designer/i18n/nb.po
@@ -0,0 +1,204 @@
+# Norwegian Bokmal 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 , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-04 13:59+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-05 04:46+0000\n"
+"X-Generator: Launchpad (build 15901)\n"
+
+#. module: base_report_designer
+#: model:ir.model,name:base_report_designer.model_base_report_sxw
+msgid "base.report.sxw"
+msgstr "basen.rapport.sxw"
+
+#. module: base_report_designer
+#: view:base_report_designer.installer:0
+msgid "OpenERP Report Designer Configuration"
+msgstr "OpenERP Rapport Designer Konfigurasjon"
+
+#. module: base_report_designer
+#: view:base_report_designer.installer:0
+msgid ""
+"This plug-in allows you to create/modify OpenERP Reports into OpenOffice "
+"Writer."
+msgstr ""
+"Denne plug-in tillater deg å lage / endre OpenERP rapporter i OpenOffice "
+"Writer."
+
+#. module: base_report_designer
+#: view:base.report.file.sxw:0
+msgid "Upload the modified report"
+msgstr "Laste opp den endrede rapporten"
+
+#. module: base_report_designer
+#: view:base.report.file.sxw:0
+msgid "The .SXW report"
+msgstr ".SXW rapport"
+
+#. module: base_report_designer
+#: model:ir.model,name:base_report_designer.model_base_report_designer_installer
+msgid "base_report_designer.installer"
+msgstr "basen.rapport.designer.installatør"
+
+#. module: base_report_designer
+#: view:base_report_designer.installer:0
+msgid "_Close"
+msgstr "_Lukk"
+
+#. module: base_report_designer
+#: view:base.report.rml.save:0
+msgid "The RML Report"
+msgstr ""
+
+#. module: base_report_designer
+#: view:base_report_designer.installer:0
+msgid "Configure"
+msgstr "Konfigurer"
+
+#. module: base_report_designer
+#: view:base_report_designer.installer:0
+msgid "title"
+msgstr "tittel"
+
+#. module: base_report_designer
+#: field:base.report.file.sxw,report_id:0
+#: field:base.report.sxw,report_id:0
+msgid "Report"
+msgstr "Rapport"
+
+#. module: base_report_designer
+#: model:ir.model,name:base_report_designer.model_base_report_rml_save
+msgid "base.report.rml.save"
+msgstr "Basen.rapport.rml.lagre"
+
+#. module: base_report_designer
+#: model:ir.ui.menu,name:base_report_designer.menu_action_report_designer_wizard
+msgid "Report Designer"
+msgstr "Rapportdesigner"
+
+#. module: base_report_designer
+#: field:base_report_designer.installer,name:0
+msgid "File name"
+msgstr "Filnavn"
+
+#. module: base_report_designer
+#: view:base.report.file.sxw:0
+#: view:base.report.sxw:0
+msgid "Get a report"
+msgstr "Få en rapport"
+
+#. module: base_report_designer
+#: view:base_report_designer.installer:0
+#: model:ir.actions.act_window,name:base_report_designer.action_report_designer_wizard
+msgid "OpenERP Report Designer"
+msgstr "OpenERP Rapport designer"
+
+#. module: base_report_designer
+#: view:base.report.sxw:0
+msgid "Continue"
+msgstr "Fortsett"
+
+#. module: base_report_designer
+#: field:base.report.rml.save,file_rml:0
+msgid "Save As"
+msgstr "Lagre som"
+
+#. module: base_report_designer
+#: help:base_report_designer.installer,plugin_file:0
+msgid ""
+"OpenObject Report Designer plug-in file. Save as this file and install this "
+"plug-in in OpenOffice."
+msgstr ""
+"OpenObject Report Designer plug-in-filen. Lagre som denne filen og "
+"installere denne plugin-modulen i OpenOffice."
+
+#. module: base_report_designer
+#: view:base.report.rml.save:0
+msgid "Save RML FIle"
+msgstr "Lagre RML fil"
+
+#. module: base_report_designer
+#: field:base.report.file.sxw,file_sxw:0
+#: field:base.report.file.sxw,file_sxw_upload:0
+msgid "Your .SXW file"
+msgstr "Din .SXW fil"
+
+#. module: base_report_designer
+#: view:base_report_designer.installer:0
+msgid "Installation and Configuration Steps"
+msgstr "Installasjon og Konfigurasjon trinn"
+
+#. module: base_report_designer
+#: field:base_report_designer.installer,description:0
+msgid "Description"
+msgstr "Beskrivelse:"
+
+#. module: base_report_designer
+#: view:base.report.file.sxw:0
+msgid ""
+"This is the template of your requested report.\n"
+"Save it as a .SXW file and open it with OpenOffice.\n"
+"Don't forget to install the OpenERP SA OpenOffice package to modify it.\n"
+"Once it is modified, re-upload it in OpenERP using this wizard."
+msgstr ""
+"Dette er malen for den forespurte rapporten.\n"
+"Lagre det som en. Sxw fil og åpne den med OpenOffice.\n"
+"Ikke glem å installere OpenERP SA OpenOffice-pakken til å endre det.\n"
+"Når den er modifisert, laste opp det i OpenERP bruke denne veiviseren."
+
+#. module: base_report_designer
+#: field:base_report_designer.installer,config_logo:0
+msgid "Image"
+msgstr "Bilde"
+
+#. module: base_report_designer
+#: model:ir.actions.act_window,name:base_report_designer.action_view_base_report_sxw
+msgid "Base Report sxw"
+msgstr "Basen rapport sxw"
+
+#. module: base_report_designer
+#: model:ir.model,name:base_report_designer.model_base_report_file_sxw
+msgid "base.report.file.sxw"
+msgstr "basen.rapport.fil.sxw"
+
+#. module: base_report_designer
+#: field:base_report_designer.installer,plugin_file:0
+msgid "OpenObject Report Designer Plug-in"
+msgstr "OpenObject Rapport Designer Plug-in"
+
+#. module: base_report_designer
+#: model:ir.actions.act_window,name:base_report_designer.action_report_designer_installer
+msgid "OpenERP Report Designer Installation"
+msgstr "OpenERP Rapport Designer Installasjon"
+
+#. module: base_report_designer
+#: view:base.report.file.sxw:0
+#: view:base.report.rml.save:0
+#: view:base.report.sxw:0
+#: view:base_report_designer.installer:0
+msgid "Cancel"
+msgstr "Kanseller"
+
+#. module: base_report_designer
+#: model:ir.model,name:base_report_designer.model_ir_actions_report_xml
+msgid "ir.actions.report.xml"
+msgstr "ir.handlinger.rapport.xml"
+
+#. module: base_report_designer
+#: view:base.report.sxw:0
+msgid "Select your report"
+msgstr "Velg din rapport"
+
+#~ msgid "The RML report"
+#~ msgstr "RML rapport"
diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py
index 4826a50400d..e1920fc05d9 100644
--- a/addons/base_setup/res_config.py
+++ b/addons/base_setup/res_config.py
@@ -35,6 +35,7 @@ class base_config_settings(osv.osv_memory):
'module_auth_anonymous': fields.boolean('activate the public portal',
help="""Enable the public part of openerp, openerp becomes a public website."""),
'module_auth_oauth': fields.boolean('use external authentication providers, sign in with google, facebook, ...'),
+ 'module_base_import': fields.boolean("Allow users to import data from CSV files"),
}
def open_company(self, cr, uid, ids, context=None):
diff --git a/addons/base_setup/res_config_view.xml b/addons/base_setup/res_config_view.xml
index 512c0a3f52e..2b717b6e0b1 100644
--- a/addons/base_setup/res_config_view.xml
+++ b/addons/base_setup/res_config_view.xml
@@ -14,9 +14,10 @@
@@ -59,6 +60,15 @@
+
+
+
+
diff --git a/addons/base_status/base_stage.py b/addons/base_status/base_stage.py
index 775de51394c..529e70fdb49 100644
--- a/addons/base_status/base_stage.py
+++ b/addons/base_status/base_stage.py
@@ -297,55 +297,31 @@ class base_stage(object):
destination=False)
def remind_user(self, cr, uid, ids, context=None, attach=False, destination=True):
- mail_message = self.pool.get('mail.message')
- for case in self.browse(cr, uid, ids, context=context):
- if not destination and not case.email_from:
- return False
- if not case.user_id.email:
- return False
- if destination and case.section_id.user_id:
- case_email = case.section_id.user_id.email
- else:
- case_email = case.user_id.email
-
- src = case_email
- dest = case.user_id.email or ""
- body = case.description or ""
- for message in case.message_ids:
- if message.email_from and message.body_text:
- body = message.body_text
- break
-
- if not destination:
- src, dest = dest, case.email_from
- if body and case.user_id.signature:
- if body:
- body += '\n\n%s' % (case.user_id.signature)
- else:
- body = '\n\n%s' % (case.user_id.signature)
-
- body = self.format_body(body)
-
- attach_to_send = {}
-
- if attach:
- attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
- attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname', 'datas'])
- attach_to_send = dict(map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send))
-
- # Send an email
- subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
- mail_message.schedule_with_attach(cr, uid,
- src,
- [dest],
- subject,
- body,
- model=self._name,
- reply_to=case.section_id.reply_to,
- res_id=case.id,
- attachments=attach_to_send,
- context=context
- )
+ if 'message_post' in self:
+ for case in self.browse(cr, uid, ids, context=context):
+ if destination:
+ recipient_id = case.user_id.partner_id.id
+ else:
+ if not case.email_from:
+ return False
+ recipient_id = self.pool.get('res.partner').find_or_create(cr, uid, case.email_from, context=context)
+
+ body = case.description or ""
+ for message in case.message_ids:
+ if message.type == 'email' and message.body:
+ body = message.body
+ break
+ body = self.format_body(body)
+ attach_to_send = {}
+ if attach:
+ attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
+ attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname', 'datas'])
+ attach_to_send = dict(map(lambda x: (x['datas_fname'], x['datas'].decode('base64')), attach_to_send))
+
+ subject = "Reminder: [%s] %s" % (case.id, case.name)
+ self.message_post(cr, uid, case.id, body=body,
+ subject=subject, attachments=attach_to_send,
+ partner_ids=[recipient_id], context=context)
return True
def _check(self, cr, uid, ids=False, context=None):
@@ -360,17 +336,6 @@ class base_stage(object):
def format_mail(self, obj, body):
return self.pool.get('base.action.rule').format_mail(obj, body)
- def message_thread_followers(self, cr, uid, ids, context=None):
- res = {}
- for case in self.browse(cr, uid, ids, context=context):
- l=[]
- if case.email_cc:
- l.append(case.email_cc)
- if case.user_id and case.user_id.email:
- l.append(case.user_id.email)
- res[case.id] = l
- return res
-
# ******************************
# Notifications
# ******************************
@@ -395,31 +360,31 @@ class base_stage(object):
def case_open_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been opened .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_close_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been closed .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_cancel_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been canceled .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_pending_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s is now pending .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_reset_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been renewed .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
@@ -428,5 +393,5 @@ class base_stage(object):
msg = '%s has been escalated to %s .' % (self.case_get_note_msg_prefix(cr, uid, id, context=context), new_section.name)
else:
msg = '%s has been escalated .' % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], 'System Notification', msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
diff --git a/addons/base_status/base_state.py b/addons/base_status/base_state.py
index 8fa2f92e22a..cebf5537485 100644
--- a/addons/base_status/base_state.py
+++ b/addons/base_status/base_state.py
@@ -179,13 +179,13 @@ class base_state(object):
# Notifications
# ******************************
- def case_get_note_msg_prefix(self, cr, uid, id, context=None):
- return ''
-
+ def case_get_note_msg_prefix(self, cr, uid, id, context=None):
+ return ''
+
def case_open_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been opened .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
@@ -194,29 +194,29 @@ class base_state(object):
msg = '%s has been escalated to %s .' % (self.case_get_note_msg_prefix(cr, uid, id, context=context), new_section.name)
else:
msg = '%s has been escalated .' % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], 'System Notification', msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_close_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been closed .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_cancel_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been canceled .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_pending_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s is now pending .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_reset_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been renewed .') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
diff --git a/addons/base_vat/i18n/es_MX.po b/addons/base_vat/i18n/es_MX.po
index 57d269be72d..cd511c528b0 100644
--- a/addons/base_vat/i18n/es_MX.po
+++ b/addons/base_vat/i18n/es_MX.po
@@ -1,70 +1,54 @@
-# Translation of OpenERP Server.
-# This file contains the translation of the following modules:
-# * base_vat
+# Spanish (Mexico) 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 , 2012.
#
msgid ""
msgstr ""
-"Project-Id-Version: OpenERP Server 6.0dev\n"
-"Report-Msgid-Bugs-To: support@openerp.com\n"
-"POT-Creation-Date: 2011-01-11 11:14+0000\n"
-"PO-Revision-Date: 2010-12-25 18:58+0000\n"
-"Last-Translator: Jordi Esteve (www.zikzakmedia.com) "
-"\n"
-"Language-Team: \n"
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-07 00:31+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Spanish (Mexico) \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2011-09-05 05:10+0000\n"
-"X-Generator: Launchpad (build 13830)\n"
+"X-Launchpad-Export-Date: 2012-09-08 04:54+0000\n"
+"X-Generator: Launchpad (build 15914)\n"
#. module: base_vat
-#: code:addons/base_vat/base_vat.py:87
+#: code:addons/base_vat/base_vat.py:141
#, python-format
msgid ""
-"The Vat does not seems to be correct. You should have entered something like "
-"this %s"
-msgstr ""
-"El CIF/NIF parece que no sea correcto. Debería haber introducido algo como "
-"esto %s"
+"This VAT number does not seem to be valid.\n"
+"Note: the expected format is %s"
+msgstr "El RFC no es válido. El formato esperado es %s"
#. module: base_vat
-#: model:ir.module.module,description:base_vat.module_meta_information
-msgid ""
-"\n"
-" Enable the VAT Number for the partner. Check the validity of that VAT "
-"Number.\n"
-"\n"
-" This module follows the methods stated at http://sima-pc.com/nif.php "
-"for\n"
-" checking the validity of VAT Number assigned to partners in European "
-"countries.\n"
-" "
+#: sql_constraint:res.company:0
+msgid "The company name must be unique !"
msgstr ""
-"\n"
-" Permite la validación del CIF/NIF de las empresas. Comprueba si el "
-"CIF/NIF es un número válido.\n"
-"\n"
-" Este módulo usa los métodos especificados en http://sima-pc.com/nif.php "
-"para\n"
-" la validación del CIF/NIF asignado a las empresas de los países "
-"europeos.\n"
-" "
-
-#. module: base_vat
-#: model:ir.module.module,shortdesc:base_vat.module_meta_information
-msgid "Base VAT - To check VAT number validity"
-msgstr "Base CIF/NIF - Para comprobar la validez de los CIF/NIF"
#. module: base_vat
#: constraint:res.partner:0
-msgid "Error ! You can not create recursive associated members."
-msgstr "¡Error! No puede crear miembros asociados recursivos."
+msgid "Error ! You cannot create recursive associated members."
+msgstr ""
#. module: base_vat
-#: code:addons/base_vat/base_vat.py:88
-#, python-format
-msgid "The VAT is invalid, It should begin with the country code"
-msgstr "El CIF/NIF no es válido, debería empezar con el código del país"
+#: field:res.company,vat_check_vies:0
+msgid "VIES VAT Check"
+msgstr ""
+
+#. module: base_vat
+#: model:ir.model,name:base_vat.model_res_company
+msgid "Companies"
+msgstr ""
+
+#. module: base_vat
+#: constraint:res.company:0
+msgid "Error! You can not create recursive companies."
+msgstr ""
#. module: base_vat
#: help:res.partner,vat_subjected:0
@@ -72,27 +56,20 @@ msgid ""
"Check this box if the partner is subjected to the VAT. It will be used for "
"the VAT legal statement."
msgstr ""
-"Marque esta opción si la empresa está sujeta al IVA. Será utilizado para la "
-"declaración legal del IVA."
#. module: base_vat
#: model:ir.model,name:base_vat.model_res_partner
msgid "Partner"
-msgstr "Empresa"
+msgstr ""
+
+#. module: base_vat
+#: help:res.company,vat_check_vies:0
+msgid ""
+"If checked, Partners VAT numbers will be fully validated against EU's VIES "
+"service rather than via a simple format validation (checksum)."
+msgstr ""
#. module: base_vat
#: field:res.partner,vat_subjected:0
msgid "VAT Legal Statement"
-msgstr "Sujeto a IVA"
-
-#~ msgid "Invalid XML for View Architecture!"
-#~ msgstr "¡XML inválido para la definición de la vista!"
-
-#~ msgid ""
-#~ "Enable the VAT Number for the partner. Check the validity of that VAT Number."
-#~ msgstr ""
-#~ "Activa el IVA (Impuesto Valor Añadido) para la empresa. Comprueba la validez "
-#~ "del CIF/NIF."
-
-#~ msgid "VAT"
-#~ msgstr "IVA"
+msgstr ""
diff --git a/addons/board/__openerp__.py b/addons/board/__openerp__.py
index f07878717fd..2690012f3b4 100644
--- a/addons/board/__openerp__.py
+++ b/addons/board/__openerp__.py
@@ -28,9 +28,7 @@
Lets the user create a custom dashboard.
========================================
-This module also creates the Administration Dashboard.
-
-The user can also publish notes.
+Allows users to create custom dashboard.
""",
'author': 'OpenERP SA',
'depends': ['base'],
diff --git a/addons/board/i18n/nb.po b/addons/board/i18n/nb.po
new file mode 100644
index 00000000000..2d759af27d1
--- /dev/null
+++ b/addons/board/i18n/nb.po
@@ -0,0 +1,348 @@
+# Norwegian Bokmal 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 , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-06 14:01+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-07 04:58+0000\n"
+"X-Generator: Launchpad (build 15914)\n"
+
+#. module: board
+#: view:res.log.report:0
+msgid " Year "
+msgstr " År "
+
+#. module: board
+#: model:ir.model,name:board.model_board_menu_create
+msgid "Menu Create"
+msgstr "Meny laget"
+
+#. module: board
+#: view:board.menu.create:0
+msgid "Menu Information"
+msgstr "Meny informasjon"
+
+#. module: board
+#: view:res.users:0
+msgid "Latest Connections"
+msgstr "Siste Tilkoblinger"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Log created in last month"
+msgstr "Logg opprettet i forrige måned"
+
+#. module: board
+#: view:board.board:0
+#: model:ir.actions.act_window,name:board.open_board_administration_form
+msgid "Administration Dashboard"
+msgstr "Administrasjon kontrollpanel"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Group By..."
+msgstr "Grupper etter ..."
+
+#. module: board
+#: view:res.log.report:0
+msgid "Log created in current year"
+msgstr "Logg opprettet i gjeldende år."
+
+#. module: board
+#: model:ir.model,name:board.model_board_board
+msgid "Board"
+msgstr "Brett"
+
+#. module: board
+#: field:board.menu.create,menu_name:0
+msgid "Menu Name"
+msgstr "Menynavn"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.board_weekly_res_log_report_action
+#: view:res.log.report:0
+msgid "Weekly Global Activity"
+msgstr "Ukentlig global aktivitet"
+
+#. module: board
+#: field:board.board.line,name:0
+msgid "Title"
+msgstr "Tittel"
+
+#. module: board
+#: field:res.log.report,nbr:0
+msgid "# of Entries"
+msgstr "# av oppføringer"
+
+#. module: board
+#: view:res.log.report:0
+#: field:res.log.report,month:0
+msgid "Month"
+msgstr "Måned"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Log created in current month"
+msgstr "Logg opprettet i gjeldende måned."
+
+#. module: board
+#: model:ir.actions.act_window,name:board.board_monthly_res_log_report_action
+#: view:res.log.report:0
+msgid "Monthly Activity per Document"
+msgstr "Månedlig aktivitet per dokument"
+
+#. module: board
+#: view:board.board:0
+msgid "Configuration Overview"
+msgstr "Konfigurasjonsoversikt"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.action_view_board_list_form
+#: model:ir.ui.menu,name:board.menu_view_board_form
+msgid "Dashboard Definition"
+msgstr "Kontrollpanel Definisjon"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "March"
+msgstr "Mars"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "August"
+msgstr "August"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.action_user_connection_tree
+msgid "User Connections"
+msgstr "Brukertilkoblinger"
+
+#. module: board
+#: field:res.log.report,creation_date:0
+msgid "Creation Date"
+msgstr "Opprettelsesdato"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Log Analysis"
+msgstr "Logg analyse"
+
+#. module: board
+#: field:res.log.report,res_model:0
+msgid "Object"
+msgstr "Objekt"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "June"
+msgstr "Juni"
+
+#. module: board
+#: field:board.board,line_ids:0
+msgid "Action Views"
+msgstr "Handling Visninger"
+
+#. module: board
+#: model:ir.model,name:board.model_res_log_report
+msgid "Log Report"
+msgstr "Logg rapport"
+
+#. module: board
+#: code:addons/board/wizard/board_menu_create.py:46
+#, python-format
+msgid "Please Insert Dashboard View(s) !"
+msgstr "Vennligst Sett Kontrollpanel Vinsning (er)!"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "July"
+msgstr "juli"
+
+#. module: board
+#: view:res.log.report:0
+#: field:res.log.report,day:0
+msgid "Day"
+msgstr "Dag"
+
+#. module: board
+#: view:board.menu.create:0
+msgid "Create Menu For Dashboard"
+msgstr "Opprett meny for Kontrollpanel."
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "February"
+msgstr "Februar"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "October"
+msgstr "Oktober"
+
+#. module: board
+#: model:ir.model,name:board.model_board_board_line
+msgid "Board Line"
+msgstr "bord Linje"
+
+#. module: board
+#: field:board.menu.create,menu_parent_id:0
+msgid "Parent Menu"
+msgstr "Overordnet meny"
+
+#. module: board
+#: view:res.log.report:0
+msgid " Month-1 "
+msgstr " Måned-1 "
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "January"
+msgstr "Januar"
+
+#. module: board
+#: view:board.board:0
+msgid "Users"
+msgstr "Brukere"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "November"
+msgstr "November"
+
+#. module: board
+#: help:board.board.line,sequence:0
+msgid ""
+"Gives the sequence order when displaying a list of "
+"board lines."
+msgstr "Gir rekkefølgen av når du viser en liste over bord linjer."
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "April"
+msgstr "April"
+
+#. module: board
+#: view:board.board:0
+#: field:board.board,name:0
+#: field:board.board.line,board_id:0
+#: model:ir.ui.menu,name:board.menu_dasboard
+msgid "Dashboard"
+msgstr "Kontrollpanel"
+
+#. module: board
+#: code:addons/board/wizard/board_menu_create.py:45
+#, python-format
+msgid "User Error!"
+msgstr "Bruker feil!"
+
+#. module: board
+#: field:board.board.line,action_id:0
+msgid "Action"
+msgstr "Handling"
+
+#. module: board
+#: field:board.board.line,position:0
+msgid "Position"
+msgstr "Posisjon"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Model"
+msgstr "Modell"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.board_homepage_action
+msgid "Home Page"
+msgstr "Hjemmeside"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.action_latest_activities_tree
+msgid "Latest Activities"
+msgstr "Senest aktiveter"
+
+#. module: board
+#: selection:board.board.line,position:0
+msgid "Left"
+msgstr "Venstre"
+
+#. module: board
+#: field:board.board,view_id:0
+msgid "Board View"
+msgstr "Bord visning"
+
+#. module: board
+#: selection:board.board.line,position:0
+msgid "Right"
+msgstr "Høyre"
+
+#. module: board
+#: field:board.board.line,width:0
+msgid "Width"
+msgstr "Bredde"
+
+#. module: board
+#: view:res.log.report:0
+msgid " Month "
+msgstr " Måned "
+
+#. module: board
+#: field:board.board.line,sequence:0
+msgid "Sequence"
+msgstr "Sekvens"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "September"
+msgstr "September"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "December"
+msgstr "Desember"
+
+#. module: board
+#: view:board.board:0
+#: view:board.menu.create:0
+msgid "Create Menu"
+msgstr "Opprett meny"
+
+#. module: board
+#: field:board.board.line,height:0
+msgid "Height"
+msgstr "Høyde"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.action_board_menu_create
+msgid "Create Board Menu"
+msgstr "Opprett bord meny"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "May"
+msgstr "Mai"
+
+#. module: board
+#: view:res.log.report:0
+#: field:res.log.report,name:0
+msgid "Year"
+msgstr "År"
+
+#. module: board
+#: view:board.menu.create:0
+msgid "Cancel"
+msgstr "Kanseller"
+
+#. module: board
+#: view:board.board:0
+msgid "Dashboard View"
+msgstr "Kontrollpanel visning"
diff --git a/addons/board/static/src/css/Makefile b/addons/board/static/src/css/Makefile
new file mode 100644
index 00000000000..0cb5ae70f4a
--- /dev/null
+++ b/addons/board/static/src/css/Makefile
@@ -0,0 +1,3 @@
+dashboard.css: dashboard.sass
+ sass --trace -t expanded dashboard.sass dashboard.css
+
diff --git a/addons/board/static/src/css/dashboard.css b/addons/board/static/src/css/dashboard.css
index 6907b4271a0..ed66e338c81 100644
--- a/addons/board/static/src/css/dashboard.css
+++ b/addons/board/static/src/css/dashboard.css
@@ -1,3 +1,17 @@
+.openerp .oe_dashboard_layout_selector ul {
+ white-space: nowrap;
+}
+.openerp .oe_dashboard_layout_selector li {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ float: left;
+}
+.openerp .oe_dashboard_layout_selector li .oe_dashboard_selected_layout {
+ margin-left: -30px;
+ vertical-align: bottom;
+ margin-bottom: 10px;
+}
.openerp .oe_dashboard_links {
text-align: right;
margin: 0 4px 6px 0;
diff --git a/addons/board/static/src/css/dashboard.sass b/addons/board/static/src/css/dashboard.sass
index 4bacc7bd892..32f0e3b4f3a 100644
--- a/addons/board/static/src/css/dashboard.sass
+++ b/addons/board/static/src/css/dashboard.sass
@@ -9,6 +9,18 @@
box-shadow: $bsval
.openerp
+ .oe_dashboard_layout_selector
+ ul
+ white-space: nowrap
+ li
+ margin: 0
+ padding: 0
+ list-style-type: none
+ float: left
+ .oe_dashboard_selected_layout
+ margin-left: -30px
+ vertical-align: bottom
+ margin-bottom: 10px
.oe_dashboard_links
text-align: right
margin: 0 4px 6px 0
diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js
index 0333690aa57..9c37924144d 100644
--- a/addons/board/static/src/js/dashboard.js
+++ b/addons/board/static/src/js/dashboard.js
@@ -220,36 +220,40 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({
am.do_action = function (action) {
self.do_action(action);
};
- if (action_attrs.creatable && action_attrs.creatable !== 'false') {
- var action_id = parseInt(action_attrs.creatable, 10);
- $action.parent().find('button.oe_dashboard_button_create').click(function() {
- if (isNaN(action_id)) {
- action_orig.flags.default_view = 'form';
- self.do_action(action_orig);
- } else {
- self.rpc('/web/action/load', {
- action_id: action_id
- }, function(result) {
- result.result.flags = result.result.flags || {};
- result.result.flags.default_view = 'form';
- self.do_action(result.result);
- });
- }
- });
- }
if (am.inner_widget) {
- am.inner_widget.on_mode_switch.add(function(mode) {
+ var new_form_action = function(id, editable) {
var new_views = [];
_.each(action_orig.views, function(view) {
- new_views[view[1] === mode ? 'unshift' : 'push'](view);
+ new_views[view[1] === 'form' ? 'unshift' : 'push'](view);
});
- if (!new_views.length || new_views[0][1] !== mode) {
- new_views.unshift([false, mode]);
+ if (!new_views.length || new_views[0][1] !== 'form') {
+ new_views.unshift([false, 'form']);
}
action_orig.views = new_views;
- action_orig.res_id = am.inner_widget.dataset.ids[am.inner_widget.dataset.index];
+ action_orig.res_id = id;
+ action_orig.flags = {
+ form: {
+ "initial_mode": editable ? "edit" : "view",
+ }
+ };
self.do_action(action_orig);
- });
+ };
+ var list = am.inner_widget.views.list;
+ if (list) {
+ list.deferred.then(function() {
+ $(list.controller.groups).off('row_link').on('row_link', function(e, id) {
+ new_form_action(id);
+ });
+ });
+ }
+ var kanban = am.inner_widget.views.kanban;
+ if (kanban) {
+ kanban.deferred.then(function() {
+ kanban.controller.open_record = function(id, editable) {
+ new_form_action(id, editable);
+ };
+ });
+ }
}
},
renderElement: function() {
diff --git a/addons/board/static/src/xml/board.xml b/addons/board/static/src/xml/board.xml
index b2363cafd53..e13fa707776 100644
--- a/addons/board/static/src/xml/board.xml
+++ b/addons/board/static/src/xml/board.xml
@@ -26,7 +26,6 @@
- Create
diff --git a/addons/crm/board_crm_view.xml b/addons/crm/board_crm_view.xml
index 16ed1e4c5ad..d80183b80c9 100644
--- a/addons/crm/board_crm_view.xml
+++ b/addons/crm/board_crm_view.xml
@@ -2,22 +2,6 @@
-
- My Opportunities
- crm.lead
- tree
- [('user_id','=',uid),('type', '=', 'opportunity'),('state','not in',('cancel','done'))]
-
-
-
-
- New Leads
- crm.lead
- tree
- [('user_id','=',uid),('state','=','draft'),('type','=','lead')]
-
-
-
Opportunities By Stage - Graph
crm.lead.report
@@ -70,11 +54,9 @@
-
-
-
- Module CRM has been installed
- From the top menu Sales, you can: trace leads and opportunities, get accurate forecast on your sales pipeline, plan meetings and phonecalls, get realtime statistics and efficiently organize the communication with your prospects.
+
+ mail.group
+
+ notification
+ CRM application installed!
+ From the top Sales menu you can track leads and opportunities, get accurate forecast on your sales pipeline, plan meetings and phonecalls, get realtime statistics and efficiently organize the communication with your prospects.
+To manage quotations and sale orders, install the "Sales Management" application.
+
-To manage quotations and sale orders, install the module "Sales Management".
-
-
sales
{'type':'lead'}
-
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index 25a47ecbadc..ddfdb05ca66 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -19,11 +19,9 @@
#
##############################################################################
-import binascii
from base_status.base_stage import base_stage
import crm
from datetime import datetime
-from mail.mail_message import to_email
from osv import fields, osv
import time
import tools
@@ -40,8 +38,7 @@ class crm_lead(base_stage, osv.osv):
_name = "crm.lead"
_description = "Lead/Opportunity"
_order = "priority,date_action,id desc"
- _inherit = ['ir.needaction_mixin', 'mail.thread']
- _mail_compose_message = True
+ _inherit = ['mail.thread','ir.needaction_mixin']
def _get_default_section_id(self, cr, uid, context=None):
""" Gives default section by checking if present in the context """
@@ -90,8 +87,8 @@ class crm_lead(base_stage, osv.osv):
search_domain = []
section_id = self._resolve_section_id_from_context(cr, uid, context=context)
if section_id:
- search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', False)]
- search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
+ search_domain += ['|', ('section_ids', '=', section_id)]
+ search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
# retrieve type from the context (if set: choose 'type' or 'both')
type = self._resolve_type_from_context(cr, uid, context=context)
if type:
@@ -101,7 +98,12 @@ class crm_lead(base_stage, osv.osv):
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])))
- return result
+
+ fold = {}
+ for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
+ fold[stage.id] = stage.fold or False
+
+ return result, fold
_group_by_full = {
'stage_id': _read_group_stage_ids
@@ -175,16 +177,6 @@ class crm_lead(base_stage, osv.osv):
else:
return [('id', '=', '0')]
- def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
- res = {}
- for obj in self.browse(cr, uid, ids, context=context):
- res[obj.id] = ''
- for msg in obj.message_ids:
- if msg.email_from:
- res[obj.id] = msg.subject
- break
- return res
-
_columns = {
'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
select=True, help="Optional linked partner, usually after conversion of the lead"),
@@ -213,7 +205,7 @@ class crm_lead(base_stage, osv.osv):
'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
'date_closed': fields.datetime('Closed', readonly=True),
'stage_id': fields.many2one('crm.case.stage', 'Stage',
- domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
+ domain="['&', ('fold', '=', False), '&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
'user_id': fields.many2one('res.users', 'Salesperson'),
'referred': fields.char('Referred By', size=64),
'date_open': fields.datetime('Opened', readonly=True),
@@ -228,7 +220,6 @@ class crm_lead(base_stage, osv.osv):
When the case is over, the state is set to \'Done\'.\
If the case needs to be reviewed then the state is \
set to \'Pending\'.'),
- 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
# Only used for type opportunity
'probability': fields.float('Success Rate (%)',group_operator="avg"),
@@ -449,7 +440,7 @@ class crm_lead(base_stage, osv.osv):
oldest_id = opportunity_ids[0]
return self.browse(cr, uid, oldest_id, context=context)
- def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
+ def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
body = []
if title:
body.append("%s\n" % (title))
@@ -486,11 +477,11 @@ class crm_lead(base_stage, osv.osv):
for opportunity in opportunities:
subject.append(opportunity.name)
title = "%s : %s" % (merge_message, opportunity.name)
- details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
+ details.append(self._mail_body(cr, uid, opportunity, fields, title=title, context=context))
subject = subject[0] + ", ".join(subject[1:])
details = "\n\n".join(details)
- return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
+ return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
message = self.pool.get('mail.message')
@@ -546,7 +537,7 @@ class crm_lead(base_stage, osv.osv):
oldest = self._merge_find_oldest(cr, uid, ids, context=context)
if ctx_opportunities :
first_opportunity = ctx_opportunities[0]
- tail_opportunities = opportunities_list
+ tail_opportunities = opportunities_list + ctx_opportunities[1:]
else:
first_opportunity = opportunities_list[0]
tail_opportunities = opportunities_list[1:]
@@ -606,19 +597,13 @@ class crm_lead(base_stage, osv.osv):
for lead in self.browse(cr, uid, ids, context=context):
if lead.state in ('done', 'cancel'):
continue
- if user_ids or section_id:
- self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
-
vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
self.write(cr, uid, [lead.id], vals, context=context)
-
self.convert_opportunity_send_note(cr, uid, lead, context=context)
- #TOCHECK: why need to change partner details in all messages of lead ?
- if lead.partner_id:
- msg_ids = [ x.id for x in lead.message_ids]
- mail_message.write(cr, uid, msg_ids, {
- 'partner_id': lead.partner_id.id
- }, context=context)
+
+ if user_ids or section_id:
+ self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
+
return True
def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
@@ -630,7 +615,7 @@ class crm_lead(base_stage, osv.osv):
'parent_id': parent_id,
'phone': lead.phone,
'mobile': lead.mobile,
- 'email': lead.email_from and to_email(lead.email_from)[0],
+ 'email': lead.email_from and tools.email_split(lead.email_from)[0],
'fax': lead.fax,
'title': lead.title and lead.title.id or False,
'function': lead.function,
@@ -650,7 +635,7 @@ class crm_lead(base_stage, osv.osv):
partner_id = False
if lead.partner_name and lead.contact_name:
partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
- self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
+ partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
elif lead.partner_name and not lead.contact_name:
partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
elif not lead.partner_name and lead.contact_name:
@@ -678,32 +663,16 @@ class crm_lead(base_stage, osv.osv):
if context is None:
context = {}
partner_ids = {}
+ force_partner_id = partner_id
for lead in self.browse(cr, uid, ids, context=context):
if action == 'create':
if not partner_id:
partner_id = self._create_lead_partner(cr, uid, lead, context)
+ partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context=context)
self._lead_set_partner(cr, uid, lead, partner_id, context=context)
partner_ids[lead.id] = partner_id
return partner_ids
- def _send_mail_to_salesman(self, cr, uid, lead, context=None):
- """
- Send mail to salesman with updated Lead details.
- @ lead: browse record of 'crm.lead' object.
- """
- #TOFIX: mail template should be used here instead of fix subject, body text.
- message = self.pool.get('mail.message')
- email_to = lead.user_id and lead.user_id.email
- if not email_to:
- return False
-
- email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.email or email_to
- partner = lead.partner_id and lead.partner_id.name or lead.partner_name
- subject = "lead %s converted into opportunity" % lead.name
- body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
- return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
-
-
def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
index = 0
for lead_id in ids:
@@ -821,14 +790,13 @@ class crm_lead(base_stage, osv.osv):
if custom_values is None: custom_values = {}
custom_values.update({
'name': msg.get('subject') or _("No Subject"),
- 'description': msg.get('body_text'),
+ 'description': msg.get('body'),
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
})
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
custom_values['priority'] = msg.get('priority')
- custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context))
return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
@@ -841,18 +809,18 @@ class crm_lead(base_stage, osv.osv):
if update_vals is None: update_vals = {}
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
- vals['priority'] = msg.get('priority')
+ update_vals['priority'] = msg.get('priority')
maps = {
'cost':'planned_cost',
'revenue': 'planned_revenue',
'probability':'probability',
}
- for line in msg.get('body_text', '').split('\n'):
+ for line in msg.get('body', '').split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower()):
key = maps.get(res.group(1).lower())
- vals[key] = res.group(2).lower()
+ update_vals[key] = res.group(2).lower()
return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
@@ -860,15 +828,10 @@ class crm_lead(base_stage, osv.osv):
# OpenChatter methods and notifications
# ----------------------------------------
- def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
- """ Add 'user_id' to the monitored fields """
- res = super(crm_lead, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
- return res + ['user_id']
-
def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
""" Override of the (void) default notification method. """
stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
- return self.message_append_note(cr, uid, ids, body= _("Stage changed to %s .") % (stage_name), context=context)
+ return self.message_post(cr, uid, ids, body= _("Stage changed to %s .") % (stage_name), context=context)
def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
if isinstance(lead, (int, long)):
@@ -878,33 +841,33 @@ class crm_lead(base_stage, osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for id in ids:
message = _("%s has been created .")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=message, context=context)
+ self.message_post(cr, uid, [id], body=message, context=context)
return True
def case_mark_lost_send_note(self, cr, uid, ids, context=None):
message = _("Opportunity has been lost .")
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def case_mark_won_send_note(self, cr, uid, ids, context=None):
message = _("Opportunity has been won .")
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
if action == 'log': prefix = 'Logged'
else: prefix = 'Scheduled'
message = _("%s a call for the %s .") % (prefix, phonecall.date)
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
for lead in self.browse(cr, uid, ids, context=context):
message = _("%s partner is now set to %s ." % (self.case_get_note_msg_prefix(cr, uid, lead, context=context), lead.partner_id.name))
- lead.message_append_note(body=message)
+ lead.message_post(body=message)
return True
def convert_opportunity_send_note(self, cr, uid, lead, context=None):
message = _("Lead has been converted to an opportunity .")
- lead.message_append_note(body=message)
+ lead.message_post(body=message)
return True
crm_lead()
diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml
index 04f91933f66..f734a839ff4 100644
--- a/addons/crm/crm_lead_data.xml
+++ b/addons/crm/crm_lead_data.xml
@@ -55,7 +55,7 @@
Dead
-
+
cancel
diff --git a/addons/crm/crm_lead_demo.xml b/addons/crm/crm_lead_demo.xml
index a274dadfe01..b3c00256b06 100644
--- a/addons/crm/crm_lead_demo.xml
+++ b/addons/crm/crm_lead_demo.xml
@@ -23,7 +23,7 @@
1
-
+
Hello,
I am Jason from Le Club SARL,
@@ -47,7 +47,7 @@ Can you send details,
4
-
+
@@ -108,7 +108,7 @@ Can you send details,
3
-
+
Hi, Can you send a quotation for 20 Computers with speakers?
Regards,
@@ -135,7 +135,7 @@ Contact: +1 813 494 5005
3
-
+
@@ -194,7 +194,7 @@ Contact: +1 813 494 5005
2
-
+
@@ -297,7 +297,7 @@ Andrew
Meeting for pricing information.
-
+
@@ -342,7 +342,7 @@ Andrew
Call to ask system requirement
-
+
@@ -411,7 +411,7 @@ Andrew
Call to define real needs about training
-
+
@@ -434,7 +434,7 @@ Andrew
Ask for the good receprion of the proposition
-
+
@@ -466,7 +466,7 @@ Andrew
3
-
+
@@ -484,7 +484,7 @@ Andrew
3
-
+
@@ -519,7 +519,7 @@ Andrew
5
-
+
@@ -542,7 +542,7 @@ Andrew
2
Conf call with technical service
-
+
@@ -552,86 +552,67 @@ Andrew
Plan to buy a Laptop
crm.lead
- html
- <![CDATA[Email0 inquiry]]><div><font size="2">Hello,</font></div><div><font size="2"><br></font></div><div><font size="2">I am interested in your company's product and I plan to buy a new laptop having latest technologies and affordable price.</font></div><div><font size="2">Can you please send me product catalogue?</font></div>
+ <![CDATA[Email0 inquiry]]><div><font size="2">Hello,</font></div><div><font size="2"><br></font></div><div><font size="2">I am interested in your company's product and I plan to buy a new laptop having latest technologies and affordable price.</font></div><div><font size="2">Can you please send me product catalogue?</font></div>
email
- received
-
Re: Plan to buy a Laptop
crm.lead
-
- plain
- Dear Customer,
+ comment
+ Dear Customer,
Thanks for showing interest in our products.
We have attached the catalogue,
We would like to know your interests, Let us know if we can call you for more details.
Thanks
- email
-
- sent
+
+
Re: Plan to buy a Laptop
crm.lead
- plain
- Yes, its open till 10:00 PM, you are welcome!
+ Yes, its open till 10:00 PM, you are welcome!
email
- sent
-
+
-
Inquiry
crm.lead
- plain
- Hello,
+ Hello,
I am Jason from Le Club SARL,
I am intertested to attend Training organized in your company,
Can you send details,
email
- received
-
-
Need Details
crm.lead
- plain
- Want to know features and benifits to use the new software.
+ Want to know features and benifits to use the new software.
comment
-
-
Leads
crm.lead
-
+
@@ -257,12 +257,11 @@
-
+
-
-
+
@@ -296,23 +295,16 @@
-
+
-
-
-
+
@@ -362,10 +353,8 @@
-
-
-
+
@@ -386,7 +375,6 @@
-
@@ -435,7 +423,7 @@
- at
+ at
%% success rate
@@ -515,7 +503,7 @@
-
+
@@ -545,7 +533,7 @@
Opportunities Tree
crm.lead
-
+
@@ -555,7 +543,6 @@
-
@@ -563,7 +550,7 @@
-
+
@@ -578,7 +565,7 @@
-
+
@@ -600,7 +587,6 @@
-
diff --git a/addons/crm/crm_meeting.py b/addons/crm/crm_meeting.py
index 13171d95b9b..62fc53364ee 100644
--- a/addons/crm/crm_meeting.py
+++ b/addons/crm/crm_meeting.py
@@ -44,7 +44,7 @@ class crm_meeting(osv.Model):
def create_send_note(self, cr, uid, ids, context=None):
if context is None:
context = {}
- # update context: if come from phonecall, default state values can make the message_append_note crash
+ # update context: if come from phonecall, default state values can make the message_post crash
context.pop('default_state', False)
for meeting in self.browse(cr, uid, ids, context=context):
# in the message, transpose meeting.date to the timezone of the current user
@@ -53,14 +53,14 @@ class crm_meeting(osv.Model):
if meeting.opportunity_id: # meeting can be create from phonecalls or opportunities, therefore checking for the parent
lead = meeting.opportunity_id
message = _("Meeting linked to the opportunity
%s has been
created and
scheduled on
%s .") % (lead.name, meeting_date_tz)
- lead.message_append_note(_('System Notification'), message)
+ lead.message_post(body=message)
elif meeting.phonecall_id:
phonecall = meeting.phonecall_id
message = _("Meeting linked to the phonecall
%s has been
created and
scheduled on
%s .") % (phonecall.name, meeting_date_tz)
- phonecall.message_append_note(body=message)
+ phonecall.message_post(body=message)
else:
message = _("A meeting has been
scheduled on
%s .") % (meeting_date_tz)
- meeting.message_append_note(body=message)
+ meeting.message_post(body=message)
return True
class calendar_attendee(osv.osv):
diff --git a/addons/crm/crm_phonecall.py b/addons/crm/crm_phonecall.py
index da08f97250a..44894fc881d 100644
--- a/addons/crm/crm_phonecall.py
+++ b/addons/crm/crm_phonecall.py
@@ -32,7 +32,7 @@ class crm_phonecall(base_state, osv.osv):
_name = "crm.phonecall"
_description = "Phonecall"
_order = "id desc"
- _inherit = ['ir.needaction_mixin', 'mail.thread']
+ _inherit = ['mail.thread']
_columns = {
# base_state required fields
'date_action_last': fields.datetime('Last Action', readonly=1),
@@ -177,11 +177,11 @@ class crm_phonecall(base_state, osv.osv):
if context is None:
context = {}
partner_ids = {}
+ force_partner_id = partner_id
for call in self.browse(cr, uid, ids, context=context):
if action == 'create':
- if not partner_id:
- partner_id = self._call_create_partner(cr, uid, call, context=context)
- self._call_create_partner_address(cr, uid, call, partner_id, context=context)
+ partner_id = force_partner_id or self._call_create_partner(cr, uid, call, context=context)
+ self._call_create_partner_address(cr, uid, call, partner_id, context=context)
self._call_set_partner(cr, uid, [call.id], partner_id, context=context)
partner_ids[call.id] = partner_id
return partner_ids
@@ -266,7 +266,7 @@ class crm_phonecall(base_state, osv.osv):
def case_reset_send_note(self, cr, uid, ids, context=None):
message = _('Phonecall has been
reset and set as open .')
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def case_open_send_note(self, cr, uid, ids, context=None):
lead_obj = self.pool.get('crm.lead')
@@ -280,11 +280,11 @@ class crm_phonecall(base_state, osv.osv):
message = _("Phonecall linked to the opportunity
%s has been
created and
scheduled on
%s .") % (lead.name, phonecall_date_str)
else:
message = _("Phonecall has been
created and opened .")
- phonecall.message_append_note(body=message)
+ phonecall.message_post(body=message)
return True
def _call_set_partner_send_note(self, cr, uid, ids, context=None):
- return self.message_append_note(cr, uid, ids, body=_("Partner has been
created ."), context=context)
+ return self.message_post(cr, uid, ids, body=_("Partner has been
created ."), context=context)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/crm/crm_phonecall_view.xml b/addons/crm/crm_phonecall_view.xml
index 3b5162288a4..2f885e09b61 100644
--- a/addons/crm/crm_phonecall_view.xml
+++ b/addons/crm/crm_phonecall_view.xml
@@ -66,8 +66,7 @@
CRM - Phone Calls Tree
crm.phonecall
-
-
+
@@ -76,7 +75,6 @@
-
CRM - Logged Phone Calls Tree
crm.phonecall
-
+
-
-
diff --git a/addons/crm/report/crm_lead_report_view.xml b/addons/crm/report/crm_lead_report_view.xml
index 4283dd8fb3c..35ed9411215 100644
--- a/addons/crm/report/crm_lead_report_view.xml
+++ b/addons/crm/report/crm_lead_report_view.xml
@@ -105,21 +105,11 @@
-
-
-
-
-
-
-
-
-
')])
context.update({'active_model': 'crm.lead','active_id': lead_ids[0]})
- id = self.create(cr, uid, {'body_text': "Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci", 'email_from': 'sales@mycompany.com'}, context=context)
+ id = self.create(cr, uid, {'body': "Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci", 'email_from': 'sales@mycompany.com'}, context=context)
try:
self.send_mail(cr, uid, [id], context=context)
except:
@@ -34,12 +34,6 @@
!python {model: crm.lead}: |
lead_ids = self.search(cr, uid, [('email_from','=', 'Mr. John Right ')])
self.convert_partner(cr, uid, lead_ids, context=context)
--
- Now, I search customer in regular customer list.
--
- !python {model: crm.lead}: |
- partner_ids = self.message_partner_by_email(cr, uid, 'Mr. John Right ')
- assert partner_ids.get('partner_id'), "Customer is not found in regular customer list."
-
I convert one phonecall request as a customer and put into regular customer list.
-
diff --git a/addons/crm/test/process/lead2opportunity2win.yml b/addons/crm/test/process/lead2opportunity2win.yml
index 23713dfca2f..ed552421cf0 100644
--- a/addons/crm/test/process/lead2opportunity2win.yml
+++ b/addons/crm/test/process/lead2opportunity2win.yml
@@ -55,7 +55,7 @@
After communicated with customer, I put some notes with contract details.
-
!python {model: crm.lead}: |
- self.message_append_note(cr, uid, [ref('crm_case_4')], subject='Test note', body='ces détails envoyés par le client sur le FAX pour la qualité')
+ self.message_post(cr, uid, [ref('crm_case_4')], subject='Test note', body='ces détails envoyés par le client sur le FAX pour la qualité')
-
I win this opportunity
-
@@ -73,7 +73,7 @@
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_4")})
+ context.update({'active_model': 'crm.lead', 'active_ids': [ref("crm_case_11"), ref("crm_case_2")], 'active_id': ref("crm_case_11")})
id = self.create(cr, uid, {'user_ids': [ref('base.user_root')], 'section_id': ref('crm.section_sales_department')}, context=context)
self.mass_convert(cr, uid, [id], context=context)
-
@@ -83,7 +83,8 @@
opp = self.browse(cr, uid, ref('crm_case_11'))
assert opp.name == "Need estimated cost for new project", "Opportunity name not correct"
assert opp.type == 'opportunity', 'Lead is not converted to opportunity!'
- assert opp.partner_id.name == "Thomas Passot", 'Partner mismatch!'
+ expected_partner = "Thomas Passot"
+ 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!'
-
Then check for second lead converted on opportunity.
diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py
index 547d3ed299e..e90303dda66 100644
--- a/addons/crm/wizard/crm_lead_to_opportunity.py
+++ b/addons/crm/wizard/crm_lead_to_opportunity.py
@@ -24,8 +24,6 @@ from tools.translate import _
import tools
import re
-import time
-
class crm_lead2opportunity_partner(osv.osv_memory):
_name = 'crm.lead2opportunity.partner'
_description = 'Lead To Opportunity Partner'
@@ -35,8 +33,8 @@ class crm_lead2opportunity_partner(osv.osv_memory):
'action': fields.selection([('exist', 'Link to an existing partner'), \
('create', 'Create a new partner'), \
('nothing', 'Do not link to a partner')], \
- 'Action', required=True),
- 'name': fields.selection([('convert', 'Convert to Opportunity'), ('merge', 'Merge with existing Opportunity')],'Select Action', required=True),
+ 'Related Partner', required=True),
+ 'name': fields.selection([('convert', 'Convert to Opportunities'), ('merge', 'Merge with existing Opportunities')], 'Conversion Action', required=True),
'opportunity_ids': fields.many2many('crm.lead', string='Opportunities', domain=[('type', '=', 'opportunity')]),
}
@@ -68,8 +66,6 @@ class crm_lead2opportunity_partner(osv.osv_memory):
if ids:
opportunities.append(ids[0])
if not partner_id:
- label = False
- opp_ids = []
if email:
# Find email of existing opportunity matches the email_from of the lead
cr.execute("""select id from crm_lead where type='opportunity' and
@@ -106,23 +102,36 @@ class crm_lead2opportunity_partner(osv.osv_memory):
if context is None:
context = {}
lead = self.pool.get('crm.lead')
- partner_id = self._create_partner(cr, uid, ids, context=context)
+ res = False
+ partner_ids_map = self._create_partner(cr, uid, ids, context=context)
lead_ids = vals.get('lead_ids', [])
- user_ids = vals.get('user_ids', False)
team_id = vals.get('section_id', False)
- return lead.convert_opportunity(cr, uid, lead_ids, partner_id, user_ids, team_id, context=context)
+ for lead_id in lead_ids:
+ partner_id = partner_ids_map.get(lead_id, False)
+ # FIXME: cannot pass user_ids as the salesman allocation only works in batch
+ res = lead.convert_opportunity(cr, uid, [lead_id], partner_id, [], team_id, context=context)
+ # FIXME: must perform salesman allocation in batch separately here
+ user_ids = vals.get('user_ids', False)
+ if user_ids:
+ lead.allocate_salesman(cr, uid, lead_ids, user_ids, team_id=team_id, context=context)
+ return res
def _merge_opportunity(self, cr, uid, ids, opportunity_ids, action='merge', context=None):
- #TOFIX: is it usefully ?
if context is None:
context = {}
- merge_opportunity = self.pool.get('crm.merge.opportunity')
res = False
- #If we convert in mass, don't merge if there is no other opportunity but no warning
- if action == 'merge' and (len(opportunity_ids) > 1 or not context.get('mass_convert') ):
- self.write(cr, uid, ids, {'opportunity_ids' : [(6,0, [opportunity_ids[0].id])]}, context=context)
- context.update({'lead_ids' : record_id, "convert" : True})
- res = merge_opportunity.merge(cr, uid, data.opportunity_ids, context=context)
+ # Expected: all newly-converted leads (active_ids) will be merged with the opportunity(ies)
+ # that have been selected in the 'opportunity_ids' m2m, with all these records
+ # merged into the first opportunity (and the rest deleted)
+ opportunity_ids = [o.id for o in opportunity_ids]
+ lead_ids = context.get('active_ids', [])
+ if action == 'merge' and lead_ids and opportunity_ids:
+ # Add the leads in the to-merge list, next to other opps
+ # (the fact that they're passed in context['lead_ids'] means that
+ # they cannot be selected to contain the result of the merge.
+ opportunity_ids.extend(lead_ids)
+ context.update({'lead_ids': lead_ids, "convert" : True})
+ res = self.pool.get('crm.lead').merge_opportunity(cr, uid, opportunity_ids, context=context)
return res
def action_apply(self, cr, uid, ids, context=None):
@@ -131,27 +140,37 @@ class crm_lead2opportunity_partner(osv.osv_memory):
"""
if not context:
context = {}
-
lead = self.pool.get('crm.lead')
lead_ids = context.get('active_ids', [])
data = self.browse(cr, uid, ids, context=context)[0]
self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids}, context=context)
- self._merge_opportunity(cr, uid, ids, data.opportunity_ids, data.action, context=context)
+ self._merge_opportunity(cr, uid, ids, data.opportunity_ids, data.name, context=context)
return lead.redirect_opportunity_view(cr, uid, lead_ids[0], context=context)
-crm_lead2opportunity_partner()
class crm_lead2opportunity_mass_convert(osv.osv_memory):
_name = 'crm.lead2opportunity.partner.mass'
_description = 'Mass Lead To Opportunity Partner'
_inherit = 'crm.lead2opportunity.partner'
-
_columns = {
- 'user_ids': fields.many2many('res.users', string='Salesmans'),
+ 'user_ids': fields.many2many('res.users', string='Salesmen'),
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
-
}
+
+ def default_get(self, cr, uid, fields, context=None):
+ res = super(crm_lead2opportunity_mass_convert, self).default_get(cr, uid, fields, context)
+ if 'partner_id' in fields:
+ # avoid forcing the partner of the first lead as default
+ res['partner_id'] = False
+ if 'action' in fields:
+ res['action'] = 'create'
+ if 'name' in fields:
+ res['name'] = 'convert'
+ if 'opportunity_ids' in fields:
+ res['opportunity_ids'] = False
+ return res
+
def _convert_opportunity(self, cr, uid, ids, vals, context=None):
data = self.browse(cr, uid, ids, context=context)[0]
salesteam_id = data.section_id and data.section_id.id or False
@@ -162,9 +181,6 @@ class crm_lead2opportunity_mass_convert(osv.osv_memory):
return super(crm_lead2opportunity_mass_convert, self)._convert_opportunity(cr, uid, ids, vals, context=context)
def mass_convert(self, cr, uid, ids, context=None):
- value = self.default_get(cr, uid, ['partner_id', 'opportunity_ids'], context=context)
- value['opportunity_ids'] = [(6, 0, value['opportunity_ids'])]
- self.write(cr, uid, ids, value, context=context)
return self.action_apply(cr, uid, ids, context=context)
-crm_lead2opportunity_mass_convert()
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/crm/wizard/crm_lead_to_opportunity_view.xml b/addons/crm/wizard/crm_lead_to_opportunity_view.xml
index 8829749efa3..6a826f7bc0e 100644
--- a/addons/crm/wizard/crm_lead_to_opportunity_view.xml
+++ b/addons/crm/wizard/crm_lead_to_opportunity_view.xml
@@ -37,24 +37,38 @@
crm.lead2opportunity.partner.mass
-
+
diff --git a/addons/crm/wizard/crm_lead_to_partner.py b/addons/crm/wizard/crm_lead_to_partner.py
index 98f114b4088..9b239bab7b9 100644
--- a/addons/crm/wizard/crm_lead_to_partner.py
+++ b/addons/crm/wizard/crm_lead_to_partner.py
@@ -50,20 +50,20 @@ class crm_lead2partner(osv.osv_memory):
def _select_partner(self, cr, uid, context=None):
if context is None:
context = {}
- lead = self.pool.get('crm.lead')
- partner = self.pool.get('res.partner')
- lead_ids = list(context and context.get('active_ids', []) or [])
- if not len(lead_ids):
+ if not context.get('active_model') == 'crm.lead' or not context.get('active_id'):
return False
- this = lead.browse(cr, uid, lead_ids[0], context=context)
- # Find partner address matches the email_from of the lead
- res = lead.message_partner_by_email(cr, uid, this.email_from, context=context)
- partner_id = res.get('partner_id', False)
- # Find partner name that matches the name of the lead
- if not partner_id and this.partner_name:
+ partner = self.pool.get('res.partner')
+ lead = self.pool.get('crm.lead')
+ this = lead.browse(cr, uid, context.get('active_id'), context=context)
+ partner_id = False
+ if this.email_from:
+ partner_ids = partner.search(cr, uid, [('email', '=', this.email_from)], context=context)
+ if partner_ids:
+ partner_id = partner_ids[0]
+ if not this.partner_id and this.partner_name:
partner_ids = partner.search(cr, uid, [('name', '=', this.partner_name)], context=context)
- if partner_ids and len(partner_ids):
- partner_id = partner_ids[0]
+ if partner_ids:
+ partner_id = partner_ids[0]
return partner_id
def default_get(self, cr, uid, fields, context=None):
@@ -107,15 +107,16 @@ class crm_lead2partner(osv.osv_memory):
lead_ids = context and context.get('active_ids') or []
data = self.browse(cr, uid, ids, context=context)[0]
partner_id = data.partner_id and data.partner_id.id or False
- partner_ids = lead.convert_partner(cr, uid, lead_ids, data.action, partner_id, context=context)
- return partner_ids[lead_ids[0]]
+ return lead.convert_partner(cr, uid, lead_ids, data.action, partner_id, context=context)
def make_partner(self, cr, uid, ids, context=None):
"""
This function Makes partner based on action.
"""
- partner_id = self._create_partner(cr, uid, ids, context=context)
- return self.pool.get('res.partner').redirect_partner_form(cr, uid, partner_id, context=context)
+ # Only called from Form view, so only meant to convert one Lead.
+ lead_id = context and context.get('active_id') or False
+ partner_ids_map = self._create_partner(cr, uid, ids, context=context)
+ return self.pool.get('res.partner').redirect_partner_form(cr, uid, partner_ids_map.get(lead_id, False), context=context)
crm_lead2partner()
diff --git a/addons/crm/wizard/crm_partner_to_opportunity_view.xml b/addons/crm/wizard/crm_partner_to_opportunity_view.xml
index 765a22de0a6..7f0b5957071 100644
--- a/addons/crm/wizard/crm_partner_to_opportunity_view.xml
+++ b/addons/crm/wizard/crm_partner_to_opportunity_view.xml
@@ -23,23 +23,5 @@
-
-
-
- Create Opportunity
- crm.partner2opportunity
- form
- form,tree,kanban,calendar
- new
-
-
-
-
-
diff --git a/addons/crm/wizard/crm_phonecall_to_partner.py b/addons/crm/wizard/crm_phonecall_to_partner.py
index 422af02f99d..715b7396c66 100644
--- a/addons/crm/wizard/crm_phonecall_to_partner.py
+++ b/addons/crm/wizard/crm_phonecall_to_partner.py
@@ -57,12 +57,10 @@ class crm_phonecall2partner(osv.osv_memory):
if context is None:
context = {}
phonecall = self.pool.get('crm.phonecall')
-
data = self.browse(cr, uid, ids, context=context)[0]
call_ids = context and context.get('active_ids') or []
partner_id = data.partner_id and data.partner_id.id or False
- partner_ids = phonecall.convert_partner(cr, uid, call_ids, data.action, partner_id, context=context)
- return partner_ids[call_ids[0]]
+ return phonecall.convert_partner(cr, uid, call_ids, data.action, partner_id, context=context)
crm_phonecall2partner()
diff --git a/addons/crm_caldav/i18n/nb.po b/addons/crm_caldav/i18n/nb.po
new file mode 100644
index 00000000000..c5a1c3cfe37
--- /dev/null
+++ b/addons/crm_caldav/i18n/nb.po
@@ -0,0 +1,33 @@
+# Norwegian Bokmal 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
, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-04 14:04+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-05 04:46+0000\n"
+"X-Generator: Launchpad (build 15901)\n"
+
+#. module: crm_caldav
+#: model:ir.actions.act_window,name:crm_caldav.action_caldav_browse
+msgid "Caldav Browse"
+msgstr "CalDAV Bla"
+
+#. module: crm_caldav
+#: model:ir.ui.menu,name:crm_caldav.menu_caldav_browse
+msgid "Synchronize This Calendar"
+msgstr "Synkroniser denne kalenderen"
+
+#. module: crm_caldav
+#: model:ir.model,name:crm_caldav.model_crm_meeting
+msgid "Meeting"
+msgstr "Møte"
diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py
index ec6252bcdca..535f6d20367 100644
--- a/addons/crm_claim/crm_claim.py
+++ b/addons/crm_claim/crm_claim.py
@@ -72,7 +72,7 @@ class crm_claim(base_stage, osv.osv):
_description = "Claim"
_order = "priority,date desc"
_inherit = ['mail.thread']
- _mail_compose_message = True
+
_columns = {
'id': fields.integer('ID', readonly=True),
'name': fields.char('Claim Subject', size=128, required=True),
@@ -194,13 +194,12 @@ class crm_claim(base_stage, osv.osv):
if custom_values is None: custom_values = {}
custom_values.update({
'name': msg.get('subject') or _("No Subject"),
- 'description': msg.get('body_text'),
+ 'description': msg.get('body'),
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
})
if msg.get('priority'):
custom_values['priority'] = msg.get('priority')
- custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from'), context=context))
return super(crm_claim,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
@@ -220,7 +219,7 @@ class crm_claim(base_stage, osv.osv):
'revenue': 'planned_revenue',
'probability':'probability'
}
- for line in msg['body_text'].split('\n'):
+ for line in msg['body'].split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower()):
@@ -239,16 +238,16 @@ class crm_claim(base_stage, osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
msg = _('Claim has been created .')
- return self.message_append_note(cr, uid, ids, body=msg, context=context)
+ return self.message_post(cr, uid, ids, body=msg, context=context)
def case_refuse_send_note(self, cr, uid, ids, context=None):
msg = _('Claim has been refused .')
- return self.message_append_note(cr, uid, ids, body=msg, context=context)
+ return self.message_post(cr, uid, ids, body=msg, context=context)
def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
""" Override of the (void) default notification method. """
stage_name = self.pool.get('crm.claim.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
- return self.message_append_note(cr, uid, ids, body= _("Stage changed to %s .") % (stage_name), context=context)
+ return self.message_post(cr, uid, ids, body= _("Stage changed to %s .") % (stage_name), context=context)
class res_partner(osv.osv):
diff --git a/addons/crm_claim/test/ui/claim_demo.yml b/addons/crm_claim/test/ui/claim_demo.yml
index ad8fdf3aa44..044e0482e93 100644
--- a/addons/crm_claim/test/ui/claim_demo.yml
+++ b/addons/crm_claim/test/ui/claim_demo.yml
@@ -9,7 +9,6 @@
-
!python {model: crm.claim}: |
try:
- self.message_update(cr, uid,[ref('crm_claim_4')], {'subject': 'Claim Update record','body_text': 'first training session completed',})
+ self.message_update(cr, uid,[ref('crm_claim_4')], {'subject': 'Claim Update record','body': 'first training session completed',})
except:
pass
-
\ No newline at end of file
diff --git a/addons/crm_helpdesk/crm_helpdesk.py b/addons/crm_helpdesk/crm_helpdesk.py
index 4b0cb2e3ffa..612537bdb77 100644
--- a/addons/crm_helpdesk/crm_helpdesk.py
+++ b/addons/crm_helpdesk/crm_helpdesk.py
@@ -38,7 +38,7 @@ class crm_helpdesk(base_state, osv.osv):
_description = "Helpdesk"
_order = "id desc"
_inherit = ['mail.thread']
- _mail_compose_message = True
+
_columns = {
'id': fields.integer('ID', readonly=True),
'name': fields.char('Name', size=128, required=True),
@@ -105,12 +105,11 @@ class crm_helpdesk(base_state, osv.osv):
if custom_values is None: custom_values = {}
custom_values.update({
'name': msg.get('subject') or _("No Subject"),
- 'description': msg.get('body_text'),
+ 'description': msg.get('body'),
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
})
- custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from'), context=context))
return super(crm_helpdesk,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
@@ -130,7 +129,7 @@ class crm_helpdesk(base_state, osv.osv):
'revenue': 'planned_revenue',
'probability':'probability'
}
- for line in msg['body_text'].split('\n'):
+ for line in msg['body'].split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower()):
@@ -149,7 +148,7 @@ class crm_helpdesk(base_state, osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
msg = _('Case has been created .')
- self.message_append_note(cr, uid, ids, body=msg, context=context)
+ self.message_post(cr, uid, ids, body=msg, context=context)
return True
diff --git a/addons/crm_helpdesk/report/crm_helpdesk_report.py b/addons/crm_helpdesk/report/crm_helpdesk_report.py
index 93a33d62819..6496935c2d2 100644
--- a/addons/crm_helpdesk/report/crm_helpdesk_report.py
+++ b/addons/crm_helpdesk/report/crm_helpdesk_report.py
@@ -97,7 +97,7 @@ class crm_helpdesk_report(osv.osv):
c.planned_cost,
count(*) as nbr,
extract('epoch' from (c.date_closed-c.create_date))/(3600*24) as delay_close,
- (SELECT count(id) FROM mail_message WHERE model='crm.helpdesk' AND res_id=c.id AND email_from IS NOT NULL) AS email,
+ (SELECT count(id) FROM mail_message WHERE model='crm.helpdesk' AND res_id=c.id AND type = 'email') AS email,
abs(avg(extract('epoch' from (c.date_deadline - c.date_closed)))/(3600*24)) as delay_expected
from
crm_helpdesk c
diff --git a/addons/crm_helpdesk/test/process/help-desk.yml b/addons/crm_helpdesk/test/process/help-desk.yml
index 320bd61c2af..4fd6a9ee9d7 100644
--- a/addons/crm_helpdesk/test/process/help-desk.yml
+++ b/addons/crm_helpdesk/test/process/help-desk.yml
@@ -23,7 +23,7 @@
!python {model: crm.helpdesk}: |
question_ids = self.search(cr, uid, [('email_from','=', 'Mr. John Right ')])
try:
- self.message_update(cr, uid, question_ids, {'subject': 'Link of product', 'body_text': 'www.openerp.com'})
+ self.message_update(cr, uid, question_ids, {'subject': 'Link of product', 'body': 'www.openerp.com'})
except:
pass
diff --git a/addons/crm_partner_assign/crm_lead_view.xml b/addons/crm_partner_assign/crm_lead_view.xml
index d4ff93d52fd..cd2e3f08c9a 100644
--- a/addons/crm_partner_assign/crm_lead_view.xml
+++ b/addons/crm_partner_assign/crm_lead_view.xml
@@ -19,8 +19,7 @@
attrs="{'invisible':[('partner_assigned_id','=',False)]}"
name="%(crm_lead_forward_to_partner_act)d"
icon="terp-mail-forward" type="action"
- context="{'default_name': 'partner', 'default_partner_id': partner_assigned_id}"
- />
+ context="{'default_composition_mode': 'forward', 'default_partner_ids': [partner_assigned_id]}"/>