[MERGE] forward port branch saas-12.1 up to 4524ad06a8
This commit is contained in:
+64
-55
@@ -71,6 +71,8 @@
|
||||
# Nicolás Broggi <rnbroggi@gmail.com>, 2019
|
||||
# Luis González <lgonzalez@vauxoo.com>, 2019
|
||||
# Gabriel Umaña <gabriel.umana@delfixcr.com>, 2019
|
||||
# Luis Marin <marin.guadarrama@gmail.com>, 2019
|
||||
# Edilianny Sánchez <esanchez@vauxoo.com>, 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 <gabriel.umana@delfixcr.com>, 2019\n"
|
||||
"Last-Translator: Edilianny Sánchez <esanchez@vauxoo.com>, 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
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
# Martin Trigaux, 2019
|
||||
# Eloïse Stilmant <est@odoo.com>, 2019
|
||||
# Alain van de Werve <avw@openerp.com>, 2019
|
||||
# Khalyomede Khalyomede <khalyomede@gmail.com>, 2019
|
||||
# Fabien Bourgeois <fabien@yaltik.com>, 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 <avw@openerp.com>, 2019\n"
|
||||
"Last-Translator: Fabien Bourgeois <fabien@yaltik.com>, 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 "<span>Unit Price</span>"
|
||||
msgstr ""
|
||||
msgstr "<span>Prix Unitaire</span>"
|
||||
|
||||
#. 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
# Nils Fredrik Gjerull, 2018
|
||||
# Jorunn D. Newth, 2019
|
||||
# Jan Pedro Tumusok <jpt@eyenetworks.no>, 2019
|
||||
# Marius Stedjan <marius@stedjan.com>, 2019
|
||||
# Martin Trigaux, 2019
|
||||
# Marius Stedjan <marius@stedjan.com>, 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 <marius@stedjan.com>, 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
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
# Cas Vissers <casvissers@brahoo.nl>, 2019
|
||||
# Gunther Clauwaert <gclauwae@hotmail.com>, 2019
|
||||
# Martin Trigaux, 2019
|
||||
# Erwin van der Ploeg <erwin@odooexperts.nl>, 2019
|
||||
# Cas Vissers <c.vissers@brahoo.nl>, 2019
|
||||
# Jordy Blankestijn <jblankestijn@erpopen.nl>, 2019
|
||||
# Wynand Tastenhoye <wta@odoo.com>, 2019
|
||||
# dpms <pieter.van.de.wygaert@telenet.be>, 2019
|
||||
# Erik-Jan Bulthuis <ejbulthuis@erpopen.nl>, 2019
|
||||
# Erwin van der Ploeg <erwin@odooexperts.nl>, 2019
|
||||
# Yenthe Van Ginneken <yenthespam@gmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
@@ -252,12 +252,12 @@ msgstr "<em>Concept factuur</em>"
|
||||
#: 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 "<i class=\"fa fa-clock-o\" aria-label=\"Date\" role=\"img\" title=\"Date\"/>"
|
||||
msgstr ""
|
||||
msgstr "<i class=\"fa fa-clock-o\" aria-label=\"Date\" role=\"img\" title=\"Date\"/>"
|
||||
|
||||
#. module: account
|
||||
#: model_terms:ir.ui.view,arch_db:account.view_account_payment_kanban
|
||||
msgid "<i class=\"fa fa-clock-o\" role=\"img\" aria-label=\"Date\" title=\"Date\"/>"
|
||||
msgstr ""
|
||||
msgstr "<i class=\"fa fa-clock-o\" role=\"img\" aria-label=\"Datum\" title=\"Datum\"/>"
|
||||
|
||||
#. module: account
|
||||
#: model_terms:ir.ui.view,arch_db:account.portal_invoice_page
|
||||
@@ -270,6 +270,8 @@ msgid ""
|
||||
"<i class=\"fa fa-ellipsis-v\" aria-label=\"Selection\" role=\"img\" "
|
||||
"title=\"Selection\"/>"
|
||||
msgstr ""
|
||||
"<i class=\"fa fa-ellipsis-v\" aria-label=\"Selection\" role=\"img\" "
|
||||
"title=\"Selection\"/>"
|
||||
|
||||
#. module: account
|
||||
#: model_terms:ir.ui.view,arch_db:account.portal_invoice_page
|
||||
@@ -365,6 +367,8 @@ msgid ""
|
||||
"<span class=\"fa fa-lg fa-building-o\" title=\"Values set here are company-"
|
||||
"specific.\"/>"
|
||||
msgstr ""
|
||||
"<span class=\"fa fa-lg fa-building-o\" title=\"De waardes die u hier instelt"
|
||||
" zijn bedrijf specifiek.\"/>"
|
||||
|
||||
#. 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
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
# sharkutz <sharkutz4life@yahoo.com>, 2018
|
||||
# Iulian Cutui <iulian.cutui@gmail.com>, 2018
|
||||
# Bogdan Nistor <inactive+BogdanNistor.Ro@transifex.com>, 2018
|
||||
# Dorin Hongu <dhongu@gmail.com>, 2019
|
||||
# Sorin Trifu <sorin.trifu@bizconsulting.ro>, 2019
|
||||
# Cozmin Candea <office@terrabit.ro>, 2019
|
||||
# Martin Trigaux, 2019
|
||||
# Dorin Hongu <dhongu@gmail.com>, 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 <dhongu@gmail.com>, 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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -162,4 +162,8 @@ var ShowPaymentLineWidget = AbstractField.extend({
|
||||
|
||||
field_registry.add('payment', ShowPaymentLineWidget);
|
||||
|
||||
return {
|
||||
ShowPaymentLineWidget: ShowPaymentLineWidget
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<td>
|
||||
<a title="assign to invoice" role="button" class="oe_form_field btn btn-link outstanding_credit_assign" t-att-data-id="line.id" style="margin-right: 10px;" href="#">Add</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="oe_form_field" style="margin-right: 30px;"><t t-esc="line.journal_name"></t></span>
|
||||
<td style="max-width: 10em;">
|
||||
<div class="oe_form_field" style="margin-right: 30px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;" t-att-title="line.title"><t t-esc="line.journal_name"></t></div>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="!outstanding">
|
||||
|
||||
@@ -212,6 +212,10 @@
|
||||
<td class="o_td_label">Tax Included in Price</td>
|
||||
<td class="o_td_field"></td>
|
||||
</tr>
|
||||
<tr class="create_date d-none">
|
||||
<td class="o_td_label">Writeoff Date</td>
|
||||
<td class="o_td_field"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="add_line_container">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1569,10 +1569,11 @@
|
||||
<field name="name">account.move.form</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Account Entry">
|
||||
<form string="Account Entry" duplicate="false">
|
||||
<header>
|
||||
<button name="action_post" states="draft" string="Post" type="object" class="oe_highlight" groups="account.group_account_invoice"/>
|
||||
<button name="%(action_view_account_move_reversal)d" states="posted" string="Reverse Entry" type="action" groups="account.group_account_invoice"/>
|
||||
<button name="action_duplicate" string="Duplicate" type="object" groups="account.group_account_invoice"/>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
# Nicolás Broggi <rnbroggi@gmail.com>, 2018
|
||||
# Jesús Alan Ramos Rodríguez <alan.ramos@jarsa.com.mx>, 2018
|
||||
# Pedro M. Baeza <pedro.baeza@gmail.com>, 2019
|
||||
# Gabriel Umaña <gabriel.umana@delfixcr.com>, 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 <pedro.baeza@gmail.com>, 2019\n"
|
||||
"Last-Translator: Gabriel Umaña <gabriel.umana@delfixcr.com>, 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
|
||||
|
||||
@@ -3,16 +3,34 @@
|
||||
# * account_voucher
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2018
|
||||
# Cristopher Cravioto <ccr@odoo.com>, 2018
|
||||
# Leonardo Chianea <noamixcontenidos@gmail.com>, 2016
|
||||
# David Perez <david@closemarketing.es>, 2016
|
||||
# Luis M. Triana <luis.triana@jarsa.com.mx>, 2016
|
||||
# Carles Antoli <carlesantoli@hotmail.com>, 2016
|
||||
# Antonio Trueba, 2016
|
||||
# Ivan Nieto <ivan.n.s@tuta.io>, 2016
|
||||
# Mateo Tibaquirá Palacios <nestormateo@gmail.com>, 2016
|
||||
# Luis M. Ontalba <luis.martinez@tecnativa.com>, 2016
|
||||
# Christopher Ormaza <chris.ormaza@gmail.com>, 2017
|
||||
# Pedro M. Baeza <pedro.baeza@gmail.com>, 2018
|
||||
# Ana Juaristi <ajuaristio@gmail.com>, 2018
|
||||
# oihane <oihanecruce@gmail.com>, 2018
|
||||
# Javier Ramirez <elbomba887@gmail.com>, 2018
|
||||
# José Vicente <txusev@gmail.com>, 2018
|
||||
# RGB Consulting <odoo@rgbconsulting.com>, 2018
|
||||
# Nicolás Broggi <rnbroggi@gmail.com>, 2018
|
||||
# Miquel Torner <miq@odoo.com>, 2018
|
||||
# Martin Trigaux, 2019
|
||||
# Cristopher Cravioto <ccr@odoo.com>, 2019
|
||||
# Luis Marin <marin.guadarrama@gmail.com>, 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 <miq@odoo.com>, 2018\n"
|
||||
"PO-Revision-Date: 2016-08-05 12:55+0000\n"
|
||||
"Last-Translator: Luis Marin <marin.guadarrama@gmail.com>, 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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
}"
|
||||
/>
|
||||
<field name='city_id' placeholder="%(placeholder)s" string="%(placeholder)s"
|
||||
context="{'default_country_id': country_id}"
|
||||
context="{'default_country_id': country_id,
|
||||
'default_name': city,
|
||||
'default_zipcode': zip,
|
||||
'default_state_id': state_id}"
|
||||
domain="[('country_id', '=', country_id)]"
|
||||
attrs="{
|
||||
'invisible': [('country_enforce_cities', '=', False)],
|
||||
|
||||
@@ -24,6 +24,7 @@ class BaseGengoTranslations(models.TransientModel):
|
||||
GROUPS = ['base.group_system']
|
||||
|
||||
_name = 'base.gengo.translations'
|
||||
_description = 'Base Gengo Translations'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
|
||||
@@ -15,13 +15,14 @@ class ResPartner(models.Model):
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
U.partner_id as id,
|
||||
CASE WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > 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:
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
<menuitem id="res_partner_menu_customer"
|
||||
|
||||
@@ -70,7 +70,7 @@ class DeliveryCarrier(models.Model):
|
||||
def install_more_provider(self):
|
||||
return {
|
||||
'name': 'New Providers',
|
||||
'view_mode': 'kanban',
|
||||
'view_mode': 'kanban,form',
|
||||
'res_model': 'ir.module.module',
|
||||
'domain': [['name', 'ilike', 'delivery_']],
|
||||
'type': 'ir.actions.act_window',
|
||||
|
||||
@@ -21,6 +21,7 @@ class ChooseDeliveryPackage(models.TransientModel):
|
||||
string='Shipping Weight',
|
||||
default=lambda self: self._default_shipping_weight()
|
||||
)
|
||||
weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name')
|
||||
|
||||
def _default_stock_quant_package_id(self):
|
||||
if self.env.context.get('default_stock_quant_package_id'):
|
||||
@@ -45,6 +46,12 @@ class ChooseDeliveryPackage(models.TransientModel):
|
||||
total_weight = sum([po.qty_done * po.product_id.weight for po in move_line_ids])
|
||||
return total_weight
|
||||
|
||||
@api.depends('stock_quant_package_id', 'delivery_packaging_id')
|
||||
def _compute_weight_uom_name(self):
|
||||
weight_uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
|
||||
for package in self:
|
||||
package.weight_uom_name = weight_uom_id.name
|
||||
|
||||
@api.onchange('delivery_packaging_id', 'shipping_weight')
|
||||
def _onchange_packaging_weight(self):
|
||||
if self.delivery_packaging_id.max_weight and self.shipping_weight > self.delivery_packaging_id.max_weight:
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<label for="shipping_weight" attrs="{'invisible': [('delivery_packaging_id', '=', False)]}"/>
|
||||
<div class="o_row" attrs="{'invisible': [('delivery_packaging_id', '=', False)]}">
|
||||
<field name="shipping_weight"/>
|
||||
<span>kg</span>
|
||||
<field name="weight_uom_name"/>
|
||||
</div>
|
||||
</group>
|
||||
<footer>
|
||||
|
||||
@@ -23,7 +23,7 @@ class FleetVehicle(models.Model):
|
||||
license_plate = fields.Char(tracking=True,
|
||||
help='License plate number of the vehicle (i = plate number for a car)')
|
||||
vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False)
|
||||
driver_id = fields.Many2one('res.partner', 'Driver', tracking=True, help='Driver of the vehicle', copy=False)
|
||||
driver_id = fields.Many2one('res.partner', 'Driver', tracking=True, help='Driver of the vehicle', copy=False, auto_join=True)
|
||||
model_id = fields.Many2one('fleet.vehicle.model', 'Model',
|
||||
tracking=True, required=True, help='Model of the vehicle')
|
||||
brand_id = fields.Many2one('fleet.vehicle.model.brand', 'Brand', related="model_id.brand_id", store=True, readonly=False)
|
||||
@@ -233,12 +233,7 @@ class FleetVehicle(models.Model):
|
||||
@api.model
|
||||
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
|
||||
domain = args or []
|
||||
domain = expression.AND([domain, [('name', operator, name)]])
|
||||
# we don't want to override the domain's filter on driver_id if present
|
||||
if not any(['driver_id' in element for element in domain]):
|
||||
partner_ids = self.env['res.partner']._search([('name', operator, name)], access_rights_uid=name_get_uid)
|
||||
if partner_ids:
|
||||
domain = expression.OR([domain, ['|', ('driver_id', 'in', partner_ids), ('driver_id', '=', False)]])
|
||||
domain = expression.AND([domain, ['|', ('name', operator, name), ('driver_id.name', operator, name)]])
|
||||
rec = self._search(domain, limit=limit, access_rights_uid=name_get_uid)
|
||||
return self.browse(rec).name_get()
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * crm_project
|
||||
#
|
||||
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-08-24 09:17+0000\n"
|
||||
"Language-Team: Filipino (https://www.transifex.com/odoo/teams/41243/fil/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: fil\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n == 1 || n==2 || n==3) || (n % 10 != 4 || n % 10 != 6 || n % 10 != 9);\n"
|
||||
|
||||
#. module: crm_project
|
||||
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_view_form
|
||||
msgid ""
|
||||
"<span class=\"o_stat_text\">Convert To</span>\n"
|
||||
" <span class=\"o_stat_text\">Task</span>"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_convert2task_view_form
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.actions.act_window,name:crm_project.crm_lead_convert2task_action
|
||||
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_convert2task_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_view_form
|
||||
msgid "Convert to Task"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model_terms:ir.ui.view,arch_db:crm_project.crm_lead_convert2task_view_form
|
||||
msgid "Create Task"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: selection:crm.lead.convert2task,action:0
|
||||
msgid "Create a new customer"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__partner_id
|
||||
msgid "Customer"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: selection:crm.lead.convert2task,action:0
|
||||
msgid "Do not link to a customer"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__lead_id
|
||||
msgid "Lead"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model,name:crm_project.model_crm_lead_convert2task
|
||||
msgid "Lead convert to Task"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: selection:crm.lead.convert2task,action:0
|
||||
msgid "Link to an existing customer"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__project_id
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
#. module: crm_project
|
||||
#: model:ir.model.fields,field_description:crm_project.field_crm_lead_convert2task__action
|
||||
msgid "Related Customer"
|
||||
msgstr ""
|
||||
@@ -501,7 +501,8 @@ class HolidaysRequest(models.Model):
|
||||
holiday.sudo().action_validate()
|
||||
holiday.message_subscribe(partner_ids=[holiday._get_responsible_for_approval().partner_id.id])
|
||||
holiday.sudo().message_post(body=_("The time off has been automatically approved"), subtype="mt_comment") # Message from OdooBot (sudo)
|
||||
holiday.activity_update()
|
||||
if not self._context.get('import_file'):
|
||||
holiday.activity_update()
|
||||
return holiday
|
||||
|
||||
def _read_from_database(self, field_names, inherited_field_names=[]):
|
||||
|
||||
@@ -57,7 +57,7 @@ class FleetVehicle(models.Model):
|
||||
for car in self:
|
||||
car.co2_fee = self._get_co2_fee(car.co2, car.fuel_type)
|
||||
|
||||
@api.depends('fuel_type', 'car_value', 'acquisition_date')
|
||||
@api.depends('fuel_type', 'car_value', 'acquisition_date', 'co2')
|
||||
def _compute_car_atn(self):
|
||||
for car in self:
|
||||
car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2)
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
<record id="pcg_120" model="account.account.template">
|
||||
<field name="name">Résultat de l'exercice (bénéfice)</field>
|
||||
<field name="code">120</field>
|
||||
<field name="user_type_id" ref="account.data_unaffected_earnings"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_06" model="account.account.tag">
|
||||
<field name="name">1c. Leveringen/diensten belast met overige tarieven behalve 0% (omzet)</field>
|
||||
<field name="name">1c. Leveringen/diensten belast met overige tarieven, behalve 0% (omzet)</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_07" model="account.account.tag">
|
||||
@@ -174,7 +174,7 @@
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_10" model="account.account.tag">
|
||||
<field name="name">2a. Leveringen/diensten waarbij de heffing van omzetbelasting naar u is verlegd (omzet)</field>
|
||||
<field name="name">2a. Leveringen/diensten waarbij de omzetbelasting naar u is verlegd (omzet)</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_11" model="account.account.tag">
|
||||
@@ -187,11 +187,11 @@
|
||||
</record>
|
||||
<!-- Keep tag_nl_40 & tag_nl_41 to avoid migration issue. -->
|
||||
<record id="tag_nl_40" model="account.account.tag">
|
||||
<field name="name">3b. Leveringen naar/diensten in landen binnen de EU (omzet) (producten)</field>
|
||||
<field name="name">3b. Leveringen naar of diensten in landen binnen de EU (omzet) (producten)</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_41" model="account.account.tag">
|
||||
<field name="name">3b. Leveringen naar/diensten in landen binnen de EU (omzet) (diensten)</field>
|
||||
<field name="name">3b. Leveringen naar of diensten in landen binnen de EU (omzet) (diensten)</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_14" model="account.account.tag">
|
||||
@@ -231,7 +231,7 @@
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_23" model="account.account.tag">
|
||||
<field name="name">1c. Leveringen/diensten belast met overige tarieven behalve 0% (BTW)</field>
|
||||
<field name="name">1c. Leveringen/diensten belast met overige tarieven, behalve 0% (BTW)</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_24" model="account.account.tag">
|
||||
@@ -247,7 +247,7 @@
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_27" model="account.account.tag">
|
||||
<field name="name">2a. Leveringen/diensten waarbij de heffing van Heffing van omzetbelasting naar u is verlegd (BTW)</field>
|
||||
<field name="name">2a. Leveringen/diensten waarbij de omzetbelasting naar u is verlegd (BTW)</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tag_nl_28" model="account.account.tag">
|
||||
@@ -297,3 +297,4 @@
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
|
||||
@@ -331,8 +331,6 @@
|
||||
<field name="description">21% BTW verlegd</field>
|
||||
<field eval="21" name="amount"/>
|
||||
<field name="amount_type">group</field>
|
||||
<field name="refund_account_id" ref="vat_payable_v"/>
|
||||
<field name="account_id" ref="vat_payable_v"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tag_nl_10')])]"/>
|
||||
<field name="children_tax_ids" eval="[(6, 0, [ref('btw_ink_0_1'), ref('btw_ink_0_2')])]"/>
|
||||
|
||||
@@ -264,8 +264,8 @@
|
||||
Best regards,
|
||||
</div>
|
||||
<div>&nbsp;</div>
|
||||
<div t-if="record.user_id and record.user_id.signature" style="font-size: 13px;">
|
||||
<div t-raw="record.user_id.signature"/>
|
||||
<div t-if="record.user_id and record.user_id.sudo().signature" style="font-size: 13px;">
|
||||
<div t-raw="record.user_id.sudo().signature"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <admin@yourcompany.example.com>",
|
||||
"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: <a id="url0" href="https://www.odoo.tz/my/${object.name}">https://www.odoo.tz/my/${object.name}</h1>
|
||||
Website1: <a id="url1" href="https://www.odoo.be">https://www.odoo.be</h1>
|
||||
Website2: <a id="url2" href="https://${url}">https://${url}</h1>
|
||||
Website3: <a id="url3" href="${httpurl}">${httpurl}</h1>
|
||||
Email: <a id="url4" href="mailto:test@odoo.com">test@odoo.com</h1>
|
||||
""",
|
||||
"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)
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -128,8 +128,8 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="mo_id" domain="[('state','=','done')]"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations" domain="[('usage','=','internal')]"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations" domain="[('usage','=','internal')]"/>
|
||||
<field name="has_tracking" invisible="1"/>
|
||||
<field name="lot_id" attrs="{'invisible': [('has_tracking', '=', 'none')], 'required': [('has_tracking', '!=', 'none')]}" groups="stock.group_production_lot"/>
|
||||
</group>
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
<div class="col-6 o_kanban_primary_left">
|
||||
<div class="btn-group" name="o_wo">
|
||||
<t t-if="record.workorder_count.raw_value > 0">
|
||||
<button class="btn btn-primary" name="%(action_work_orders)d" type="action" context="{'search_default_ready': 1, 'search_default_progress': 1, 'search_default_pending': 1}">
|
||||
<button class="btn btn-primary" name="%(action_work_orders)d" type="action" context="{'search_default_ready': 1, 'search_default_progress': 1}">
|
||||
<span>WORK ORDERS</span>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<!-- TDE FIXME: was customize_show=True -->
|
||||
<template id="pay_meth_link" inherit_id="portal.portal_layout">
|
||||
<xpath expr="//div[hasclass('o_portal_my_details')]" position="inside">
|
||||
<t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False), ('payment_flow', '=', 's2s')])">
|
||||
<t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False), ('payment_flow', '=', 's2s'), ('company_id', '=', request.env.user.company_id.id)])">
|
||||
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your payment methods</a></div>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
@@ -583,21 +583,21 @@ class PosOrder(models.Model):
|
||||
|
||||
@api.onchange('statement_ids', 'lines')
|
||||
def _onchange_amount_all(self):
|
||||
amounts = {order_id: {'paid': 0, 'return': 0, 'untaxed': 0} for order_id in self.ids}
|
||||
amounts = {order_id: {'paid': 0, 'return': 0, 'taxed': 0, 'taxes': 0} for order_id in self.ids}
|
||||
for order in self.env['account.bank.statement.line'].read_group([('pos_statement_id', 'in', self.ids)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
|
||||
amounts[order['pos_statement_id'][0]]['paid'] = order['amount']
|
||||
for order in self.env['account.bank.statement.line'].read_group(['&', ('pos_statement_id', 'in', self.ids), ('amount', '<', 0)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
|
||||
amounts[order['pos_statement_id'][0]]['return'] = order['amount']
|
||||
for order in self.env['pos.order.line'].read_group([('order_id', 'in', self.ids)], ['order_id', 'price_subtotal'], ['order_id']):
|
||||
amounts[order['order_id'][0]]['untaxed'] = order['price_subtotal']
|
||||
for order in self.env['pos.order.line'].read_group([('order_id', 'in', self.ids)], ['order_id', 'price_subtotal', 'price_subtotal_incl'], ['order_id']):
|
||||
amounts[order['order_id'][0]]['taxed'] = order['price_subtotal_incl']
|
||||
amounts[order['order_id'][0]]['taxes'] = order['price_subtotal_incl'] - order['price_subtotal']
|
||||
|
||||
for order in self:
|
||||
currency = order.pricelist_id.currency_id
|
||||
order.amount_paid = amounts[order.id]['paid']
|
||||
order.amount_return = amounts[order.id]['return']
|
||||
order.amount_tax = currency.round(sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines))
|
||||
amount_untaxed = currency.round(amounts[order.id]['untaxed'])
|
||||
order.amount_total = order.amount_tax + amount_untaxed
|
||||
order.amount_tax = currency.round(amounts[order.id]['taxes'])
|
||||
order.amount_total = currency.round(amounts[order.id]['taxed'])
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id(self):
|
||||
|
||||
@@ -876,12 +876,20 @@ exports.PosModel = Backbone.Model.extend({
|
||||
transfer.pipe(function(order_server_id){
|
||||
|
||||
// generate the pdf and download it
|
||||
self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
|
||||
active_ids:order_server_id,
|
||||
}}).done(function () {
|
||||
invoiced.resolve();
|
||||
done.resolve();
|
||||
});
|
||||
if (order_server_id.length) {
|
||||
self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
|
||||
active_ids:order_server_id,
|
||||
}}).done(function () {
|
||||
invoiced.resolve();
|
||||
done.resolve();
|
||||
});
|
||||
} else {
|
||||
// The order has been pushed separately in batch when
|
||||
// the connection came back.
|
||||
// The user has to go to the backend to print the invoice
|
||||
invoiced.reject({code:401, message:'Backend Invoice', data:{order: order}});
|
||||
done.reject();
|
||||
}
|
||||
});
|
||||
|
||||
return done;
|
||||
|
||||
@@ -2092,6 +2092,17 @@ var PaymentScreenWidget = ScreenWidget.extend({
|
||||
self.gui.show_screen('clientlist');
|
||||
},
|
||||
});
|
||||
} else if (error.message === 'Backend Invoice') {
|
||||
self.gui.show_popup('confirm',{
|
||||
'title': _t('Please print the invoice from the backend'),
|
||||
'body': _t('The order has been synchronized earlier. Please make the invoice from the backend for the order: ') + error.data.order.name,
|
||||
confirm: function () {
|
||||
this.gui.show_screen('receipt');
|
||||
},
|
||||
cancel: function () {
|
||||
this.gui.show_screen('receipt');
|
||||
},
|
||||
});
|
||||
} else if (error.code < 0) { // XmlHttpRequest Errors
|
||||
self.gui.show_popup('error',{
|
||||
'title': _t('The order could not be sent'),
|
||||
|
||||
@@ -159,6 +159,19 @@ li > p {
|
||||
.container-fluid .container-fluid {
|
||||
@extend %o-double-container-no-padding;
|
||||
}
|
||||
#wrap {
|
||||
> .container, > .container-fluid {
|
||||
// BS3 used to do this on all containers so that margins and floats are
|
||||
// cleared inside containers. As lots of current odoo layouts may rely
|
||||
// on this for some alignments, this is restored (at least for a while)
|
||||
// here only for main containers of the frontend.
|
||||
&::before, &::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
}
|
||||
[class^="col-lg-"] {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@
|
||||
<t t-if='prev_record or next_record'>
|
||||
<div class="record_pager btn-group" role="group">
|
||||
<a role="button" t-att-class="'btn btn-link %s' % ('disabled' if not prev_record else '')" t-att-href="prev_record or '#'" ><i class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous"></i></a>
|
||||
<a role="button" t-att-class="'btn btn-link %s' % ('disabled' if not next_record else '')" t-att-href="next_record or '#'" ><i class="fa fa-chevron-right" role="img" aria-label="Next" title="Previous"></i></a>
|
||||
<a role="button" t-att-class="'btn btn-link %s' % ('disabled' if not next_record else '')" t-att-href="next_record or '#'" ><i class="fa fa-chevron-right" role="img" aria-label="Next" title="Next"></i></a>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,75 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="report_simple_label">
|
||||
<div class="col-4" style="padding:0;">
|
||||
<table class="table table-condensed" style="border-bottom: 0px solid white !important;width: 3in;">
|
||||
<tr>
|
||||
<th style="text-align: left;">
|
||||
<strong t-field="product.display_name"/>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Price:</strong>
|
||||
<strong t-field="product.lst_price" t-options="{'widget': 'monetary', 'display_currency': product.company_id.currency_id}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-if="product.barcode">
|
||||
<tr>
|
||||
<td style="text-align: center; vertical-align: middle;" class="col-5">
|
||||
<img alt="Barcode" t-if="len(product.barcode) == 13" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
|
||||
<img alt="Barcode" t-elif="len(product.barcode) == 8" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN8', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
|
||||
<img alt="Barcode" t-else="" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
|
||||
<span t-field="product.barcode"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template id="report_productlabel">
|
||||
<t t-call="web.basic_layout">
|
||||
<div class="page">
|
||||
<t t-foreach="docs" t-as="product">
|
||||
<t t-call="product.report_simple_label">
|
||||
<t t-set="product" t-value="product"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="report_simple_barcode">
|
||||
<div class="col-4" style="padding:0;">
|
||||
<table class="table table-condensed" style="border-bottom: 0px solid white !important;width: 3in;">
|
||||
<tr>
|
||||
<th style="text-align: left;">
|
||||
<strong t-field="product.display_name"/>
|
||||
</th>
|
||||
</tr>
|
||||
<t t-if="product.barcode">
|
||||
<tr>
|
||||
<td style="text-align: center; vertical-align: middle;" class="col-5">
|
||||
<img alt="Barcode" t-if="len(product.barcode) == 13" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
|
||||
<img alt="Barcode" t-elif="len(product.barcode) == 8" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN8', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
|
||||
<img alt="Barcode" t-else="" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
|
||||
<span t-field="product.barcode"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template id="report_productbarcode">
|
||||
<t t-call="web.basic_layout">
|
||||
<div class="page">
|
||||
<t t-foreach="docs" t-as="product">
|
||||
<t t-call="product.report_simple_barcode">
|
||||
<t t-set="product" t-value="product"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="report_simple_label">
|
||||
<div style="width: 32%; display: inline-table; height:14rem;">
|
||||
<table class="table table-bordered mb-0" style="border: 2px solid black;">
|
||||
<tr>
|
||||
<th class="table-active text-left" style="height: 4rem;">
|
||||
<strong t-field="product.display_name"/>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 2rem">
|
||||
<strong>Price:</strong>
|
||||
<strong t-field="product.lst_price" t-options="{'widget': 'monetary', 'display_currency': product.company_id.currency_id}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center align-middle" style="height: 6rem">
|
||||
<t t-if="product.barcode">
|
||||
<img alt="Barcode" t-if="len(product.barcode) == 13" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height::4rem;"/>
|
||||
<img alt="Barcode" t-elif="len(product.barcode) == 8" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN8', product.barcode, 600, 150)" style="width:100%;height::4rem;"/>
|
||||
<img alt="Barcode" t-else="" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', product.barcode, 600, 150)" style="width:100%;height::4rem;"/>
|
||||
<span t-field="product.barcode"/>
|
||||
</t>
|
||||
<t t-else=""><span class="text-muted">No barcode available</span></t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="report_productlabel">
|
||||
<t t-call="web.basic_layout">
|
||||
<div class="page">
|
||||
<t t-foreach="docs" t-as="product">
|
||||
<t t-call="product.report_simple_label">
|
||||
<t t-set="product" t-value="product"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_simple_barcode">
|
||||
<div style="width: 32%; display: inline-table; height: 10rem;">
|
||||
<table class="table table-bordered mb-0" style="border: 2px solid black;">
|
||||
<tr>
|
||||
<th class="table-active text-left" style="height: 4rem;">
|
||||
<strong t-field="product.display_name"/>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center align-middle" style="height: 6rem;">
|
||||
<t t-if="product.barcode">
|
||||
<img alt="Barcode" t-if="len(product.barcode) == 13" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height:4rem;"/>
|
||||
<img alt="Barcode" t-elif="len(product.barcode) == 8" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN8', product.barcode, 600, 150)" style="width:100%;height:4rem;"/>
|
||||
<img alt="Barcode" t-else="" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', product.barcode, 600, 150)" style="width:100%;height:4rem"/>
|
||||
<span t-field="product.barcode"/>
|
||||
</t>
|
||||
<t t-else=""><span class="text-muted">No barcode available</span></t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="report_productbarcode">
|
||||
<t t-call="web.basic_layout">
|
||||
<div class="page">
|
||||
<t t-foreach="docs" t-as="product">
|
||||
<t t-call="product.report_simple_barcode">
|
||||
<t t-set="product" t-value="product"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template" ref="action_report_saleorder"/>
|
||||
<field name="report_name">${(object.name or '').replace('/','_')}${object.state == 'draft' and '_draft' or ''}</field>
|
||||
<field name="report_name">${(object.name or '').replace('/','_')}</field>
|
||||
<field name="lang">${object.partner_id.lang}</field>
|
||||
<field name="user_signature" eval="False"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
<menuitem id="sale_order_menu"
|
||||
@@ -17,6 +16,7 @@
|
||||
<menuitem id="report_sales_team"
|
||||
name="Sales Teams"
|
||||
parent="sale_order_menu"
|
||||
groups="sales_team.group_sale_manager"
|
||||
action="sales_team.crm_team_salesteams_act"
|
||||
sequence="3"/>
|
||||
|
||||
@@ -314,6 +314,7 @@
|
||||
<field name="qty_delivered_manual" invisible="1"/>
|
||||
<field name="qty_delivered_method" invisible="1"/>
|
||||
<field name="price_total" invisible="1"/>
|
||||
<field name="price_tax" invisible="1"/>
|
||||
<field name="price_subtotal" invisible="1"/>
|
||||
<label for="product_uom_qty" string="Ordered Quantity"/>
|
||||
<div>
|
||||
@@ -322,6 +323,7 @@
|
||||
name="product_uom_qty" class="oe_inline"/>
|
||||
<field
|
||||
name="product_uom"
|
||||
force_save="1"
|
||||
groups="uom.group_uom"
|
||||
class="oe_inline oe_no_button"
|
||||
attrs="{
|
||||
@@ -442,6 +444,7 @@
|
||||
<field name="qty_to_invoice" invisible="1"/>
|
||||
<field
|
||||
name="product_uom"
|
||||
force_save="1"
|
||||
attrs="{
|
||||
'readonly': [('state', 'in', ('sale','done', 'cancel'))],
|
||||
'required': [('display_type', '=', False)],
|
||||
@@ -473,6 +476,7 @@
|
||||
<field name="invoice_status" invisible="1"/>
|
||||
<field name="customer_lead" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="price_tax" invisible="1"/>
|
||||
</tree>
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name"/>
|
||||
@@ -480,6 +484,7 @@
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_uom" groups="uom.group_uom"/>
|
||||
<field name="price_subtotal"/>
|
||||
<field name="price_tax" invisible="1"/>
|
||||
<field name="price_total" invisible="1"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="display_type"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<t t-set="no_reserved_product" t-value="o.move_lines.filtered(lambda x: x.product_uom_qty != x.reserved_availability and x.move_line_ids and x.state!='done')"/>
|
||||
<p t-if="o.state in ['draft', 'waiting', 'confirmed'] or no_reserved_product" class="fa fa-exclamation-triangle">
|
||||
<p t-if="o.state in ['draft', 'waiting', 'confirmed'] or no_reserved_product"><i class="fa fa-exclamation-triangle" />
|
||||
All products could not be reserved. Click on the "Check Availability" button to try to reserve products
|
||||
</p>
|
||||
<p t-field="o.note"/>
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
<button name="%(act_stock_return_picking)d" string="Return" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('is_locked', '=', False)]}" type="action" groups="base.group_user"/>
|
||||
<button name="action_cancel" attrs="{'invisible': ['|', ('state', 'not in', ('assigned', 'confirmed', 'partially_available', 'draft', 'waiting')), ('is_locked', '=', False)]}" string="Cancel" groups="base.group_user" type="object"/>
|
||||
<field name="picking_type_code" invisible="1"/>
|
||||
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', '|', '|', ('picking_type_code', '=', 'incoming'), ('state', 'not in', ('assigned', 'partially_available')), ('is_locked', '=', False), ('immediate_transfer', '=', True)]}"/>
|
||||
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', '|', '|', '|', ('picking_type_code', '=', 'incoming'), ('immediate_transfer', '=', True), ('is_locked', '=', False), '&', ('state', 'not in', ('assigned', 'partially_available')), ('move_type', '!=', 'one'), '&', ('state', 'not in', ('assigned', 'partially_available', 'confirmed')), ('move_type', '=', 'one')]}"/>
|
||||
<field name="show_operations" invisible="1" readonly="1"/>
|
||||
<field name="move_line_exist" invisible="1"/>
|
||||
<field name="has_packages" invisible="1"/>
|
||||
|
||||
@@ -138,23 +138,38 @@ class ProductProduct(models.Model):
|
||||
StockMove = self.env['stock.move']
|
||||
to_date = self.env.context.get('to_date')
|
||||
|
||||
self.env['account.move.line'].check_access_rights('read')
|
||||
fifo_automated_values = {}
|
||||
query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
|
||||
FROM account_move_line AS aml
|
||||
WHERE aml.product_id IS NOT NULL AND aml.company_id=%%s %s
|
||||
GROUP BY aml.product_id, aml.account_id"""
|
||||
params = (self.env.user.company_id.id,)
|
||||
if to_date:
|
||||
query = query % ('AND aml.date <= %s',)
|
||||
params = params + (to_date,)
|
||||
else:
|
||||
query = query % ('',)
|
||||
self.env.cr.execute(query, params=params)
|
||||
real_time_product_ids = [product.id for product in self if product.product_tmpl_id.valuation == 'real_time']
|
||||
if real_time_product_ids:
|
||||
self.env['account.move.line'].check_access_rights('read')
|
||||
fifo_automated_values = {}
|
||||
query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
|
||||
FROM account_move_line AS aml
|
||||
WHERE aml.product_id IN %%s AND aml.company_id=%%s %s
|
||||
GROUP BY aml.product_id, aml.account_id"""
|
||||
params = (tuple(real_time_product_ids), self.env.user.company_id.id)
|
||||
if to_date:
|
||||
query = query % ('AND aml.date <= %s',)
|
||||
params = params + (to_date,)
|
||||
else:
|
||||
query = query % ('',)
|
||||
self.env.cr.execute(query, params=params)
|
||||
|
||||
res = self.env.cr.fetchall()
|
||||
for row in res:
|
||||
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
|
||||
res = self.env.cr.fetchall()
|
||||
for row in res:
|
||||
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
|
||||
|
||||
product_values = {product: 0 for product in self}
|
||||
product_move_ids = {product: [] for product in self}
|
||||
if to_date:
|
||||
domain = [('product_id', 'in', self.ids), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
|
||||
for move in StockMove.search(domain).with_context(prefetch_fields=False):
|
||||
product_values[move.product_id] += move.value
|
||||
product_move_ids[move.product_id].append(move.id)
|
||||
else:
|
||||
domain = [('product_id', 'in', self.ids)] + StockMove._get_all_base_domain()
|
||||
for move in StockMove.search(domain).with_context(prefetch_fields=False):
|
||||
product_values[move.product_id] += move.remaining_value
|
||||
product_move_ids[move.product_id].append(move.id)
|
||||
|
||||
for product in self:
|
||||
if product.cost_method in ['standard', 'average']:
|
||||
@@ -170,11 +185,9 @@ class ProductProduct(models.Model):
|
||||
elif product.cost_method == 'fifo':
|
||||
if to_date:
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
|
||||
moves = StockMove.search(domain)
|
||||
product.stock_value = sum(moves.mapped('value'))
|
||||
product.stock_value = product_values[product]
|
||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
|
||||
product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product])
|
||||
elif product.product_tmpl_id.valuation == 'real_time':
|
||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||
@@ -182,7 +195,7 @@ class ProductProduct(models.Model):
|
||||
product.qty_at_date = quantity
|
||||
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
|
||||
else:
|
||||
product.stock_value, moves = product._sum_remaining_values()
|
||||
product.stock_value, moves = product_values[product], StockMove.browse(product_move_ids[product])
|
||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
product.stock_fifo_manual_move_ids = moves
|
||||
|
||||
@@ -387,7 +387,7 @@ class StockMove(models.Model):
|
||||
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
|
||||
rounding = move.product_id.uom_id.rounding
|
||||
|
||||
qty_done = 0.0
|
||||
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
|
||||
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
|
||||
new_std_price = move._get_price_unit()
|
||||
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
|
||||
@@ -396,7 +396,6 @@ class StockMove(models.Model):
|
||||
else:
|
||||
# Get the standard price
|
||||
amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
|
||||
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
|
||||
qty = forced_qty or qty_done
|
||||
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done)
|
||||
|
||||
@@ -483,10 +482,8 @@ class StockMove(models.Model):
|
||||
fifo_valued_products = self.env['product.product']
|
||||
fifo_valued_categories = self.env['product.category'].search([('property_cost_method', '=', 'fifo')])
|
||||
fifo_valued_products |= self.env['product.product'].search([('categ_id', 'child_of', fifo_valued_categories.ids)])
|
||||
moves_to_vacuum = self.env['stock.move']
|
||||
for product in fifo_valued_products:
|
||||
moves_to_vacuum |= self.search(
|
||||
[('product_id', '=', product.id), ('remaining_qty', '<', 0)] + self._get_all_base_domain())
|
||||
moves_to_vacuum = self.search(
|
||||
[('product_id', 'in', fifo_valued_products.ids), ('remaining_qty', '<', 0)] + self._get_all_base_domain())
|
||||
moves_to_vacuum._fifo_vacuum()
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -2587,6 +2587,40 @@ class TestStockValuation(TransactionCase):
|
||||
self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
|
||||
self.assertAlmostEqual(self.product1.stock_value, 0.0)
|
||||
|
||||
def test_average_perpetual_6(self):
|
||||
self.product1.product_tmpl_id.cost_method = 'average'
|
||||
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'Receive 1 unit at 10',
|
||||
'location_id': self.supplier_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 10,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
move1.move_line_ids.qty_done = 1.0
|
||||
|
||||
move2 = self.env['stock.move'].create({
|
||||
'name': 'Receive 1 units at 5',
|
||||
'location_id': self.supplier_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 5,
|
||||
})
|
||||
move2._action_confirm()
|
||||
move2._action_assign()
|
||||
move2.move_line_ids.qty_done = 1.0
|
||||
|
||||
# Receive both at the same time
|
||||
(move1 | move2)._action_done()
|
||||
|
||||
self.assertAlmostEqual(self.product1.standard_price, 7.5)
|
||||
|
||||
def test_average_negative_1(self):
|
||||
""" Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
|
||||
"""
|
||||
|
||||
@@ -307,6 +307,9 @@ class AdjustmentLines(models.Model):
|
||||
return False
|
||||
accounts = self.product_id.product_tmpl_id.get_product_accounts()
|
||||
debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False
|
||||
# If the stock move is dropshipped move we need to get the cost account instead the stock valuation account
|
||||
if self.move_id._is_dropshipped():
|
||||
debit_account_id = accounts.get('expense') and accounts['expense'].id or False
|
||||
already_out_account_id = accounts['stock_output'].id
|
||||
credit_account_id = self.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id
|
||||
|
||||
|
||||
@@ -155,7 +155,15 @@ class TestChannelFeatures(common.BaseFunctionalTest, common.MockEmails):
|
||||
""" Posting a message on a mailing list should send one email to all recipients """
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'schlouby.fr')
|
||||
self.test_channel.write({'email_send': True})
|
||||
self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner)
|
||||
|
||||
# Subscribe an user without email. We shouldn't try to send email to them.
|
||||
nomail = self.env['res.users'].create({
|
||||
"login": "nomail",
|
||||
"name": "No Mail",
|
||||
"email": False,
|
||||
"notification_type": "email",
|
||||
})
|
||||
self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner | nomail.partner_id)
|
||||
self.test_channel.message_post(body="Test", message_type='comment', subtype='mt_comment')
|
||||
|
||||
self.assertEqual(len(self._mails), 1)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user