diff --git a/addons/account/i18n/es.po b/addons/account/i18n/es.po index 79821f9d52b..be0a06a0e79 100644 --- a/addons/account/i18n/es.po +++ b/addons/account/i18n/es.po @@ -71,6 +71,8 @@ # Nicolás Broggi , 2019 # Luis González , 2019 # Gabriel Umaña , 2019 +# Luis Marin , 2019 +# Edilianny Sánchez , 2019 # msgid "" msgstr "" @@ -78,7 +80,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-16 07:59+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Gabriel Umaña , 2019\n" +"Last-Translator: Edilianny Sánchez , 2019\n" "Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2104,7 +2106,7 @@ msgstr "Completar automáticamente" #. module: account #: model:ir.model.fields,help:account.field_account_invoice__vendor_bill_id msgid "Auto-complete from a past bill." -msgstr "" +msgstr "Auto-completar de una factura anterior" #. module: account #: model_terms:ir.ui.view,arch_db:account.invoice_supplier_form @@ -2120,7 +2122,7 @@ msgstr "Auto-validar" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Autocomplete Vendor Bills (OCR + AI)" -msgstr "" +msgstr "Auto-completar facturas de proveedor (OCR + IA)" #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__module_account_invoice_extract @@ -2347,7 +2349,7 @@ msgstr "Extractos bancarios" #. module: account #: model:ir.model.fields,help:account.field_account_bank_statement_line__bank_account_id msgid "Bank account that was used in this transaction." -msgstr "" +msgstr "Cuenta bancaria usada en esta transacción" #. module: account #: model:ir.model.fields,help:account.field_account_setup_bank_manual_config__acc_type @@ -2435,7 +2437,7 @@ msgstr "Basado en pago" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Batch Payments" -msgstr "" +msgstr "Lote de pagos" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__belongs_to_company @@ -2972,7 +2974,7 @@ msgstr "Elija plantilla del plan contable" #: model_terms:ir.ui.view,arch_db:account.account_invoice_onboarding_sale_tax_form #: model_terms:ir.ui.view,arch_db:account.onboarding_sale_tax_step msgid "Choose a default sales tax for your products." -msgstr "" +msgstr "Seleccione los impuestos de venta predeterminados para sus productos." #. module: account #. openerp-web @@ -3372,7 +3374,7 @@ msgstr "Contrapartida" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_reconcile_model_form msgid "Counterpart Values" -msgstr "" +msgstr "Valores de la Contrapartida" #. module: account #: model:ir.model.fields,field_description:account.field_account_fiscal_position__country_id @@ -3447,22 +3449,22 @@ msgstr "Crear una nueva posición fiscal" #. module: account #: model_terms:ir.actions.act_window,help:account.action_incoterms_tree msgid "Create a new incoterm" -msgstr "" +msgstr "Crear un nuevo incoterm" #. module: account #: model_terms:ir.actions.act_window,help:account.product_product_action_purchasable msgid "Create a new purchasable product" -msgstr "" +msgstr "Crear un nuevo producto comprable" #. module: account #: model_terms:ir.actions.act_window,help:account.action_account_reconcile_model msgid "Create a new reconciliation model" -msgstr "" +msgstr "Crear un nuevo modelo de reconciliación" #. module: account #: model_terms:ir.actions.act_window,help:account.product_product_action_sellable msgid "Create a new sellable product" -msgstr "" +msgstr "Crear un nuevo producto vendible" #. module: account #: model_terms:ir.actions.act_window,help:account.action_tax_form @@ -3472,7 +3474,7 @@ msgstr "Crea un nuevo impuesto" #. module: account #: model_terms:ir.actions.act_window,help:account.action_invoice_in_refund msgid "Create a vendor credit note" -msgstr "" +msgstr "Crear una nota de crédito de proveedor" #. module: account #: model_terms:ir.ui.view,arch_db:account.tax_adjustments_wizard @@ -3508,7 +3510,7 @@ msgstr "Crea modelo" #. module: account #: selection:account.setup.bank.manual.config,create_or_link_option:0 msgid "Create new journal" -msgstr "" +msgstr "Crear un diario nuevo" #. module: account #: model_terms:ir.actions.act_window,help:account.rounding_list_action @@ -3849,7 +3851,7 @@ msgstr "Personalizar" #. module: account #: model_terms:ir.ui.view,arch_db:account.onboarding_invoice_layout_step msgid "Customize the look of your invoices." -msgstr "" +msgstr "Personalizar la apariencia de sus facturas" #. module: account #: model:account.incoterms,name:account.incoterm_DAF @@ -3960,6 +3962,10 @@ msgid "" "greater than the last day of a month, this number will instead select the " "last day of this month." msgstr "" +"Día del mes en que la factura debe llegar a su término. Si es cero o " +"negativo, este valor se ignorará y no se establecerá un día específico. Si " +"es mayor que el último día de un mes, este número seleccionará el último día" +" de este mes." #. module: account #: code:addons/account/models/company.py:51 @@ -4024,7 +4030,7 @@ msgstr "Secuencia de notas de crédito dedicada" #. module: account #: model:ir.model.fields,field_description:account.field_res_company__invoice_reference_type msgid "Default Communication Type" -msgstr "" +msgstr "Tipo de Comunicación por defecto" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__default_credit_account_id @@ -4050,7 +4056,7 @@ msgstr "Impuesto de compra por defecto" #. module: account #: model:ir.model.fields,help:account.field_res_config_settings__invoice_reference_type msgid "Default Reference Type on Invoices." -msgstr "" +msgstr "Tipo de Referencia por defecto en facturas." #. module: account #: model:ir.model.fields,field_description:account.field_res_company__account_sale_tax_id @@ -4075,12 +4081,12 @@ msgstr "Impuestos por defecto" #: model:ir.model.fields,field_description:account.field_res_company__incoterm_id #: model:ir.model.fields,field_description:account.field_res_config_settings__incoterm_id msgid "Default incoterm" -msgstr "" +msgstr "Por defecto Incoterm" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form msgid "Default payment communication on customer invoices" -msgstr "" +msgstr "Comunicación de pago por defecto en facturas de clientes" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form @@ -4091,13 +4097,13 @@ msgstr "Impuestos por defecto aplicados a transacciones locales" #: model:ir.model.fields,help:account.field_product_product__supplier_taxes_id #: model:ir.model.fields,help:account.field_product_template__supplier_taxes_id msgid "Default taxes used when buying the product." -msgstr "" +msgstr "Impuestos por defecto usados cuando se compra el producto" #. module: account #: model:ir.model.fields,help:account.field_product_product__taxes_id #: model:ir.model.fields,help:account.field_product_template__taxes_id msgid "Default taxes used when selling the product." -msgstr "" +msgstr "Impuestos por defecto usados cuando se vende el producto" #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form @@ -4253,7 +4259,7 @@ msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.invoice_form msgid "Disc (%)" -msgstr "" +msgstr "Descuento (%)" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_line__discount @@ -4327,7 +4333,7 @@ msgstr "Nombre a mostrar" #: model:ir.model.fields,field_description:account.field_res_company__qr_code #: model:ir.model.fields,field_description:account.field_res_config_settings__qr_code msgid "Display SEPA QR code" -msgstr "" +msgstr "Desplegar código SEPA QR" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_line__display_type @@ -4447,7 +4453,7 @@ msgstr "Fecha vencimiento" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_payment_term_line_form msgid "Due the" -msgstr "" +msgstr "Vencido al " #. module: account #: code:addons/account/models/account_invoice.py:1285 @@ -4787,12 +4793,12 @@ msgstr "Ejercicio fiscal" #. module: account #: model:ir.model.fields,help:account.field_account_financial_year_op__fiscalyear_last_day msgid "Fiscal year last day." -msgstr "" +msgstr "Último día del año fiscal." #. module: account #: model:ir.model.fields,help:account.field_account_financial_year_op__fiscalyear_last_month msgid "Fiscal year last month." -msgstr "" +msgstr "Último mes del año fiscal." #. module: account #: model:ir.model.fields,field_description:account.field_account_financial_year_op__fiscalyear_last_day @@ -4867,12 +4873,12 @@ msgstr "Seguidores (Empresas)" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__inbound_payment_method_ids msgid "For Incoming Payments" -msgstr "" +msgstr "Para pagos recibidos" #. module: account #: model:ir.model.fields,field_description:account.field_account_journal__outbound_payment_method_ids msgid "For Outgoing Payments" -msgstr "" +msgstr "Para pagos salientes" #. module: account #: model:ir.model.fields,help:account.field_account_payment_term_line__value_amount @@ -5451,7 +5457,7 @@ msgstr "Importar archivos .qif" #. module: account #: model:ir.actions.act_window,name:account.account_invoice_import_wizard_action msgid "Import Vendor Bills" -msgstr "" +msgstr "Importar facturas de proveedor" #. module: account #: model:ir.model,name:account.model_account_invoice_import_wizard @@ -5815,7 +5821,7 @@ msgstr "Número de Factura:" #. module: account #: model:ir.model.fields,field_description:account.field_res_config_settings__module_account_payment msgid "Invoice Online Payment" -msgstr "" +msgstr "Pago en línea de la factura" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_line__invoice_id @@ -5869,7 +5875,7 @@ msgstr "Factura pagada" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_invoice_send_wizard_form msgid "Invoice send & Print" -msgstr "" +msgstr "Imprimir y enviar factura" #. module: account #: model:mail.message.subtype,description:account.mt_invoice_validated @@ -5965,7 +5971,7 @@ msgstr "Facturación" #: selection:account.reconcile.model,match_amount:0 #: selection:account.reconcile.model.template,match_amount:0 msgid "Is Between" -msgstr "" +msgstr "Es entre" #. module: account #: model:ir.model.fields,field_description:account.field_account_bank_statement__message_is_follower @@ -5978,18 +5984,18 @@ msgstr "Es un seguidor" #: selection:account.reconcile.model,match_amount:0 #: selection:account.reconcile.model.template,match_amount:0 msgid "Is Greater Than" -msgstr "" +msgstr "es mayor que" #. module: account #: selection:account.reconcile.model,match_amount:0 #: selection:account.reconcile.model.template,match_amount:0 msgid "Is Lower Than" -msgstr "" +msgstr "es menor que" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__printed msgid "Is Printed" -msgstr "" +msgstr "es impreso" #. module: account #: model:ir.model.fields,field_description:account.field_account_reconcile_model__is_second_tax_price_included @@ -6344,7 +6350,7 @@ msgstr "Etiqueta" #: model:ir.model.fields,field_description:account.field_account_reconcile_model__match_label_param #: model:ir.model.fields,field_description:account.field_account_reconcile_model_template__match_label_param msgid "Label Parameter" -msgstr "" +msgstr "Parámetro de la etiqueta" #. module: account #: model:ir.model.fields,field_description:account.field_account_tax__description @@ -6574,7 +6580,7 @@ msgstr "Fecha de la última conciliación de facturas y pagos" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice_send__layout msgid "Layout" -msgstr "" +msgstr "Diseño" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_position_form @@ -6678,7 +6684,7 @@ msgstr "Registrar una nota interna" #. module: account #: model_terms:ir.ui.view,arch_db:account.onboarding_invoice_layout_step msgid "Looks great!" -msgstr "" +msgstr "¡Se ve muy bien!" #. module: account #: code:addons/account/models/account_bank_statement.py:175 @@ -7383,7 +7389,7 @@ msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_payment_term_line_form msgid "On the" -msgstr "" +msgstr "En el " #. module: account #: model_terms:ir.ui.view,arch_db:account.account_invoice_confirm_view @@ -7844,7 +7850,7 @@ msgstr "Diferencia en pago" #: model:ir.model.fields,field_description:account.field_account_payment__payment_difference_handling #: model:ir.model.fields,field_description:account.field_account_register_payments__payment_difference_handling msgid "Payment Difference Handling" -msgstr "" +msgstr "Manejo de diferencias de pago" #. module: account #: model:ir.model.fields,field_description:account.field_account_abstract_payment__journal_id @@ -7906,7 +7912,7 @@ msgstr "Payment Receipt:" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__reference msgid "Payment Ref." -msgstr "" +msgstr "Ref. de Pago" #. module: account #: model:ir.model.fields,field_description:account.field_account_payment__payment_reference @@ -8234,7 +8240,7 @@ msgstr "Previsualizar" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_invoice_send_wizard_form msgid "Preview as a PDF" -msgstr "" +msgstr "Vista previa como un PDF" #. module: account #: model_terms:ir.ui.view,arch_db:account.invoice_form @@ -8631,14 +8637,14 @@ msgstr "Registrar pagos" #. module: account #: model_terms:ir.actions.act_window,help:account.action_bank_statement_tree msgid "Register a bank statement" -msgstr "" +msgstr "Registrar un extracto bancario" #. module: account #: model_terms:ir.actions.act_window,help:account.action_account_payments #: model_terms:ir.actions.act_window,help:account.action_account_payments_payable #: model_terms:ir.actions.act_window,help:account.action_account_payments_transfer msgid "Register a payment" -msgstr "" +msgstr "Registrar un pago" #. module: account #: selection:account.account.type,type:0 @@ -8921,7 +8927,7 @@ msgstr "Impuesto de Ventas" #. module: account #: model:ir.actions.act_window,name:account.action_open_account_onboarding_sale_tax msgid "Sales tax" -msgstr "" +msgstr "Impuesto de venta" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__user_id @@ -8940,7 +8946,7 @@ msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.onboarding_sample_invoice_step msgid "Sample Invoice" -msgstr "" +msgstr "Factura ejemplo" #. module: account #: code:addons/account/models/company.py:485 @@ -9201,7 +9207,7 @@ msgstr "Enviar correo electrónico" #: code:addons/account/models/account_invoice.py:622 #, python-format msgid "Send Invoice" -msgstr "" +msgstr "Enviar factura" #. module: account #: selection:account.abstract.payment,payment_type:0 @@ -9218,12 +9224,12 @@ msgstr "Enviar recibo por correo electrónico" #. module: account #: model:ir.actions.act_window,name:account.action_open_account_onboarding_sample_invoice msgid "Send a sample invoice" -msgstr "" +msgstr "Enviar una factura ejemplo" #. module: account #: model_terms:ir.ui.view,arch_db:account.onboarding_sample_invoice_step msgid "Send an invoice to test the customer portal." -msgstr "" +msgstr "Enviar una factura para probar el portal del cliente." #. module: account #: model_terms:ir.ui.view,arch_db:account.onboarding_sample_invoice_step @@ -9265,7 +9271,7 @@ msgstr "Secuencia" #: code:addons/account/static/src/xml/account_reconciliation.xml:249 #, python-format msgid "Set" -msgstr "" +msgstr "Establecer" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_payment_form @@ -9292,7 +9298,7 @@ msgstr "Establezca activo en falso para ocultar el impuesto sin eliminarlo." #. module: account #: model_terms:ir.ui.view,arch_db:account.onboarding_sale_tax_step msgid "Set taxes" -msgstr "" +msgstr "Establecer impuestos" #. module: account #: model:ir.model.fields,help:account.field_account_chart_template__visible @@ -9368,7 +9374,7 @@ msgstr "Mostrar características de contabilidad completas" #: model:ir.model.fields,field_description:account.field_account_payment__show_partner_bank_account #: model:ir.model.fields,field_description:account.field_account_register_payments__show_partner_bank_account msgid "Show Partner Bank Account" -msgstr "" +msgstr "Mostrar cuenta bancaria del Contacto" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_tax_search @@ -9933,7 +9939,7 @@ msgstr "Impuestos" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_move_form msgid "Taxes Applied" -msgstr "" +msgstr "Impuestos aplicados" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_position_template_form @@ -10405,6 +10411,7 @@ msgstr "La divisa utilizada para introducir asientos." #, python-format msgid "The day of the month used for this term must be stricly positive." msgstr "" +"El día del mes utilizado para este término debe ser estrictamente positivo." #. module: account #: code:addons/account/models/account_bank_statement.py:193 @@ -10594,6 +10601,8 @@ msgid "" "The payment communication that will be automatically populated once the " "invoice validation. You can also write a free communication." msgstr "" +"El comentario de pago que se completará automáticamente una vez que la " +"factura sea validada. También puedes escribir un comentario libre." #. module: account #: model_terms:ir.ui.view,arch_db:account.res_config_settings_view_form @@ -11694,7 +11703,7 @@ msgstr "Nota de crédito de proveedor" #. module: account #: model:ir.model.fields,field_description:account.field_account_invoice__vendor_display_name msgid "Vendor Display Name" -msgstr "" +msgstr "Nombre para mostrar del proveedor" #. module: account #: code:addons/account/models/account_payment.py:774 @@ -12207,7 +12216,7 @@ msgstr "" #: code:addons/account/models/account_invoice.py:1726 #, python-format msgid "You must first select a partner." -msgstr "" +msgstr "Primero debe seleccionar un Contacto" #. module: account #. openerp-web @@ -12303,7 +12312,7 @@ msgstr "código" #. module: account #: selection:account.payment.term.line,option:0 msgid "day(s) after the end of the invoice month" -msgstr "" +msgstr "día(s) después del final del mes de la factura" #. module: account #: selection:account.payment.term.line,option:0 diff --git a/addons/account/i18n/fr.po b/addons/account/i18n/fr.po index ef64c1af150..1288fa09eb5 100644 --- a/addons/account/i18n/fr.po +++ b/addons/account/i18n/fr.po @@ -77,6 +77,8 @@ # Martin Trigaux, 2019 # Eloïse Stilmant , 2019 # Alain van de Werve , 2019 +# Khalyomede Khalyomede , 2019 +# Fabien Bourgeois , 2019 # msgid "" msgstr "" @@ -84,7 +86,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-16 07:59+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Alain van de Werve , 2019\n" +"Last-Translator: Fabien Bourgeois , 2019\n" "Language-Team: French (https://www.transifex.com/odoo/teams/41243/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -646,7 +648,7 @@ msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.report_invoice_document msgid "Unit Price" -msgstr "" +msgstr "Prix Unitaire" #. module: account #: model_terms:ir.ui.view,arch_db:account.account_journal_dashboard_kanban_view @@ -12207,6 +12209,8 @@ msgid "" "You cannot set a currency on this account as it already has some journal " "entries having a different foreign currency." msgstr "" +"Vous ne pouvez pas choisir une devise pour ce compte car il existe déjà des " +"entrées dans le journal ayant des devises différentes." #. module: account #: code:addons/account/models/account.py:301 diff --git a/addons/account/i18n/hu.po b/addons/account/i18n/hu.po index c7a13eefe95..861fda09f13 100644 --- a/addons/account/i18n/hu.po +++ b/addons/account/i18n/hu.po @@ -6487,7 +6487,7 @@ msgstr "Levelezőlista" #: model:ir.model.fields,field_description:account.field_account_invoice__message_main_attachment_id #: model:ir.model.fields,field_description:account.field_account_payment__message_main_attachment_id msgid "Main Attachment" -msgstr "" +msgstr "Fő melléklet" #. module: account #: model:ir.model.fields,help:account.field_res_config_settings__currency_id diff --git a/addons/account/i18n/nb.po b/addons/account/i18n/nb.po index d3d0bf7705e..516cc84eeb6 100644 --- a/addons/account/i18n/nb.po +++ b/addons/account/i18n/nb.po @@ -11,8 +11,8 @@ # Nils Fredrik Gjerull, 2018 # Jorunn D. Newth, 2019 # Jan Pedro Tumusok , 2019 -# Marius Stedjan , 2019 # Martin Trigaux, 2019 +# Marius Stedjan , 2019 # msgid "" msgstr "" @@ -20,7 +20,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-16 07:59+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Martin Trigaux, 2019\n" +"Last-Translator: Marius Stedjan , 2019\n" "Language-Team: Norwegian Bokmål (https://www.transifex.com/odoo/teams/41243/nb/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2500,12 +2500,12 @@ msgstr "" #. module: account #: model:ir.model,name:account.model_cash_box_in msgid "Cash Box In" -msgstr "" +msgstr "Kasseinnskudd" #. module: account #: model:ir.model,name:account.model_cash_box_out msgid "Cash Box Out" -msgstr "" +msgstr "Kasseuttak" #. module: account #: code:addons/account/models/account_bank_statement.py:212 diff --git a/addons/account/i18n/nl.po b/addons/account/i18n/nl.po index 7789ff96a59..c8ced565d47 100644 --- a/addons/account/i18n/nl.po +++ b/addons/account/i18n/nl.po @@ -13,12 +13,12 @@ # Cas Vissers , 2019 # Gunther Clauwaert , 2019 # Martin Trigaux, 2019 -# Erwin van der Ploeg , 2019 # Cas Vissers , 2019 # Jordy Blankestijn , 2019 # Wynand Tastenhoye , 2019 # dpms , 2019 # Erik-Jan Bulthuis , 2019 +# Erwin van der Ploeg , 2019 # Yenthe Van Ginneken , 2019 # msgid "" @@ -252,12 +252,12 @@ msgstr "Concept factuur" #: model_terms:ir.ui.view,arch_db:account.account_move_line_view_kanban #: model_terms:ir.ui.view,arch_db:account.view_account_move_kanban msgid "" -msgstr "" +msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.view_account_payment_kanban msgid "" -msgstr "" +msgstr "" #. module: account #: model_terms:ir.ui.view,arch_db:account.portal_invoice_page @@ -270,6 +270,8 @@ msgid "" "" msgstr "" +"" #. module: account #: model_terms:ir.ui.view,arch_db:account.portal_invoice_page @@ -365,6 +367,8 @@ msgid "" "" msgstr "" +"" #. module: account #: model_terms:ir.ui.view,arch_db:account.invoice_supplier_form @@ -10321,7 +10325,7 @@ msgstr "Het dagboek waarin automatisch koersverschillen worden geboekt" #: code:addons/account/static/src/js/reconciliation/reconciliation_model.js:585 #, python-format msgid "The amount %s is not a valid partial amount" -msgstr "" +msgstr "Het bedrag %s is geen geldig gedeeltelijk bedrag" #. module: account #: model:ir.model.fields,help:account.field_account_bank_statement_line__amount_currency diff --git a/addons/account/i18n/ro.po b/addons/account/i18n/ro.po index f9c7e5ecfea..7001c5548eb 100644 --- a/addons/account/i18n/ro.po +++ b/addons/account/i18n/ro.po @@ -7,10 +7,10 @@ # sharkutz , 2018 # Iulian Cutui , 2018 # Bogdan Nistor , 2018 -# Dorin Hongu , 2019 # Sorin Trifu , 2019 # Cozmin Candea , 2019 # Martin Trigaux, 2019 +# Dorin Hongu , 2019 # msgid "" msgstr "" @@ -18,7 +18,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-16 07:59+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Martin Trigaux, 2019\n" +"Last-Translator: Dorin Hongu , 2019\n" "Language-Team: Romanian (https://www.transifex.com/odoo/teams/41243/ro/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -9058,7 +9058,7 @@ msgstr "" #. module: account #: model:res.groups,name:account.group_account_user msgid "Show Full Accounting Features" -msgstr "" +msgstr "Afișați toate funcțiile din contabilitate" #. module: account #: model:ir.model.fields,field_description:account.field_account_abstract_payment__show_partner_bank_account diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 60713fc8dd1..7c3a0777e84 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -67,12 +67,11 @@ class AccountAccount(models.Model): @api.constrains('user_type_id') def _check_user_type_id(self): data_unaffected_earnings = self.env.ref('account.data_unaffected_earnings') - for company in self.mapped('company_id'): - account_unaffected_earnings = self.search([ - ('company_id', '=', company.id), - ('user_type_id', '=', data_unaffected_earnings.id), - ]) - if len(account_unaffected_earnings) >= 2: + result = self.read_group([('user_type_id', '=', data_unaffected_earnings.id)], ['company_id'], ['company_id']) + for res in result: + if res.get('company_id_count', 0) >= 2: + account_unaffected_earnings = self.search([('company_id', '=', res['company_id'][0]), + ('user_type_id', '=', data_unaffected_earnings.id)]) raise ValidationError(_('You cannot have more than one account with "Current Year Earnings" as type. (accounts: %s)') % [a.code for a in account_unaffected_earnings]) name = fields.Char(required=True, index=True) @@ -228,7 +227,6 @@ class AccountAccount(models.Model): self.group_id = group @api.multi - @api.depends('name', 'code') def name_get(self): result = [] for account in self: @@ -802,7 +800,6 @@ class AccountJournal(models.Model): }).id @api.multi - @api.depends('name', 'currency_id', 'company_id', 'company_id.currency_id') def name_get(self): res = [] for journal in self: @@ -947,7 +944,6 @@ class AccountTax(models.Model): default = dict(default or {}, name=_("%s (Copy)") % self.name) return super(AccountTax, self).copy(default=default) - @api.depends('name', 'type_tax_use') def name_get(self): if not self._context.get('append_type_to_tax_name'): return super(AccountTax, self).name_get() diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 113980d1306..ad10a04def7 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -551,6 +551,8 @@ class AccountBankStatementLine(models.Model): :returns: The journal entries with which the transaction was matched. If there was at least an entry in counterpart_aml_dicts or new_aml_dicts, this list contains the move created by the reconciliation, containing entries for the statement.line (1), the counterpart move lines (0..*) and the new move lines (0..*). """ + payable_account_type = self.env.ref('account.data_account_type_payable') + receivable_account_type = self.env.ref('account.data_account_type_receivable') counterpart_aml_dicts = counterpart_aml_dicts or [] payment_aml_rec = payment_aml_rec or self.env['account.move.line'] new_aml_dicts = new_aml_dicts or [] @@ -571,10 +573,16 @@ class AccountBankStatementLine(models.Model): raise UserError(_('A selected move line was already reconciled.')) if isinstance(aml_dict['move_line'], int): aml_dict['move_line'] = aml_obj.browse(aml_dict['move_line']) + + account_types = self.env['account.account.type'] for aml_dict in (counterpart_aml_dicts + new_aml_dicts): if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], int): # Transform the value in the format required for One2many and Many2many fields aml_dict['tax_ids'] = [(4, id, None) for id in aml_dict['tax_ids']] + + user_type_id = self.env['account.account'].browse(aml_dict.get('account_id')).user_type_id + if user_type_id in [payable_account_type, receivable_account_type] and user_type_id not in account_types: + account_types |= user_type_id if any(line.journal_entry_ids for line in self): raise UserError(_('A selected statement line was already reconciled with an account move.')) @@ -610,6 +618,8 @@ class AccountBankStatementLine(models.Model): payment = self.env['account.payment'] if abs(total)>0.00001: payment_vals = self._prepare_payment_vals(total) + if self.partner_id and len(account_types) == 1: + payment_vals['partner_type'] = 'customer' if account_types == receivable_account_type else 'supplier' payment = payment.create(payment_vals) # Complete dicts to create both counterpart move lines and write-offs diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 1bb87f853ae..ee22f64f699 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -147,8 +147,13 @@ class AccountInvoice(models.Model): amount_to_show = currency._convert(abs(line.amount_residual), self.currency_id, self.company_id, line.date or fields.Date.today()) if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding): continue + if line.ref : + title = '%s : %s' % (line.move_id.name, line.ref) + else: + title = line.move_id.name info['content'].append({ 'journal_name': line.ref or line.move_id.name, + 'title': title, 'amount': amount_to_show, 'currency': currency_id.symbol, 'id': line.id, @@ -1402,6 +1407,20 @@ class AccountInvoice(models.Model): return result @api.model + def _refund_tax_lines_account_change(self, lines, taxes_to_change): + # Let's change the account on tax lines when + # @param {list} lines: a list of orm commands + # @param {dict} taxes_to_change + # key: tax ID, value: refund account + + if not taxes_to_change: + return lines + + for line in lines: + if isinstance(line[2], dict) and line[2]['tax_id'] in taxes_to_change: + line[2]['account_id'] = taxes_to_change[line[2]['tax_id']] + return lines + def _get_refund_common_fields(self): return ['partner_id', 'payment_term_id', 'account_id', 'currency_id', 'journal_id'] @@ -1447,7 +1466,12 @@ class AccountInvoice(models.Model): values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids) tax_lines = invoice.tax_line_ids - values['tax_line_ids'] = self._refund_cleanup_lines(tax_lines) + taxes_to_change = { + line.tax_id.id: line.tax_id.refund_account_id.id + for line in tax_lines.filtered(lambda l: l.tax_id.refund_account_id != l.tax_id.account_id) + } + cleaned_tax_lines = self._refund_cleanup_lines(tax_lines) + values['tax_line_ids'] = self._refund_tax_lines_account_change(cleaned_tax_lines, taxes_to_change) if journal_id: journal = self.env['account.journal'].browse(journal_id) diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index f4753ef4796..ee8087a33b0 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -185,13 +185,14 @@ class account_journal(models.Model): self.env.cr.execute(query, query_args) query_results_drafts = self.env.cr.dictfetchall() - today = datetime.today() - query = """SELECT amount_total, currency_id AS currency, type, date_invoice, company_id FROM account_invoice WHERE journal_id = %s AND date <= %s AND state = 'open';""" + today = fields.Date.today() + query = """SELECT residual_signed as amount_total, currency_id AS currency, type, date_invoice, company_id FROM account_invoice WHERE journal_id = %s AND date <= %s AND state = 'open';""" self.env.cr.execute(query, (self.id, today)) late_query_results = self.env.cr.dictfetchall() - (number_waiting, sum_waiting) = self._count_results_and_sum_amounts(query_results_to_pay, currency) - (number_draft, sum_draft) = self._count_results_and_sum_amounts(query_results_drafts, currency) - (number_late, sum_late) = self._count_results_and_sum_amounts(late_query_results, currency) + curr_cache = {} + (number_waiting, sum_waiting) = self._count_results_and_sum_amounts(query_results_to_pay, currency, curr_cache=curr_cache) + (number_draft, sum_draft) = self._count_results_and_sum_amounts(query_results_drafts, currency, curr_cache=curr_cache) + (number_late, sum_late) = self._count_results_and_sum_amounts(late_query_results, currency, curr_cache=curr_cache) difference = currency.round(last_balance-account_sum) + 0.0 return { @@ -226,23 +227,45 @@ class account_journal(models.Model): gather the bills in draft state data, and the arguments dictionary to use to run it as its second. """ - return ("""SELECT state, amount_total, currency_id AS currency, type, date_invoice, company_id - FROM account_invoice + # there is no account_move_lines for draft invoices, so no relevant residual_signed value + return ("""SELECT state, + (CASE WHEN inv.type in ('out_invoice', 'in_invoice') + THEN inv.amount_total + ELSE (-1 * inv.amount_total) + END) AS amount_total, + inv.currency_id AS currency, + inv.type, + inv.date_invoice, + inv.company_id + FROM account_invoice inv WHERE journal_id = %(journal_id)s AND state = 'draft';""", {'journal_id':self.id}) - def _count_results_and_sum_amounts(self, results_dict, target_currency): + def _count_results_and_sum_amounts(self, results_dict, target_currency, curr_cache=None): """ Loops on a query result to count the total number of invoices and sum their amount_total field (expressed in the given target currency). + amount_total must be signed ! """ rslt_count = 0 rslt_sum = 0.0 + # Create a cache with currency rates to avoid unnecessary SQL requests. Do not copy + # curr_cache on purpose, so the dictionary is modified and can be re-used for subsequent + # calls of the method. + curr_cache = {} if curr_cache is None else curr_cache for result in results_dict: cur = self.env['res.currency'].browse(result.get('currency')) company = self.env['res.company'].browse(result.get('company_id')) or self.env.user.company_id rslt_count += 1 - type_factor = result.get('type') in ('in_refund', 'out_refund') and -1 or 1 - rslt_sum += type_factor * cur._convert( - result.get('amount_total'), target_currency, company, result.get('date_invoice') or fields.Date.today()) + date = result.get('date_invoice') or fields.Date.today() + + amount = result.get('amount_total', 0) + if cur != target_currency: + key = (cur, target_currency, company, date) + # Using setdefault will call _get_conversion_rate, so we explicitly check the + # existence of the key in the cache instead. + if key not in curr_cache: + curr_cache[key] = self.env['res.currency']._get_conversion_rate(*key) + amount *= curr_cache[key] + rslt_sum += target_currency.round(amount) return (rslt_count, rslt_sum) @api.multi @@ -360,7 +383,7 @@ class account_journal(models.Model): action['view_id'] = False if self.type == 'purchase': new_help = self.env['account.invoice'].with_context(ctx).complete_empty_list_help() - action.update({'help': action.get('help', '') + new_help}) + action.update({'help': (action.get('help') or '') + new_help}) return action @api.multi diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 7f09cdee78c..b37bd67f027 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -429,9 +429,6 @@ class AccountMove(models.Model): journal_id=journal_id, auto=auto) reversed_moves |= reversed_move - #unreconcile all lines reversed - aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity') - aml.remove_move_reconcile() #reconcile together the reconcilable (or the liquidity aml) and their newly created counterpart for account in set([x.account_id for x in aml]): to_rec = aml.filtered(lambda y: y.account_id == account) @@ -448,6 +445,18 @@ class AccountMove(models.Model): def open_reconcile_view(self): return self.line_ids.open_reconcile_view() + # FIXME: Clarify me and change me in master + @api.multi + def action_duplicate(self): + self.ensure_one() + action = self.env.ref('account.action_move_journal_line').read()[0] + action['target'] = 'inline' + action['context'] = dict(self.env.context) + action['context']['view_no_maturity'] = False + action['views'] = [(self.env.ref('account.view_move_form').id, 'form')] + action['res_id'] = self.copy().id + return action + @api.model def _run_reverses_entries(self): ''' This method is called from a cron job. ''' @@ -638,7 +647,7 @@ class AccountMoveLine(models.Model): index=True, store=True, copy=False) # related is required blocked = fields.Boolean(string='No Follow-up', default=False, help="You can check this box to mark this journal item as a litigation with the associated partner") - date_maturity = fields.Date(string='Due date', index=True, required=True, + date_maturity = fields.Date(string='Due date', index=True, required=True, copy=False, help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.") date = fields.Date(related='move_id.date', string='Date', index=True, store=True, copy=False, readonly=False) # related is required analytic_line_ids = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines', oldname="analytic_lines") @@ -897,22 +906,13 @@ class AccountMoveLine(models.Model): ret = self._reconcile_lines(debit_moves, credit_moves, field) return ret - @api.multi - def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False): - # Empty self can happen if the user tries to reconcile entries which are already reconciled. - # The calling method might have filtered out reconciled lines. - if not self: - return True - + def _check_reconcile_validity(self): #Perform all checks on lines company_ids = set() all_accounts = [] - partners = set() for line in self: company_ids.add(line.company_id.id) all_accounts.append(line.account_id) - if (line.account_id.internal_type in ('receivable', 'payable')): - partners.add(line.partner_id.id) if line.reconciled: raise UserError(_('You are trying to reconcile some entries that are already reconciled.')) if len(company_ids) > 1: @@ -922,6 +922,14 @@ class AccountMoveLine(models.Model): if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'): raise UserError(_('Account %s (%s) does not allow reconciliation. First change the configuration of this account to allow it.') % (all_accounts[0].name, all_accounts[0].code)) + @api.multi + def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False): + # Empty self can happen if the user tries to reconcile entries which are already reconciled. + # The calling method might have filtered out reconciled lines. + if not self: + return True + + self._check_reconcile_validity() #reconcile everything that can be remaining_moves = self.auto_reconcile_lines() @@ -939,7 +947,7 @@ class AccountMoveLine(models.Model): #add writeoff line to reconcile algorithm and finish the reconciliation remaining_moves = (remaining_moves + writeoff_to_reconcile).auto_reconcile_lines() # Check if reconciliation is total or needs an exchange rate entry to be created - (self+writeoff_to_reconcile).check_full_reconcile() + (self + writeoff_to_reconcile).check_full_reconcile() return True def _create_writeoff(self, writeoff_vals): @@ -983,8 +991,9 @@ class AccountMoveLine(models.Model): raise UserError(_("Either pass both debit and credit or none.")) if 'date' not in vals: vals['date'] = self._context.get('date_p') or fields.Date.today() - if vals['date'] < date: - date = vals['date'] + vals['date'] = fields.Date.to_date(vals['date']) + if vals['date'] and vals['date'] < date: + date = vals['date'] if 'name' not in vals: vals['name'] = self._context.get('comment') or _('Write-Off') if 'analytic_account_id' not in vals: diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index c83d2b31ed3..bb2e7ff4f03 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -324,7 +324,7 @@ class account_register_payments(models.TransientModel): pmt_communication = self.show_communication_field and self.communication \ or self.group_invoices and ' '.join([inv.reference or inv.number for inv in invoices]) \ or invoices[0].reference # in this case, invoices contains only one element, since group_invoices is False - return { + values = { 'journal_id': self.journal_id.id, 'payment_method_id': self.payment_method_id.id, 'payment_date': self.payment_date, @@ -337,8 +337,13 @@ class account_register_payments(models.TransientModel): 'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type], 'partner_bank_account_id': bank_account.id, 'multi': False, + 'payment_difference_handling': self.payment_difference_handling, + 'writeoff_account_id': self.writeoff_account_id.id, + 'writeoff_label': self.writeoff_label, } + return values + @api.multi def get_payments_vals(self): '''Compute the values for payments. @@ -429,6 +434,8 @@ class account_payment(models.Model): if move_line.account_id.reconcile: move_line_id = move_line.id break; + if not self.partner_id: + raise UserError(_("Payments without a customer can't be matched")) action_context = {'company_ids': [self.company_id.id], 'partner_ids': [self.partner_id.commercial_partner_id.id]} if self.partner_type == 'customer': action_context.update({'mode': 'customers'}) diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 1b4f3dc45f8..552e69a03f7 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -173,7 +173,7 @@ class AccountChartTemplate(models.Model): # (won't work well for multi-company) company = self.env.user.company_id # If we don't have any chart of account on this company, install this chart of account - if not company.chart_template_id: + if not self.existing_accounting(company): self.load_for_current_company(15.0, 15.0) def load_for_current_company(self, sale_tax_rate, purchase_tax_rate): diff --git a/addons/account/models/company.py b/addons/account/models/company.py index ea47862f89e..3ddd4881f66 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -397,8 +397,13 @@ Best Regards,''')) ('user_type_id', '=', unaffected_earnings_type.id)]) if account: return account[0] + # Do not assume '999999' doesn't exist since the user might have created such an account + # manually. + code = 999999 + while self.env['account.account'].search([('code', '=', str(code)), ('company_id', '=', self.id)]): + code -= 1 return self.env['account.account'].create({ - 'code': '999999', + 'code': str(code), 'name': _('Undistributed Profits/Losses'), 'user_type_id': unaffected_earnings_type.id, 'company_id': self.id, diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index f2485b379e6..85ec5f9bb66 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -204,7 +204,7 @@ class ResPartner(models.Model): @api.multi def _credit_debit_get(self): - tables, where_clause, where_params = self.env['account.move.line']._query_get() + tables, where_clause, where_params = self.env['account.move.line'].with_context(company_id=self.env.user.company_id.id)._query_get() where_params = [tuple(self.ids)] + where_params if where_clause: where_clause = 'AND ' + where_clause diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py index 2fb6aad94b8..df92a5b45f1 100644 --- a/addons/account/models/reconciliation_widget.py +++ b/addons/account/models/reconciliation_widget.py @@ -224,6 +224,27 @@ class AccountReconciliation(models.AbstractModel): """ Returns the data required for the invoices & payments matching of partners/accounts. If an argument is None, fetch all related reconciliations. Use [] to fetch nothing. """ + MoveLine = self.env['account.move.line'] + aml_ids = self._context.get('active_ids') and self._context.get('active_model') == 'account.move.line' and tuple(self._context.get('active_ids')) + if aml_ids: + aml = MoveLine.browse(aml_ids) + aml._check_reconcile_validity() + account = aml[0].account_id + currency = account.currency_id or account.company_id.currency_id + return { + 'accounts': [{ + 'reconciliation_proposition': self._prepare_move_lines(aml, target_currency=currency), + 'company_id': account.company_id.id, + 'currency_id': currency.id, + 'mode': 'accounts', + 'account_id': account.id, + 'account_name': account.name, + 'account_code': account.code, + }], + 'customers': [], + 'suppliers': [], + } + return { 'customers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'receivable'), 'suppliers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'payable'), @@ -344,7 +365,6 @@ class AccountReconciliation(models.AbstractModel): # be reconciled. return [r for r in rows if r['reconciliation_proposition']] + [r for r in rows if not r['reconciliation_proposition']] - @api.model def process_move_lines(self, data): """ Used to validate a batch of reconciliations in a single call diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index bc660b494b3..08d0b976263 100644 --- a/addons/account/report/account_aged_partner_balance.py +++ b/addons/account/report/account_aged_partner_balance.py @@ -142,6 +142,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): if not self.env.user.company_id.currency_id.is_zero(line_amount): partners_amount[partner_id] += line_amount + lines.setdefault(partner_id, []) lines[partner_id].append({ 'line': line, 'amount': line_amount, @@ -179,6 +180,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): line_amount -= partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, user_company, date_from) if not self.env.user.company_id.currency_id.is_zero(line_amount): undue_amounts[partner_id] += line_amount + lines.setdefault(partner_id, []) lines[partner_id].append({ 'line': line, 'amount': line_amount, diff --git a/addons/account/static/src/js/account_payment_field.js b/addons/account/static/src/js/account_payment_field.js index a20a681b0e6..c4dec36dda0 100644 --- a/addons/account/static/src/js/account_payment_field.js +++ b/addons/account/static/src/js/account_payment_field.js @@ -162,4 +162,8 @@ var ShowPaymentLineWidget = AbstractField.extend({ field_registry.add('payment', ShowPaymentLineWidget); +return { + ShowPaymentLineWidget: ShowPaymentLineWidget +}; + }); diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 2a0148acbe8..3fb98cc9697 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -858,6 +858,7 @@ var StatementModel = BasicModel.extend({ 'tax_id': [tax.id, null], 'amount': tax.amount, 'label': tax.name, + 'date': prop.date, 'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id, 'analytic': tax.analytic, 'is_tax': true, @@ -1072,6 +1073,7 @@ var StatementModel = BasicModel.extend({ */ _formatQuickCreate: function (line, values) { values = values || {}; + var today = new moment().utc().format(); var account = this._formatNameGet(values.account_id); var formatOptions = { currency_id: line.st_line.currency_id, @@ -1088,6 +1090,7 @@ var StatementModel = BasicModel.extend({ 'tax_id': this._formatNameGet(values.tax_id), 'debit': 0, 'credit': 0, + 'date': values.date ? values.date : field_utils.parse.date(today, {}, {isUTC: true}), 'base_amount': values.amount_type !== "percentage" ? (amount) : line.balance.amount * values.amount / 100, 'percent': values.amount_type === "percentage" ? values.amount : null, @@ -1239,7 +1242,7 @@ var StatementModel = BasicModel.extend({ * datas allowing manual reconciliation */ var ManualModel = StatementModel.extend({ - quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids'], + quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids', 'date'], //-------------------------------------------------------------------------- // Public @@ -1637,6 +1640,12 @@ var ManualModel = StatementModel.extend({ }) .then(this._formatMoveLine.bind(this, handle)); }, + + _formatToProcessReconciliation: function (line, prop) { + var result = this._super(line, prop); + result['date'] = prop.date; + return result; + }, }); return { diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index f0641eae409..23c680ddcda 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -542,10 +542,13 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { }, { type: 'float', name: 'amount', + }, { + type: 'char', //TODO is it a bug or a feature when type date exists ? + name: 'date', }], { account_id: {string: _t("Account")}, label: {string: _t("Label")}, - amount: {string: _t("Account")} + amount: {string: _t("Account")}, }).then(function (recordID) { self.handleCreateRecord = recordID; var record = self.model.get(self.handleCreateRecord); @@ -573,6 +576,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { self.fields.amount = new basic_fields.FieldFloat(self, 'amount', record, {mode: 'edit'}); + + self.fields.date = new basic_fields.FieldDate(self, + 'date', record, {mode: 'edit'}); var $create = $(qweb.render("reconciliation.line.create", {'state': state})); self.fields.account_id.appendTo($create.find('.create_account_id .o_td_field')) @@ -586,6 +592,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { .then(addRequiredStyle.bind(self, self.fields.label)); self.fields.amount.appendTo($create.find('.create_amount .o_td_field')) .then(addRequiredStyle.bind(self, self.fields.amount)); + self.fields.date.appendTo($create.find('.create_date .o_td_field')) self.$('.create').append($create); function addRequiredStyle(widget) { @@ -893,6 +900,7 @@ var ManualLineRenderer = LineRenderer.extend({ _renderCreate: function (state) { this._super(state); this.$('.create .create_journal_id').show(); + this.$('.create .create_date').removeClass('d-none') this.$('.create .create_journal_id .o_input').addClass('o_required_modifier'); }, diff --git a/addons/account/static/src/xml/account_payment.xml b/addons/account/static/src/xml/account_payment.xml index 4e72bd67d06..55b327183fc 100644 --- a/addons/account/static/src/xml/account_payment.xml +++ b/addons/account/static/src/xml/account_payment.xml @@ -16,8 +16,8 @@ Add - - + +
diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 3caa61eb633..33c778a9b7c 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -212,6 +212,10 @@ Tax Included in Price + + Writeoff Date + +
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 01645d5c97e..62fa37e1199 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -649,8 +649,8 @@ QUnit.module('account', { assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait"); assert.strictEqual(clientAction.widgets[2].$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp"); widget.$('.accounting_view tfoot td:first').trigger('click'); - assert.containsN(widget, '.create input.o_input', 7, - "create panel should contain 7 fields (account_id, tax_id, journal_id, analytic_account_id, analytic_tag_ids, label, amount)"); + assert.containsN(widget, '.create input.o_input', 8, + "create panel should contain 8 fields (account_id, tax_id, journal_id, analytic_account_id, analytic_tag_ids, label, amount, date)"); assert.containsN(widget, '.create .create_account_id .o_required_modifier, .create .create_label .o_required_modifier, .create .create_amount .o_required_modifier', 3, "account_id, label and amount should be required fields"); assert.strictEqual(widget.$('.create .create_label input').val(), 'SAJ/2014/002 and SAJ/2014/003', diff --git a/addons/account/tests/test_account_customer_invoice.py b/addons/account/tests/test_account_customer_invoice.py index 0906eae595b..03ca61aecf5 100644 --- a/addons/account/tests/test_account_customer_invoice.py +++ b/addons/account/tests/test_account_customer_invoice.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from odoo.addons.account.tests.account_test_users import AccountTestUsers import datetime from odoo.tests import tagged @@ -165,3 +167,174 @@ class TestAccountCustomerInvoice(AccountTestUsers): )) self.assertEquals(invoice.amount_untaxed, sum([x.base for x in invoice.tax_line_ids])) + + def test_customer_invoice_tax_refund(self): + company = self.env.user.company_id + tax_account = self.env['account.account'].create({ + 'name': 'TAX', + 'code': 'TAX', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + 'company_id': company.id, + }) + + tax_refund_account = self.env['account.account'].create({ + 'name': 'TAX_REFUND', + 'code': 'TAX_R', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + 'company_id': company.id, + }) + + journalrec = self.env['account.journal'].search([('type', '=', 'sale')])[0] + partner3 = self.env.ref('base.res_partner_3') + account_id = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id + + tax = self.env['account.tax'].create({ + 'name': 'Tax 15.0', + 'amount': 15.0, + 'amount_type': 'percent', + 'type_tax_use': 'sale', + 'account_id': tax_account.id, + 'refund_account_id': tax_refund_account.id + }) + + invoice_line_data = [ + (0, 0, + { + 'product_id': self.env.ref('product.product_product_1').id, + 'quantity': 40.0, + 'account_id': account_id, + 'name': 'product test 1', + 'discount': 10.00, + 'price_unit': 2.27, + 'invoice_line_tax_ids': [(6, 0, [tax.id])], + } + )] + + invoice = self.env['account.invoice'].create(dict( + name="Test Customer Invoice", + journal_id=journalrec.id, + partner_id=partner3.id, + invoice_line_ids=invoice_line_data + )) + + invoice.action_invoice_open() + + refund = invoice.refund() + self.assertEqual(invoice.tax_line_ids.mapped('account_id'), tax_account) + self.assertEqual(refund.tax_line_ids.mapped('account_id'), tax_refund_account) + + def test_customer_invoice_dashboard(self): + def patched_today(*args, **kwargs): + return '2019-01-22' + + date_invoice = '2019-01-21' + partner3 = self.env.ref('base.res_partner_3') + account_id = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id + + journal = self.env['account.journal'].create({ + 'name': 'sale_0', + 'code': 'SALE0', + 'type': 'sale', + }) + + invoice_line_data = [ + (0, 0, + { + 'product_id': self.env.ref('product.product_product_1').id, + 'quantity': 40.0, + 'account_id': account_id, + 'name': 'product test 1', + 'discount': 10.00, + 'price_unit': 2.27, + } + ) + ] + + invoice = self.env['account.invoice'].create(dict( + name="Test Customer Invoice", + journal_id=journal.id, + partner_id=partner3.id, + invoice_line_ids=invoice_line_data, + date_invoice=date_invoice, + )) + + refund_line_data = [ + (0, 0, + { + 'product_id': self.env.ref('product.product_product_1').id, + 'quantity': 1.0, + 'account_id': account_id, + 'name': 'product test 1', + 'price_unit': 13.3, + } + )] + + refund = self.env['account.invoice'].create(dict( + name="Test Customer Refund", + type='out_refund', + journal_id=journal.id, + partner_id=partner3.id, + invoice_line_ids=refund_line_data, + date_invoice=date_invoice, + )) + + # Check Draft + dashboard_data = journal.get_journal_dashboard_datas() + + self.assertEquals(dashboard_data['number_draft'], 2) + self.assertEquals(dashboard_data['sum_draft'], '$ 68.42') + + self.assertEquals(dashboard_data['number_waiting'], 0) + self.assertEquals(dashboard_data['sum_waiting'], '$ 0.00') + + # Check Both + invoice.action_invoice_open() + + dashboard_data = journal.get_journal_dashboard_datas() + self.assertEquals(dashboard_data['number_draft'], 1) + self.assertEquals(dashboard_data['sum_draft'], '$ -13.30') + + self.assertEquals(dashboard_data['number_waiting'], 1) + self.assertEquals(dashboard_data['sum_waiting'], '$ 81.72') + + # Check waiting payment + refund.action_invoice_open() + + dashboard_data = journal.get_journal_dashboard_datas() + self.assertEquals(dashboard_data['number_draft'], 0) + self.assertEquals(dashboard_data['sum_draft'], '$ 0.00') + + self.assertEquals(dashboard_data['number_waiting'], 2) + self.assertEquals(dashboard_data['sum_waiting'], '$ 68.42') + + # Check partial + receivable_account = refund.move_id.line_ids.mapped('account_id').filtered(lambda a: a.internal_type == 'receivable') + payment_move = self.env['account.move'].create({ + 'journal_id': journal.id, + }) + payment_move_line = self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'move_id': payment_move.id, + 'account_id': receivable_account.id, + 'debit': 10.00, + }) + self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'move_id': payment_move.id, + 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_liquidity').id)], limit=1).id, + 'credit': 10.00, + }) + + payment_move.post() + + refund.register_payment(payment_move_line) + + dashboard_data = journal.get_journal_dashboard_datas() + self.assertEquals(dashboard_data['number_draft'], 0) + self.assertEquals(dashboard_data['sum_draft'], '$ 0.00') + + self.assertEquals(dashboard_data['number_waiting'], 2) + self.assertEquals(dashboard_data['sum_waiting'], '$ 78.42') + + with patch('odoo.fields.Date.today', patched_today): + dashboard_data = journal.get_journal_dashboard_datas() + self.assertEquals(dashboard_data['number_late'], 2) + self.assertEquals(dashboard_data['sum_late'], '$ 78.42') diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index a061f0c7964..c010d0084aa 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -595,3 +595,37 @@ class TestPayment(AccountingTestCase): # The invoice should now be paid self.assertEqual(invoice.state, 'paid', "Invoice should be in 'paid' state after having reconciled the two payments with a bank statement") + + def test_register_payments_multi_invoices(self): + inv1 = self.create_invoice(amount=40) + inv2 = inv1.copy() + inv2.action_invoice_open() + + batch_payment = self.env['account.register.payments'].with_context(active_ids=(inv1 + inv2).ids).create({ + 'amount': 70, + 'partner_id': inv1.partner_id.id, + 'journal_id': self.bank_journal_usd.id, + 'invoice_ids': [(6, False, (inv1 + inv2).ids)], + 'partner_type': 'customer', + 'payment_difference_handling': 'reconcile', + 'payment_type': 'inbound', + 'payment_method_id': self.payment_method_manual_in.id, + 'writeoff_account_id': self.account_revenue.id, + 'writeoff_label': "why can't we live together"}) + + payment_id = batch_payment.create_payments()['res_id'] + payment_id = self.env['account.payment'].browse(payment_id) + + self.assertEqual(inv1.state, 'paid') + self.assertEqual(inv2.state, 'paid') + + receivable_inv1 = inv1.move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable') + receivable_inv2 = inv2.move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable') + receivable_payment = payment_id.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable') + + self.assertEqual( + receivable_inv1.full_reconcile_id.reconciled_line_ids, + receivable_inv1 + receivable_inv2 + receivable_payment + ) + write_off_line = payment_id.move_line_ids.filtered(lambda l: l.account_id == payment_id.writeoff_account_id) + self.assertEqual(write_off_line.debit, 10) diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 99a9e6c882e..52904915083 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -1569,10 +1569,11 @@ account.move.form account.move -
+
diff --git a/addons/account/wizard/account_invoice_send.py b/addons/account/wizard/account_invoice_send.py index ba5da81b889..54fbd7ff4be 100644 --- a/addons/account/wizard/account_invoice_send.py +++ b/addons/account/wizard/account_invoice_send.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models -from odoo.exceptions import UserError +from odoo import api, fields, models, _ +from odoo.addons.mail.wizard.mail_compose_message import _reopen class AccountInvoiceSend(models.TransientModel): @@ -67,3 +67,11 @@ class AccountInvoiceSend(models.TransientModel): if self.is_print: return self._print_document() return {'type': 'ir.actions.act_window_close'} + + @api.multi + def save_as_template(self): + self.ensure_one() + self.composer_id.save_as_template() + action = _reopen(self, self.id, self.model, context=self._context) + action.update({'name': _('Send Invoice')}) + return action diff --git a/addons/account_bank_statement_import/i18n/es.po b/addons/account_bank_statement_import/i18n/es.po index 4db267d439c..793b92740d1 100644 --- a/addons/account_bank_statement_import/i18n/es.po +++ b/addons/account_bank_statement_import/i18n/es.po @@ -23,6 +23,7 @@ # Nicolás Broggi , 2018 # Jesús Alan Ramos Rodríguez , 2018 # Pedro M. Baeza , 2019 +# Gabriel Umaña , 2019 # msgid "" msgstr "" @@ -30,7 +31,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-21 13:17+0000\n" "PO-Revision-Date: 2016-08-05 12:55+0000\n" -"Last-Translator: Pedro M. Baeza , 2019\n" +"Last-Translator: Gabriel Umaña , 2019\n" "Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -305,12 +306,12 @@ msgstr "Nombre de archivo" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import_journal_creation__inbound_payment_method_ids msgid "For Incoming Payments" -msgstr "" +msgstr "Para pagos recibidos" #. module: account_bank_statement_import #: model:ir.model.fields,field_description:account_bank_statement_import.field_account_bank_statement_import_journal_creation__outbound_payment_method_ids msgid "For Outgoing Payments" -msgstr "" +msgstr "Para pagos salientes" #. module: account_bank_statement_import #: model:ir.model.fields,help:account_bank_statement_import.field_account_bank_statement_import__data_file diff --git a/addons/account_voucher/i18n/es.po b/addons/account_voucher/i18n/es.po index 5caba8393e3..c94151ae53d 100644 --- a/addons/account_voucher/i18n/es.po +++ b/addons/account_voucher/i18n/es.po @@ -3,16 +3,34 @@ # * account_voucher # # Translators: -# Martin Trigaux, 2018 -# Cristopher Cravioto , 2018 +# Leonardo Chianea , 2016 +# David Perez , 2016 +# Luis M. Triana , 2016 +# Carles Antoli , 2016 +# Antonio Trueba, 2016 +# Ivan Nieto , 2016 +# Mateo Tibaquirá Palacios , 2016 +# Luis M. Ontalba , 2016 +# Christopher Ormaza , 2017 +# Pedro M. Baeza , 2018 +# Ana Juaristi , 2018 +# oihane , 2018 +# Javier Ramirez , 2018 +# José Vicente , 2018 +# RGB Consulting , 2018 +# Nicolás Broggi , 2018 # Miquel Torner , 2018 +# Martin Trigaux, 2019 +# Cristopher Cravioto , 2019 +# Luis Marin , 2019 +# msgid "" msgstr "" "Project-Id-Version: Odoo Server saas~11.5\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-21 13:17+0000\n" -"PO-Revision-Date: 2018-09-21 13:17+0000\n" -"Last-Translator: Miquel Torner , 2018\n" +"PO-Revision-Date: 2016-08-05 12:55+0000\n" +"Last-Translator: Luis Marin , 2019\n" "Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -692,4 +710,4 @@ msgstr "" #: code:addons/account_voucher/models/account_voucher.py:457 #, python-format msgid "You must first select a partner." -msgstr "" +msgstr "Primero debe seleccionar un Contacto" diff --git a/addons/account_voucher/i18n/hu.po b/addons/account_voucher/i18n/hu.po index fdc798a76af..39fcda07cba 100644 --- a/addons/account_voucher/i18n/hu.po +++ b/addons/account_voucher/i18n/hu.po @@ -325,7 +325,7 @@ msgstr "Utoljára frissítve " #. module: account_voucher #: model:ir.model.fields,field_description:account_voucher.field_account_voucher__message_main_attachment_id msgid "Main Attachment" -msgstr "" +msgstr "Fő melléklet" #. module: account_voucher #: model:ir.model.fields,field_description:account_voucher.field_account_voucher__message_has_error diff --git a/addons/analytic/i18n/hu.po b/addons/analytic/i18n/hu.po index beeb5f5e2b5..922b04b7734 100644 --- a/addons/analytic/i18n/hu.po +++ b/addons/analytic/i18n/hu.po @@ -387,7 +387,7 @@ msgstr "Utoljára frissítve " #. module: analytic #: model:ir.model.fields,field_description:analytic.field_account_analytic_account__message_main_attachment_id msgid "Main Attachment" -msgstr "" +msgstr "Fő melléklet" #. module: analytic #: model:ir.model.fields,field_description:analytic.field_account_analytic_account__message_has_error diff --git a/addons/attachment_indexation/models/ir_attachment.py b/addons/attachment_indexation/models/ir_attachment.py index 2bc6aee234d..bdc4559e893 100644 --- a/addons/attachment_indexation/models/ir_attachment.py +++ b/addons/attachment_indexation/models/ir_attachment.py @@ -9,7 +9,7 @@ import zipfile from odoo import api, models _logger = logging.getLogger(__name__) -FTYPES = ['docx', 'pptx', 'xlsx', 'opendoc', 'pdf'] +FTYPES = ['docx', 'pptx', 'xlsx', 'opendoc'] def textToString(element): buff = u"" @@ -92,6 +92,9 @@ class IrAttachment(models.Model): def _index_pdf(self, bin_data): '''Index PDF documents''' + # extractText gives very bad results for indexing, hence we don't index PDF anymore. A + # better alternative is probably PDFMiner.six, but not for stable. + # See POC at https://github.com/odoo/odoo/pull/27568. buf = u"" if bin_data.startswith(b'%PDF-'): f = io.BytesIO(bin_data) diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py index 7591299f3e2..f8f01119e7d 100644 --- a/addons/base_address_city/models/res_partner.py +++ b/addons/base_address_city/models/res_partner.py @@ -15,9 +15,10 @@ class Partner(models.Model): @api.onchange('city_id') def _onchange_city_id(self): - self.city = self.city_id.name - self.zip = self.city_id.zipcode - self.state_id = self.city_id.state_id + if self.city_id: + self.city = self.city_id.name + self.zip = self.city_id.zipcode + self.state_id = self.city_id.state_id @api.model def _fields_view_get_address(self, arch): @@ -37,7 +38,10 @@ class Partner(models.Model): }" /> interval %s THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away' + CASE WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away' ELSE 'online' END as status FROM bus_presence B JOIN res_users U ON B.user_id = U.id WHERE U.partner_id IN %s AND U.active = 't' + GROUP BY U.partner_id """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids))) res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall())) for partner in self: diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index c67993ea072..079e4bd301a 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -898,6 +898,7 @@ for rec in records: name="Team Pipelines" parent="crm_menu_sales" action="sales_team.crm_team_salesteams_pipelines_act" + groups="sales_team.group_sale_manager" sequence="4"/> self.delivery_packaging_id.max_weight: diff --git a/addons/delivery/wizard/choose_delivery_package_views.xml b/addons/delivery/wizard/choose_delivery_package_views.xml index c37231ed38d..72e5074def3 100644 --- a/addons/delivery/wizard/choose_delivery_package_views.xml +++ b/addons/delivery/wizard/choose_delivery_package_views.xml @@ -11,7 +11,7 @@
&nbsp;
-
-
+
+
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 076dffd61b7..c7c0d1c85b0 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -538,6 +538,16 @@ class Message(models.Model): failures_infos = [] # for each channel, build the information header and include the logged partner information for message in self: + # Check if user has access to the record before displaying a notification about it. + # In case the user switches from one company to another, it might happen that he doesn't + # have access to the record related to the notification. In this case, we skip it. + if message.model and message.res_id: + record = self.env[message.model].browse(message.res_id) + try: + record.check_access_rights('read') + record.check_access_rule('read') + except AccessError: + continue info = { 'message_id': message.id, 'record_name': message.record_name, diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 8e7ee7a8c14..f9d2395cda2 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1339,7 +1339,8 @@ class MailThread(models.AbstractModel): subtype = 'mail.mt_note' if message_dict.get('parent_id'): parent_message = self.env['mail.message'].sudo().browse(message_dict['parent_id']) - partner_ids = [(4, parent_message.author_id.id)] + if parent_message.author_id: + partner_ids = [(4, parent_message.author_id.id)] else: subtype = 'mail.mt_comment' @@ -2066,7 +2067,7 @@ class MailThread(models.AbstractModel): prioritary_attachments = all_attachments.filtered(lambda x: x.mimetype.endswith('pdf')) \ or all_attachments.filtered(lambda x: x.mimetype.startswith('image')) \ or all_attachments - self.write({'message_main_attachment_id': prioritary_attachments[0].id}) + self.sudo().write({'message_main_attachment_id': prioritary_attachments[0].id}) # Notify recipients of the newly-created message (Inbox / Email + channels) if msg_vals.get('moderation_status') != 'pending_moderation': message._notify( diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index 9543a7ebff2..a0cb23b940d 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -333,7 +333,7 @@ var BasicActivity = AbstractField.extend({ var self = this; var $markDoneBtn = $(ev.currentTarget); var activityID = $markDoneBtn.data('activity-id'); - var previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id'); + var previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id') || false; var forceNextActivity = $markDoneBtn.data('force-next-activity'); if ($markDoneBtn.data('toggle') == 'collapse') { diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index da23521f65f..330e56b77b4 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -300,6 +300,7 @@ var Chatter = Widget.extend({ recordName: this.recordName, defaultBody: oldComposer && oldComposer.$input && oldComposer.$input.val(), defaultMentionSelections: oldComposer && oldComposer.getMentionListenerSelections(), + attachmentIds: (oldComposer && oldComposer.get('attachment_ids')) || [], }); this._composer.on('input_focused', this, function () { this._composer.mentionSetPrefetchedPartners(this._mentionSuggestions || []); @@ -309,9 +310,7 @@ var Chatter = Widget.extend({ if (oldComposer) { oldComposer.destroy(); } - if (!config.device.isMobile) { - self._composer.focus(); - } + self._composer.focus(); self._composer.on('post_message', self, function (messageData) { self._discardOnReload(messageData).then(function () { self._disableComposer(); @@ -321,7 +320,7 @@ var Chatter = Widget.extend({ self.trigger_up('reload'); } else if (messageData.attachment_ids.length) { self._reloadAttachmentBox(); - self.trigger_up('reload', {fieldNames: ['message_attachment_count']}); + self.trigger_up('reload', {fieldNames: ['message_attachment_count'], keepChanges: true}); } }).fail(function () { self._enableComposer(); diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js index b88d7f6b0d9..8a045ac2d82 100644 --- a/addons/mail/static/src/js/composers/basic_composer.js +++ b/addons/mail/static/src/js/composers/basic_composer.js @@ -26,7 +26,7 @@ var BasicComposer = Widget.extend({ 'click .o_composer_button_add_attachment': '_onClickAddAttachment', 'click .o_composer_button_emoji': '_onEmojiButtonClick', 'focusout .o_composer_button_emoji': '_onEmojiButtonFocusout', - 'click .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageClick', + 'mousedown .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageClick', 'focus .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageFocus', 'dragover .o_file_drop_zone_container': '_onFileDragover', 'drop .o_file_drop_zone_container': '_onFileDrop', @@ -58,7 +58,7 @@ var BasicComposer = Widget.extend({ // Attachments this._attachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context); this.fileuploadID = _.uniqueId('o_chat_fileupload'); - this.set('attachment_ids', []); + this.set('attachment_ids', options.attachmentIds || []); // Mention this._mentionManager = new MentionManager(this); @@ -120,6 +120,7 @@ var BasicComposer = Widget.extend({ }); // Attachments + this._renderAttachments(); $(window).on(this.fileuploadID, this._onAttachmentLoaded.bind(this)); this.on('change:attachment_ids', this, this._renderAttachments); diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js index c8e05957afb..4971e7f3f4b 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -101,31 +101,37 @@ MailManager.include({ // valid threadID, therefore no check var thread = this.getThread(threadID); var threadWindow = this._getThreadWindow(threadID); + var def = $.when(); if (!threadWindow) { - threadWindow = this._makeNewThreadWindow(thread, options); - this._placeNewThreadWindow(threadWindow, options.passively); - - threadWindow.appendTo($(this.THREAD_WINDOW_APPENDTO)) - .then(function () { - self._repositionThreadWindows(); - return thread.fetchMessages(); - }).then(function () { - threadWindow.render(); - threadWindow.scrollToBottom(); - if ( - !self._areAllThreadWindowsHidden() && - !thread.isFolded() && - !threadWindow.isPassive() - ) { - thread.markAsRead(); - } - }); + def = thread.fetchMessages().then(function () { + threadWindow = self._makeNewThreadWindow(thread, options); + self._placeNewThreadWindow(threadWindow, options.passively); + return threadWindow.appendTo($(self.THREAD_WINDOW_APPENDTO)); + }).then(function () { + self._repositionThreadWindows(); + threadWindow.render(); + threadWindow.scrollToBottom(); + if ( + !self._areAllThreadWindowsHidden() && + !thread.isFolded() && + !threadWindow.isPassive() + ) { + thread.markAsRead(); + } + }).fail(function () { + // thread window could not be open, which may happen due to + // access error while fetching messages to the document. + // abort opening the thread window in this case. + thread.close(); + }); } else if (!options.passively) { if (threadWindow.isHidden()) { this._makeThreadWindowVisible(threadWindow); } } - threadWindow.updateVisualFoldState(); + def.then(function () { + threadWindow.updateVisualFoldState(); + }); }, /** * Called when a thread has its window state that has been changed, so its diff --git a/addons/mail/static/src/js/systray/systray_activity_menu.js b/addons/mail/static/src/js/systray/systray_activity_menu.js index c825844851e..593cac91506 100644 --- a/addons/mail/static/src/js/systray/systray_activity_menu.js +++ b/addons/mail/static/src/js/systray/systray_activity_menu.js @@ -46,7 +46,7 @@ var ActivityMenu = Widget.extend({ kwargs: {context: session.user_context}, }).then(function (data) { self._activities = data; - self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0); + self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count || 0; }, 0); self.$('.o_notification_counter').text(self.activityCounter); self.$el.toggleClass('o_no_notification', !self.activityCounter); }); @@ -118,7 +118,8 @@ var ActivityMenu = Widget.extend({ name: targetAction.data('model_name'), views: [[false, 'activity'], [false, 'kanban'], [false, 'list']], view_mode: 'activity', - res_model: targetAction.data('res_model') + res_model: targetAction.data('res_model'), + context: { search_default_activities_my: true }, }); } }, diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js index 57907a55943..331228c91c8 100644 --- a/addons/mail/static/src/js/thread_windows/thread_window.js +++ b/addons/mail/static/src/js/thread_windows/thread_window.js @@ -280,8 +280,10 @@ var ThreadWindow = AbstractThreadWindow.extend({ clear_breadcrumbs: false, active_id: this.hasThread() ? this._getThreadID() : undefined, on_reverse_breadcrumb: function () { - self.call('mail_service', 'getMailBus') - .trigger('discuss_open', false); + var mailBus = self.call('mail_service', 'getMailBus'); + if (mailBus) { + mailBus.trigger('discuss_open', false); + } }, }); } diff --git a/addons/mail/static/src/scss/followers.scss b/addons/mail/static/src/scss/followers.scss index b2bbc77f7f2..934df9b7acc 100644 --- a/addons/mail/static/src/scss/followers.scss +++ b/addons/mail/static/src/scss/followers.scss @@ -84,6 +84,8 @@ .o_followers_list { font-size: small; + max-height: 50vh; // ancestor has size 33px + overflow: auto; .o_partner { position: relative; // required for old design compatibility diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js index 2feb23a3c8b..5f903898255 100644 --- a/addons/mail/static/tests/discuss_tests.js +++ b/addons/mail/static/tests/discuss_tests.js @@ -1366,13 +1366,13 @@ QUnit.test('select emoji replaces cursor position', function (assert) { $composer[0].setSelectionRange(2, 4); testUtils.dom.click(discuss.$('.o_composer_button_emoji')); - testUtils.dom.click(discuss.$('.o_mail_emoji[data-emoji=":)"]')); + testUtils.dom.triggerMouseEvent(discuss.$('.o_mail_emoji[data-emoji=":)"]'), 'mousedown'); assert.strictEqual($composer.val(), "ab :) efgh", "should have inserted emoji "); testUtils.dom.click(discuss.$('.o_composer_button_emoji')); - testUtils.dom.click(discuss.$('.o_mail_emoji[data-emoji=":)"]')); + testUtils.dom.triggerMouseEvent(discuss.$('.o_mail_emoji[data-emoji=":)"]'), 'mousedown'); assert.strictEqual($composer.val(), "ab :) :) efgh", "should have inserted emoji after previously inserted emoji"); diff --git a/addons/mail/static/tests/document_thread_window_tests.js b/addons/mail/static/tests/document_thread_window_tests.js index 811b01f62e4..93095362306 100644 --- a/addons/mail/static/tests/document_thread_window_tests.js +++ b/addons/mail/static/tests/document_thread_window_tests.js @@ -271,6 +271,37 @@ QUnit.test('open, fold, unfold and close a document thread window', function (as messagingMenu.destroy(); }); +QUnit.test('do not open thread window on fetch message failure', function (assert) { + // this may happen when the user receives a notification from a document + // that he does not have access rights at the moment. + assert.expect(4); + + var messagingMenu = new MessagingMenu(); + testUtils.addMockEnvironment(messagingMenu, { + services: this.services, + data: this.data, + session: this.session, + mockRPC: function (route, args) { + if (args.method === 'read' && args.model === 'some.res.model' && args.args[0][0] === 1) { + assert.step('some.res.model/1/read'); + return $.Deferred().reject(); // simulate failure + } + return this._super.apply(this, arguments); + }, + }); + messagingMenu.appendTo($('#qunit-fixture')); + + testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + assert.containsOnce(messagingMenu, '.o_mail_preview'); + + testUtils.dom.click(messagingMenu.$('.o_mail_preview')); + assert.verifySteps(['some.res.model/1/read']); + assert.strictEqual($('.o_thread_window').length, 0, + "should not have open the DocumentThread in a thread window on fetch messages failure"); + + messagingMenu.destroy(); +}); + }); }); }); diff --git a/addons/mail/static/tests/systray/systray_activity_menu_tests.js b/addons/mail/static/tests/systray/systray_activity_menu_tests.js index d9977822916..abf4dc6daf7 100644 --- a/addons/mail/static/tests/systray/systray_activity_menu_tests.js +++ b/addons/mail/static/tests/systray/systray_activity_menu_tests.js @@ -151,7 +151,7 @@ QUnit.test('activity menu widget: activity menu with 3 records', function (asser }); QUnit.test('activity menu widget: activity view icon', function (assert) { - assert.expect(8); + assert.expect(10); var self = this; var activityMenu = new ActivityMenu(); testUtils.mock.addMockEnvironment(activityMenu, { @@ -177,9 +177,15 @@ QUnit.test('activity menu widget: activity view icon', function (assert) { "Second activity action should link to 'Note'"); assert.hasClass($second,'fa-clock-o', "should display the activity action icon"); - testUtils.mock.intercept(activityMenu, 'do_action', function (event) { - assert.step('do_action:' + - (event.data.action.name ? event.data.action.name : event.data.action)); + testUtils.mock.intercept(activityMenu, 'do_action', function (ev) { + if (ev.data.action.name) { + assert.ok(ev.data.action.context, "should define a context on the action"); + assert.ok(ev.data.action.context.search_default_activities_my, + "should auto-set search to 'My Activities' in the context of the action"); + assert.step('do_action:' + ev.data.action.name); + } else { + assert.step('do_action:' + ev.data.action); + } }, true); // click on the "Issue" activity icon diff --git a/addons/mass_mailing/models/__init__.py b/addons/mass_mailing/models/__init__.py index 6afa7a98d09..c542abccfba 100644 --- a/addons/mass_mailing/models/__init__.py +++ b/addons/mass_mailing/models/__init__.py @@ -4,6 +4,7 @@ from . import mass_mailing from . import mass_mailing_stats from . import mail_mail +from . import mail_template from . import mail_thread from . import res_config_settings from . import mass_mailing_report diff --git a/addons/mass_mailing/models/mail_template.py b/addons/mass_mailing/models/mail_template.py new file mode 100644 index 00000000000..68005ee0d10 --- /dev/null +++ b/addons/mass_mailing/models/mail_template.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class MailTemplate(models.Model): + _inherit = "mail.template" + + @api.model + def render_post_process(self, html): + # super will transform relative url to absolute + html = super(MailTemplate, self).render_post_process(html) + + # apply shortener after + if self.env.context.get('post_convert_links'): + html = self.env['link.tracker'].convert_links( + html, + self.env.context['post_convert_links'], + blacklist=['/unsubscribe_from_list'] + ) + return html diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index a5121e3d773..d660c6b624b 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -792,19 +792,47 @@ class MassMailing(models.Model): _logger.info("Mass-mailing %s targets %s, no opt out list available", self, target._name) return opt_out + def _get_convert_links(self): + self.ensure_one() + utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self + vals = {'mass_mailing_id': self.id} + + if self.mass_mailing_campaign_id: + vals['mass_mailing_campaign_id'] = self.mass_mailing_campaign_id.id + if utm_mixin.campaign_id: + vals['campaign_id'] = utm_mixin.campaign_id.id + if utm_mixin.source_id: + vals['source_id'] = utm_mixin.source_id.id + if utm_mixin.medium_id: + vals['medium_id'] = utm_mixin.medium_id.id + return vals + def _get_seen_list(self): """Returns a set of emails already targeted by current mailing/campaign (no duplicates)""" self.ensure_one() target = self.env[self.mailing_model_real] - mail_field = 'email' if 'email' in target._fields else 'email_from' - # avoid loading a large number of records in memory - # + use a basic heuristic for extracting emails - query = """ - SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) - FROM mail_mail_statistics s - JOIN %(target)s t ON (s.res_id = t.id) - WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL - """ + if set(['email', 'email_from']) & set(target._fields): + mail_field = 'email' if 'email' in target._fields else 'email_from' + # avoid loading a large number of records in memory + # + use a basic heuristic for extracting emails + query = """ + SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) + FROM mail_mail_statistics s + JOIN %(target)s t ON (s.res_id = t.id) + WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL + """ + elif 'partner_id' in target._fields: + mail_field = 'email' + query = """ + SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) + FROM mail_mail_statistics s + JOIN %(target)s t ON (s.res_id = t.id) + JOIN res_partner p ON (t.partner_id = p.id) + WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL + """ + else: + raise UserError(_("Unsupported mass mailing model %s") % self.mailing_model_id.name) + if self.mass_mailing_campaign_id.unique_ab_testing: query +=""" AND s.mass_mailing_campaign_id = %%(mailing_campaign_id)s; @@ -827,6 +855,7 @@ class MassMailing(models.Model): return { 'mass_mailing_opt_out_list': self._get_opt_out_list(), 'mass_mailing_seen_list': self._get_seen_list(), + 'post_convert_links': self._get_convert_links(), } def get_recipients(self): @@ -868,13 +897,10 @@ class MassMailing(models.Model): if not res_ids: raise UserError(_('There is no recipients selected.')) - # Convert links in absolute URLs before the application of the shortener - mailing.body_html = self.env['mail.thread']._replace_local_links(mailing.body_html) - composer_values = { 'author_id': author_id, 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], - 'body': mailing.convert_links()[mailing.id], + 'body': mailing.body_html, 'subject': mailing.subject, 'model': mailing.mailing_model_real, 'email_from': mailing.email_from, diff --git a/addons/mass_mailing/tests/__init__.py b/addons/mass_mailing/tests/__init__.py index b02648f85eb..36745d1525a 100644 --- a/addons/mass_mailing/tests/__init__.py +++ b/addons/mass_mailing/tests/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_mass_mailing_list_merge +from . import test_mass_mailing_shortener diff --git a/addons/mass_mailing/tests/test_mass_mailing_shortener.py b/addons/mass_mailing/tests/test_mass_mailing_shortener.py new file mode 100644 index 00000000000..456498f3c74 --- /dev/null +++ b/addons/mass_mailing/tests/test_mass_mailing_shortener.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import common +from lxml import etree + +try: + from unittest.mock import patch +except ImportError: + from mock import patch + + +class TestMassMailingShortener(common.TransactionCase): + def getHrefFor(self, html, id): + return html.xpath("*[@id='%s']" % id)[0].attrib.get('href') + + def shorturl_to_link(self, short_url): + return self.env['link.tracker.code'].search([('code', '=', short_url.split('/r/')[-1])]).link_id + + def setUp(self): + super(TestMassMailingShortener, self).setUp() + + def _get_title_from_url(u): + return "Hello" + + def _compute_favicon(): + # 1px to avoid real request + return 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==' + + patcher = patch('odoo.addons.link_tracker.models.link_tracker.LinkTracker._compute_favicon', wraps=_compute_favicon) + patcher2 = patch('odoo.addons.link_tracker.models.link_tracker.LinkTracker._get_title_from_url', wraps=_get_title_from_url) + patcher.start() + patcher2.start() + self.addCleanup(patcher.stop) + self.addCleanup(patcher2.stop) + + def test_00_test_mass_mailing_shortener(self): + mailing_list_A = self.env['mail.mass_mailing.list'].create({ + 'name': 'A', + }) + self.env['mail.mass_mailing.contact'].create({ + 'name': 'User 1', 'email': 'user1@example.com', 'list_ids': [(4, mailing_list_A.id)] + }) + self.env['mail.mass_mailing.contact'].create({ + 'name': 'User 2', 'email': 'user2@example.com', 'list_ids': [(4, mailing_list_A.id)] + }) + self.env['mail.mass_mailing.contact'].create({ + 'name': 'User 3', 'email': 'user3@example.com', 'list_ids': [(4, mailing_list_A.id)] + }) + + mass_mailing = self.env['mail.mass_mailing'].create({ + "reply_to_mode": "email", + "reply_to": "Administrator ", + "mailing_model_id": self.env.ref('mass_mailing.model_mail_mass_mailing_list').id, + "mailing_domain": "[('list_ids', 'in', [%d])]" % mailing_list_A.id, + "contact_list_ids": [[6, False, [mailing_list_A.id]]], + "mass_mailing_campaign_id": False, + "name": "sdf", + "body_html": """ +Hi, +% set url = "www.odoo.com" +% set httpurl = "https://www.odoo.eu" +Website0: https://www.odoo.tz/my/${object.name} +Website1: https://www.odoo.be +Website2: https://${url} +Website3: ${httpurl} +Email: test@odoo.com + """, + "schedule_date": False, + "state": "draft", + "keep_archives": True, + }) + + mass_mailing.put_in_queue() + mass_mailing._process_mass_mailing_queue() + + sent_mails = self.env['mail.mail'].search([('mailing_id', '=', mass_mailing.id)]) + sent_messages = sent_mails.mapped('mail_message_id') + + self.assertEqual(mailing_list_A.contact_nbr, len(sent_messages), + 'Some message has not been sent') + + xbody = etree.fromstring(sent_messages[0].body) + after_url0 = self.getHrefFor(xbody, 'url0') + after_url1 = self.getHrefFor(xbody, 'url1') + after_url2 = self.getHrefFor(xbody, 'url2') + after_url3 = self.getHrefFor(xbody, 'url3') + after_url4 = self.getHrefFor(xbody, 'url4') + + self.assertTrue('/r/' in after_url0, 'URL0 should be shortened: %s' % after_url0) + self.assertTrue('/r/' in after_url1, 'URL1 should be shortened: %s' % after_url1) + self.assertTrue('/r/' in after_url2, 'URL2 should be shortened: %s' % after_url2) + self.assertTrue('/r/' in after_url3, 'URL3 should be shortened: %s' % after_url3) + self.assertEqual(after_url4, "mailto:test@odoo.com", 'mailto: has been converted') + + short0 = self.shorturl_to_link(after_url0) + short1 = self.shorturl_to_link(after_url1) + short2 = self.shorturl_to_link(after_url2) + short3 = self.shorturl_to_link(after_url3) + + self.assertTrue("https://www.odoo.tz/my/User" in short0.url, 'URL mismatch') + self.assertEqual(short1.url, "https://www.odoo.be", 'URL mismatch') + self.assertEqual(short2.url, "https://www.odoo.com", 'URL mismatch') + self.assertEqual(short3.url, "https://www.odoo.eu", 'URL mismatch') + + _xbody = etree.fromstring(sent_messages[1].body) + _after_url0 = self.getHrefFor(_xbody, 'url0') + _after_url1 = self.getHrefFor(_xbody, 'url1') + _after_url2 = self.getHrefFor(_xbody, 'url2') + _after_url3 = self.getHrefFor(_xbody, 'url3') + _after_url4 = self.getHrefFor(_xbody, 'url4') + + self.assertTrue('/r/' in _after_url0, 'URL0 should be shortened: %s' % _after_url0) + self.assertTrue('/r/' in _after_url1, 'URL1 should be shortened: %s' % _after_url1) + self.assertTrue('/r/' in _after_url2, 'URL2 should be shortened: %s' % _after_url2) + self.assertTrue('/r/' in _after_url3, 'URL3 should be shortened: %s' % _after_url3) + self.assertEqual(_after_url4, "mailto:test@odoo.com", 'mailto: has been converted') + + _short0 = self.shorturl_to_link(_after_url0) + _short1 = self.shorturl_to_link(_after_url1) + _short2 = self.shorturl_to_link(_after_url2) + _short3 = self.shorturl_to_link(_after_url3) + + self.assertTrue("https://www.odoo.tz/my/User" in _short0.url, 'URL mismatch') + self.assertEqual(_short1.url, "https://www.odoo.be", 'URL mismatch') + self.assertEqual(_short2.url, "https://www.odoo.com", 'URL mismatch') + self.assertEqual(_short3.url, "https://www.odoo.eu", 'URL mismatch') + + self.assertNotEqual(short0.url, _short0.url) + self.assertEqual(short1.url, _short1.url) + self.assertEqual(short2.url, _short2.url) + self.assertEqual(short3.url, _short3.url) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index d8a957ba627..580b0e84d19 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -8,7 +8,7 @@ import math from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp -from odoo.exceptions import UserError +from odoo.exceptions import AccessError, UserError from odoo.tools import float_compare, float_round, DEFAULT_SERVER_DATETIME_FORMAT class MrpProduction(models.Model): @@ -33,6 +33,10 @@ class MrpProduction(models.Model): location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_src_id if not location: location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False) + try: + location.check_access_rule('read') + except (AttributeError, AccessError): + location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id return location and location.id or False @api.model @@ -42,6 +46,10 @@ class MrpProduction(models.Model): location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_dest_id if not location: location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False) + try: + location.check_access_rule('read') + except (AttributeError, AccessError): + location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id return location and location.id or False name = fields.Char( @@ -425,6 +433,10 @@ class MrpProduction(models.Model): @api.onchange('picking_type_id') def onchange_picking_type(self): location = self.env.ref('stock.stock_location_stock') + try: + location.check_access_rule('read') + except (AttributeError, AccessError): + location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id self.move_raw_ids.update({'picking_type_id': self.picking_type_id}) self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py index e33e0dfbea5..2bac0c1d0f7 100644 --- a/addons/mrp/models/mrp_unbuild.py +++ b/addons/mrp/models/mrp_unbuild.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ -from odoo.exceptions import UserError +from odoo.exceptions import AccessError, UserError from odoo.tools import float_compare @@ -13,10 +13,20 @@ class MrpUnbuild(models.Model): _order = 'id desc' def _get_default_location_id(self): - return self.env.ref('stock.stock_location_stock', raise_if_not_found=False) + stock_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False) + try: + stock_location.check_access_rule('read') + return stock_location.id + except (AttributeError, AccessError): + return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id.id def _get_default_location_dest_id(self): - return self.env.ref('stock.stock_location_stock', raise_if_not_found=False) + stock_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False) + try: + stock_location.check_access_rule('read') + return stock_location.id + except (AttributeError, AccessError): + return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id.id name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New')) product_id = fields.Many2one( diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index e773986f5b2..44a20ac0c69 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -45,8 +45,7 @@ class ProductTemplate(models.Model): action['domain'] = [('state', '=', 'done'), '&', ('product_tmpl_id', 'in', self.ids)] action['context'] = { 'graph_measure': 'product_uom_qty', - 'search_default_confirmed': 0, - 'time_ranges': {'field': 'date_finished', 'range': 'last_365_days'} + 'time_ranges': {'field': 'date_planned_start', 'range': 'last_365_days'} } return action @@ -133,8 +132,7 @@ class ProductProduct(models.Model): action = self.env.ref('mrp.mrp_production_report').read()[0] action['domain'] = [('state', '=', 'done'), '&', ('product_id', 'in', self.ids)] action['context'] = { - 'search_default_last_year_mo_order': 1, - 'search_default_status': 1, 'search_default_scheduled_month': 1, + 'time_ranges': {'field': 'date_planned_start', 'range': 'last_365_days'}, 'graph_measure': 'product_uom_qty', } return action diff --git a/addons/mrp/views/mrp_unbuild_views.xml b/addons/mrp/views/mrp_unbuild_views.xml index 58c1fb6014b..1a05dbc0b4a 100644 --- a/addons/mrp/views/mrp_unbuild_views.xml +++ b/addons/mrp/views/mrp_unbuild_views.xml @@ -128,8 +128,8 @@ - - + + diff --git a/addons/mrp/views/mrp_workcenter_views.xml b/addons/mrp/views/mrp_workcenter_views.xml index 8172349a27b..906cbc0070b 100644 --- a/addons/mrp/views/mrp_workcenter_views.xml +++ b/addons/mrp/views/mrp_workcenter_views.xml @@ -171,7 +171,7 @@
- diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py index 9cdcbec12af..6e3476099fd 100644 --- a/addons/pad/models/pad.py +++ b/addons/pad/models/pad.py @@ -18,6 +18,7 @@ _logger = logging.getLogger(__name__) class PadCommon(models.AbstractModel): _name = 'pad.common' + _description = 'Pad Common' @api.model def pad_is_configured(self): diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py index ba2a2551892..516eb8712a9 100644 --- a/addons/payment/controllers/portal.py +++ b/addons/payment/controllers/portal.py @@ -100,7 +100,10 @@ class PaymentProcessing(http.Controller): class WebsitePayment(http.Controller): @http.route(['/my/payment_method'], type='http', auth="user", website=True) def payment_method(self, **kwargs): - acquirers = list(request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False), ('payment_flow', '=', 's2s')])) + acquirers = list(request.env['payment.acquirer'].search([ + ('website_published', '=', True), ('registration_view_template_id', '!=', False), + ('payment_flow', '=', 's2s'), ('company_id', '=', request.env.user.company_id.id) + ])) partner = request.env.user.partner_id payment_tokens = partner.payment_token_ids payment_tokens |= partner.commercial_partner_id.sudo().payment_token_ids diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml index bac8783fcea..8328fac20c8 100644 --- a/addons/payment/views/payment_portal_templates.xml +++ b/addons/payment/views/payment_portal_templates.xml @@ -34,7 +34,7 @@ diff --git a/addons/pos_cache/models/pos_cache.py b/addons/pos_cache/models/pos_cache.py index 86dbfdfb39f..e7b779581d9 100644 --- a/addons/pos_cache/models/pos_cache.py +++ b/addons/pos_cache/models/pos_cache.py @@ -9,6 +9,7 @@ from odoo import models, fields, api class pos_cache(models.Model): _name = 'pos.cache' + _description = 'Point of Sale Cache' cache = fields.Binary(attachment=True) product_domain = fields.Text(required=True) diff --git a/addons/product/report/product_product_templates.xml b/addons/product/report/product_product_templates.xml index 2a612f556bc..f8213d924b0 100644 --- a/addons/product/report/product_product_templates.xml +++ b/addons/product/report/product_product_templates.xml @@ -1,75 +1,80 @@ - - - - - - - - - + + + + + + + + + + + + diff --git a/addons/rating/models/mail_message.py b/addons/rating/models/mail_message.py index f6b3553b7f4..aade3b9ade7 100644 --- a/addons/rating/models/mail_message.py +++ b/addons/rating/models/mail_message.py @@ -8,7 +8,7 @@ class MailMessage(models.Model): _inherit = 'mail.message' rating_ids = fields.One2many('rating.rating', 'message_id', string='Related ratings') - rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False) + rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False, search='_search_rating_value') @api.multi @api.depends('rating_ids', 'rating_ids.rating') @@ -17,3 +17,7 @@ class MailMessage(models.Model): mapping = dict((r.message_id.id, r.rating) for r in ratings) for message in self: message.rating_value = mapping.get(message.id, 0.0) + + @api.multi + def _search_rating_value(self, operator, operand): + return [('rating_ids.rating', operator, operand)] diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 55493728f13..6972025ae41 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -196,7 +196,8 @@ class ResourceCalendar(models.Model): hour_count = 0.0 for attendance in attendances: hour_count += attendance.hour_to - attendance.hour_from - self.hours_per_day = float_round(hour_count / float(len(set(attendances.mapped('dayofweek')))), precision_digits=2) + if attendances: + self.hours_per_day = float_round(hour_count / float(len(set(attendances.mapped('dayofweek')))), precision_digits=2) # -------------------------------------------------- # Computation API diff --git a/addons/sale/data/mail_data.xml b/addons/sale/data/mail_data.xml index 3bde9253038..938a39e1576 100644 --- a/addons/sale/data/mail_data.xml +++ b/addons/sale/data/mail_data.xml @@ -103,7 +103,7 @@
- ${(object.name or '').replace('/','_')}${object.state == 'draft' and '_draft' or ''} + ${(object.name or '').replace('/','_')} ${object.partner_id.lang} diff --git a/addons/sale/models/analytic.py b/addons/sale/models/analytic.py index 8fb938582a0..a60dd11b023 100644 --- a/addons/sale/models/analytic.py +++ b/addons/sale/models/analytic.py @@ -19,7 +19,7 @@ class AccountAnalyticLine(models.Model): @api.model def create(self, values): result = super(AccountAnalyticLine, self).create(values) - if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy != 'no' and result.amount <= 0: # allow to force a False value for so_line + if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy not in [False, 'no'] and result.amount <= 0: # allow to force a False value for so_line result.sudo()._sale_determine_order_line() return result @@ -28,7 +28,8 @@ class AccountAnalyticLine(models.Model): result = super(AccountAnalyticLine, self).write(values) if 'so_line' not in values: # allow to force a False value for so_line # only take the AAL from expense or vendor bill, meaning having a negative amount - self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no' and aal.amount <= 0)._sale_determine_order_line() + self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no'] and aal.amount <= 0)._sale_determine_order_line() + return result # ---------------------------------------------------------- # Vendor Bill / Expense : determine the Sale Order to reinvoice diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index f0d7799765e..f79d542589c 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -47,8 +47,8 @@ class SaleOrder(models.Model): amount_untaxed += line.price_subtotal amount_tax += line.price_tax order.update({ - 'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed), - 'amount_tax': order.pricelist_id.currency_id.round(amount_tax), + 'amount_untaxed': amount_untaxed, + 'amount_tax': amount_tax, 'amount_total': amount_untaxed + amount_tax, }) @@ -66,12 +66,14 @@ class SaleOrder(models.Model): for possible refunds created directly from existing invoices. This is necessary since such a refund is not directly linked to the SO. """ + # Ignore the status of the deposit product + deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id() + line_invoice_status_all = [(d['order_id'][0], d['invoice_status']) for d in self.env['sale.order.line'].read_group([('order_id', 'in', self.ids), ('product_id', '!=', deposit_product_id.id)], ['order_id', 'invoice_status'], ['order_id', 'invoice_status'], lazy=False)] for order in self: invoice_ids = order.order_line.mapped('invoice_lines.invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund']) - # Ignore the status of the deposit product - deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id() - line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id] + + line_invoice_status = [d[1] for d in line_invoice_status_all if d[0] == order.id] if order.state not in ('sale', 'done'): invoice_status = 'no' @@ -654,7 +656,7 @@ class SaleOrder(models.Model): # create an analytic account if at least an expense product for order in self: - if any([expense_policy != 'no' for expense_policy in order.order_line.mapped('product_id.expense_policy')]): + if any([expense_policy not in [False, 'no'] for expense_policy in order.order_line.mapped('product_id.expense_policy')]): if not order.analytic_account_id: order._create_analytic_account() @@ -1018,7 +1020,7 @@ class SaleOrderLine(models.Model): msg = _("Extra line with %s ") % (line.product_id.display_name,) line.order_id.message_post(body=msg) # create an analytic account if at least an expense product - if line.product_id.expense_policy != 'no' and not self.order_id.analytic_account_id: + if line.product_id.expense_policy not in [False, 'no'] and not self.order_id.analytic_account_id: self.order_id._create_analytic_account() return line diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index bbba828d5fd..3abaf475dcc 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -6,7 +6,6 @@ name="Sales" web_icon="sale,static/description/icon.png" active="False" - groups="sales_team.group_sale_salesman,sales_team.group_sale_salesman_all_leads,sales_team.group_sale_manager" sequence="7"/> @@ -314,6 +314,7 @@ +
@@ -322,6 +323,7 @@ name="product_uom_qty" class="oe_inline"/> + @@ -480,6 +484,7 @@ + diff --git a/addons/sale_expense/models/analytic.py b/addons/sale_expense/models/analytic.py index 9f037bfcae1..d387091adc1 100644 --- a/addons/sale_expense/models/analytic.py +++ b/addons/sale_expense/models/analytic.py @@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model): @api.multi def _sale_determine_order(self): mapping = super(AccountAnalyticLine, self)._sale_determine_order() - for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no'): + for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no']): if analytic_line.move_id.expense_id.sale_order_id: mapping[analytic_line.id] = analytic_line.move_id.expense_id.sale_order_id return mapping diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 9f7613ccf51..75e7953e903 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -51,7 +51,7 @@ class SaleOrder(models.Model): def _action_confirm(self): """ On SO confirmation, some lines should generate a task or a project. """ result = super(SaleOrder, self)._action_confirm() - self.mapped('order_line').sudo()._timesheet_service_generation() + self.mapped('order_line').sudo().with_context(default_company_id=self.company_id.id)._timesheet_service_generation() return result @api.multi diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 5ce579a5cf6..cb2ddefeca0 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1095,7 +1095,7 @@ class StockMove(models.Model): # this operation could trigger a merge of moves. for move in moves: if move.quantity_done <= 0: - if float_compare(move.product_uom_qty, 0.0, precision_rounding=move.product_uom.rounding) == 0: + if float_compare(move.product_uom_qty, 0.0, precision_rounding=move.product_uom.rounding) == 0 or cancel_backorder: move._action_cancel() # Create extra moves where necessary diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index d2c9ced957d..2ba9a35f313 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -146,7 +146,7 @@ class StockMoveLine(models.Model): """ res = {} if self.qty_done and self.product_id.tracking == 'serial': - if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0: + if float_compare(self.qty_done, 1.0, precision_rounding=self.product_id.uom_id.rounding) != 0: message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name res['warning'] = {'title': _('Warning'), 'message': message} return res diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py index 16f6b505acb..7c8eeefb57a 100644 --- a/addons/stock/models/stock_scrap.py +++ b/addons/stock/models/stock_scrap.py @@ -131,7 +131,8 @@ class StockScrap(models.Model): self.package_id, self.owner_id, strict=True).mapped('quantity')) - if float_compare(available_qty, self.scrap_qty, precision_digits=precision) >= 0: + scrap_qty = self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id) + if float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0: return self.do_scrap() else: return { diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 2e2d171eddf..8e9ebbd980c 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -971,7 +971,7 @@ class Orderpoint(models.Model): # These days will be substracted when creating the PO days += self.product_id._select_seller( quantity=product_qty, - date=start_date, + date=fields.Date.context_today(self,start_date), uom_id=self.product_uom).delay or 0.0 date_planned = start_date + relativedelta.relativedelta(days=days) return date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT) diff --git a/addons/stock/report/report_stockpicking_operations.xml b/addons/stock/report/report_stockpicking_operations.xml index 63bfbd8cc14..09715938ca3 100644 --- a/addons/stock/report/report_stockpicking_operations.xml +++ b/addons/stock/report/report_stockpicking_operations.xml @@ -155,7 +155,7 @@ -

+

All products could not be reserved. Click on the "Check Availability" button to try to reserve products

diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index 2037ec4f34e..0939389dda9 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -3569,6 +3569,24 @@ class StockMove(TransactionCase): self.assertEqual(scrap.state, 'done') self.assertEqual(move1.reserved_availability, 0.25) + def test_scrap_6(self): + """ Check that scrap correctly handle UoM. """ + self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 1) + scrap = self.env['stock.scrap'].create({ + 'product_id': self.product1.id, + 'product_uom_id': self.uom_dozen.id, + 'scrap_qty': 1, + }) + warning_message = scrap.action_validate() + self.assertEqual(warning_message.get('res_model', 'Wrong Model'), 'stock.warn.insufficient.qty.scrap') + insufficient_qty_wizard = self.env['stock.warn.insufficient.qty.scrap'].create({ + 'product_id': self.product1.id, + 'location_id': self.stock_location.id, + 'scrap_id': scrap.id + }) + insufficient_qty_wizard.action_done() + self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).quantity, -11) + def test_in_date_1(self): """ Check that moving a tracked quant keeps the incoming date. """ diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py index 71df404527e..13b092a9407 100644 --- a/addons/stock/tests/test_move2.py +++ b/addons/stock/tests/test_move2.py @@ -830,6 +830,55 @@ class TestSinglePicking(TestStockCommon): backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)]) self.assertEqual(backorder.state, 'confirmed') + def test_backorder_4(self): + """ Check the good behavior if no backorder are created + for a picking with a missing product. + """ + delivery_order = self.env['stock.picking'].create({ + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_out, + }) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': delivery_order.id, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + }) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productB.id, + 'product_uom_qty': 2, + 'product_uom': self.productB.uom_id.id, + 'picking_id': delivery_order.id, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + }) + + # Update available quantities for each products + pack_location = self.env['stock.location'].browse(self.pack_location) + self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2) + self.env['stock.quant']._update_available_quantity(self.productB, pack_location, 2) + + delivery_order.action_confirm() + delivery_order.action_assign() + self.assertEqual(delivery_order.state, 'assigned') + + # Process only one product without creating a backorder + delivery_order.move_lines[0].move_line_ids[0].qty_done = 2 + backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, delivery_order.id)]}) + backorder_wizard.process_cancel_backorder() + + # No backorder should be created and the move corresponding to the missing product should be cancelled + backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)]) + self.assertFalse(backorder) + self.assertEquals(delivery_order.state, 'done') + self.assertEquals(delivery_order.move_lines[1].state, 'cancel') + def test_extra_move_1(self): """ Check the good behavior of creating an extra move in a delivery order. This usecase simulates the delivery of 2 item while the initial stock move had to move 1 and there's diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index d919419a94c..2cf07320365 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -221,7 +221,7 @@